mirror of
https://github.com/RGBCube/serenity
synced 2025-07-23 13:27:35 +00:00
LibJS: Fix this values in arrow functions
Also added a large this value test (and strict variant) to ensure this values have no regressions.
This commit is contained in:
parent
151447bdf7
commit
e1573991a3
4 changed files with 889 additions and 4 deletions
|
@ -132,6 +132,7 @@ FunctionEnvironment* OrdinaryFunctionObject::create_environment(FunctionObject&
|
||||||
auto* environment = heap().allocate<FunctionEnvironment>(global_object(), m_environment, variables);
|
auto* environment = heap().allocate<FunctionEnvironment>(global_object(), m_environment, variables);
|
||||||
environment->set_function_object(function_being_invoked);
|
environment->set_function_object(function_being_invoked);
|
||||||
if (m_is_arrow_function) {
|
if (m_is_arrow_function) {
|
||||||
|
environment->set_this_binding_status(FunctionEnvironment::ThisBindingStatus::Lexical);
|
||||||
if (is<FunctionEnvironment>(m_environment))
|
if (is<FunctionEnvironment>(m_environment))
|
||||||
environment->set_new_target(static_cast<FunctionEnvironment*>(m_environment)->new_target());
|
environment->set_new_target(static_cast<FunctionEnvironment*>(m_environment)->new_target());
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
#include <LibJS/Interpreter.h>
|
#include <LibJS/Interpreter.h>
|
||||||
#include <LibJS/Runtime/AbstractOperations.h>
|
#include <LibJS/Runtime/AbstractOperations.h>
|
||||||
#include <LibJS/Runtime/Array.h>
|
#include <LibJS/Runtime/Array.h>
|
||||||
|
#include <LibJS/Runtime/BoundFunction.h>
|
||||||
#include <LibJS/Runtime/Error.h>
|
#include <LibJS/Runtime/Error.h>
|
||||||
#include <LibJS/Runtime/FinalizationRegistry.h>
|
#include <LibJS/Runtime/FinalizationRegistry.h>
|
||||||
#include <LibJS/Runtime/FunctionEnvironment.h>
|
#include <LibJS/Runtime/FunctionEnvironment.h>
|
||||||
|
@ -471,7 +472,7 @@ Value VM::construct(FunctionObject& function, FunctionObject& new_target, Option
|
||||||
if (auto* environment = callee_context.lexical_environment) {
|
if (auto* environment = callee_context.lexical_environment) {
|
||||||
auto& function_environment = verify_cast<FunctionEnvironment>(*environment);
|
auto& function_environment = verify_cast<FunctionEnvironment>(*environment);
|
||||||
function_environment.set_new_target(&new_target);
|
function_environment.set_new_target(&new_target);
|
||||||
if (!this_argument.is_empty()) {
|
if (!this_argument.is_empty() && function_environment.this_binding_status() != FunctionEnvironment::ThisBindingStatus::Lexical) {
|
||||||
function_environment.bind_this_value(global_object, this_argument);
|
function_environment.bind_this_value(global_object, this_argument);
|
||||||
if (exception())
|
if (exception())
|
||||||
return {};
|
return {};
|
||||||
|
@ -603,10 +604,9 @@ void VM::ordinary_call_bind_this(FunctionObject& function, ExecutionContext& cal
|
||||||
auto* local_environment = callee_context.lexical_environment;
|
auto* local_environment = callee_context.lexical_environment;
|
||||||
auto& function_environment = verify_cast<FunctionEnvironment>(*local_environment);
|
auto& function_environment = verify_cast<FunctionEnvironment>(*local_environment);
|
||||||
|
|
||||||
// This is not completely as the spec describes it however without this stuff breaks
|
// This almost as the spec describes it however we sometimes don't have callee_realm when dealing
|
||||||
// (Could be related to the note at https://tc39.es/ecma262/#sec-runtime-semantics-instantiatearrowfunctionexpression )
|
// with proxies and arrow functions however this does seemingly achieve spec like behavior.
|
||||||
if (!callee_realm || this_mode == FunctionObject::ThisMode::Lexical) {
|
if (!callee_realm || this_mode == FunctionObject::ThisMode::Lexical) {
|
||||||
function_environment.bind_this_value(function.global_object(), callee_context.this_value);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -629,6 +629,20 @@ Value VM::call_internal(FunctionObject& function, Value this_value, Optional<Mar
|
||||||
VERIFY(!exception());
|
VERIFY(!exception());
|
||||||
VERIFY(!this_value.is_empty());
|
VERIFY(!this_value.is_empty());
|
||||||
|
|
||||||
|
if (is<BoundFunction>(function)) {
|
||||||
|
auto& bound_function = static_cast<BoundFunction&>(function);
|
||||||
|
auto bound_arguments = bound_function.bound_arguments();
|
||||||
|
if (arguments.has_value())
|
||||||
|
bound_arguments.extend(*arguments);
|
||||||
|
|
||||||
|
MarkedValueList with_bound_arguments { heap() };
|
||||||
|
with_bound_arguments.extend(bound_function.bound_arguments());
|
||||||
|
if (arguments.has_value())
|
||||||
|
with_bound_arguments.extend(*arguments);
|
||||||
|
|
||||||
|
return call_internal(bound_function.target_function(), bound_function.bound_this(), move(with_bound_arguments));
|
||||||
|
}
|
||||||
|
|
||||||
ExecutionContext callee_context;
|
ExecutionContext callee_context;
|
||||||
prepare_for_ordinary_call(function, callee_context, js_undefined());
|
prepare_for_ordinary_call(function, callee_context, js_undefined());
|
||||||
if (exception())
|
if (exception())
|
||||||
|
|
424
Userland/Libraries/LibJS/Tests/this-value-strict.js
Normal file
424
Userland/Libraries/LibJS/Tests/this-value-strict.js
Normal file
|
@ -0,0 +1,424 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Note the globalThisValue and globalObject do not need to be the same.
|
||||||
|
const globalThisValue = this;
|
||||||
|
const globalObject = (0, eval)("this");
|
||||||
|
|
||||||
|
// These tests are done in global state to ensure that is possible
|
||||||
|
const globalArrow = () => {
|
||||||
|
expect(this).toBe(globalThisValue);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
function globalFunction() {
|
||||||
|
expect(this).toBe(undefined);
|
||||||
|
|
||||||
|
expect(globalArrow()).toBe(globalThisValue);
|
||||||
|
|
||||||
|
const arrowInGlobalFunction = () => this;
|
||||||
|
expect(arrowInGlobalFunction()).toBe(undefined);
|
||||||
|
|
||||||
|
return arrowInGlobalFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(globalArrow()).toBe(globalThisValue);
|
||||||
|
expect(globalFunction()()).toBe(undefined);
|
||||||
|
|
||||||
|
const arrowFromGlobalFunction = globalFunction();
|
||||||
|
|
||||||
|
const customThisValue = {
|
||||||
|
isCustomThis: true,
|
||||||
|
variant: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const otherCustomThisValue = {
|
||||||
|
isCustomThis: true,
|
||||||
|
variant: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("describe with arrow function", () => {
|
||||||
|
expect(this).toBe(globalThisValue);
|
||||||
|
|
||||||
|
test("nested test with normal function should get global object", function () {
|
||||||
|
expect(this).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("nested test with arrow function should get same this value as enclosing function", () => {
|
||||||
|
expect(this).toBe(globalThisValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("describe with normal function", function () {
|
||||||
|
expect(this).toBe(undefined);
|
||||||
|
test("nested test with normal function should get global object", function () {
|
||||||
|
expect(this).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("nested test with arrow function should get same this value as enclosing function", () => {
|
||||||
|
expect(this).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("basic behavior", () => {
|
||||||
|
expect(this).toBe(globalThisValue);
|
||||||
|
|
||||||
|
expect(arrowFromGlobalFunction()).toBeUndefined();
|
||||||
|
|
||||||
|
expect(customThisValue).not.toBe(otherCustomThisValue);
|
||||||
|
|
||||||
|
test("binding arrow function does not influence this value", () => {
|
||||||
|
const boundGlobalArrow = globalArrow.bind({ shouldNotBeHere: true });
|
||||||
|
|
||||||
|
expect(boundGlobalArrow()).toBe(globalThisValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
function functionInArrow() {
|
||||||
|
expect(arrowFromGlobalFunction()).toBeUndefined();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
function functionWithArrow() {
|
||||||
|
expect(arrowFromGlobalFunction()).toBeUndefined();
|
||||||
|
return () => {
|
||||||
|
expect(arrowFromGlobalFunction()).toBeUndefined();
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function strictFunction() {
|
||||||
|
"use strict";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
test("functions get globalObject as this value", () => {
|
||||||
|
expect(functionInArrow()).toBeUndefined();
|
||||||
|
expect(functionWithArrow()()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("strict functions get undefined as this value", () => {
|
||||||
|
expect(strictFunction()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bound function gets overwritten this value", () => {
|
||||||
|
const boundFunction = functionInArrow.bind(customThisValue);
|
||||||
|
expect(boundFunction()).toBe(customThisValue);
|
||||||
|
|
||||||
|
const boundFunctionWithArrow = functionWithArrow.bind(customThisValue);
|
||||||
|
expect(boundFunctionWithArrow()()).toBe(customThisValue);
|
||||||
|
|
||||||
|
// However we cannot bind the arrow function itself
|
||||||
|
const failingArrowBound = boundFunctionWithArrow().bind(otherCustomThisValue);
|
||||||
|
expect(failingArrowBound()).toBe(customThisValue);
|
||||||
|
|
||||||
|
const boundStrictFunction = strictFunction.bind(customThisValue);
|
||||||
|
expect(boundStrictFunction()).toBe(customThisValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("functions on created objects", () => {
|
||||||
|
const obj = {
|
||||||
|
func: function () {
|
||||||
|
expect(arrowFromGlobalFunction()).toBeUndefined();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
funcWithArrow: function () {
|
||||||
|
expect(arrowFromGlobalFunction()).toBeUndefined();
|
||||||
|
return () => this;
|
||||||
|
},
|
||||||
|
|
||||||
|
arrow: () => {
|
||||||
|
expect(arrowFromGlobalFunction()).toBeUndefined();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
otherProperty: "yes",
|
||||||
|
};
|
||||||
|
|
||||||
|
test("function get this value of associated object", () => {
|
||||||
|
expect(obj.func()).toBe(obj);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("arrow function on object get above this value", () => {
|
||||||
|
expect(obj.arrow()).toBe(globalThisValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("arrow function from normal function from object has object as this value", () => {
|
||||||
|
expect(obj.funcWithArrow()()).toBe(obj);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bound overwrites value of normal object function", () => {
|
||||||
|
const boundFunction = obj.func.bind(customThisValue);
|
||||||
|
expect(boundFunction()).toBe(customThisValue);
|
||||||
|
|
||||||
|
const boundFunctionWithArrow = obj.funcWithArrow.bind(customThisValue);
|
||||||
|
expect(boundFunctionWithArrow()()).toBe(customThisValue);
|
||||||
|
|
||||||
|
const boundArrowFunction = obj.arrow.bind(customThisValue);
|
||||||
|
expect(boundArrowFunction()).toBe(globalThisValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("also works for object defined in function", () => {
|
||||||
|
(function () {
|
||||||
|
expect(arrowFromGlobalFunction()).toBeUndefined();
|
||||||
|
|
||||||
|
// It is bound below
|
||||||
|
expect(this).toBe(customThisValue);
|
||||||
|
|
||||||
|
const obj2 = {
|
||||||
|
func: function () {
|
||||||
|
expect(arrowFromGlobalFunction()).toBeUndefined();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
arrow: () => {
|
||||||
|
expect(arrowFromGlobalFunction()).toBeUndefined();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
otherProperty: "also",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(obj2.func()).toBe(obj2);
|
||||||
|
expect(obj2.arrow()).toBe(customThisValue);
|
||||||
|
}.bind(customThisValue)());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("behavior with classes", () => {
|
||||||
|
class Basic {
|
||||||
|
constructor(value) {
|
||||||
|
expect(this).toBeInstanceOf(Basic);
|
||||||
|
this.arrowFunctionInClass = () => {
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.value = value;
|
||||||
|
|
||||||
|
expect(arrowFromGlobalFunction()).toBeUndefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
func() {
|
||||||
|
expect(arrowFromGlobalFunction()).toBeUndefined();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const basic = new Basic(14);
|
||||||
|
const basic2 = new Basic(457);
|
||||||
|
|
||||||
|
expect(basic).not.toBe(basic2);
|
||||||
|
|
||||||
|
test("calling functions on class should give instance as this value", () => {
|
||||||
|
expect(basic.func()).toBe(basic);
|
||||||
|
expect(basic2.func()).toBe(basic2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("calling arrow function created in constructor should give instance as this value", () => {
|
||||||
|
expect(basic.arrowFunctionInClass()).toBe(basic);
|
||||||
|
expect(basic2.arrowFunctionInClass()).toBe(basic2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can bind function in class", () => {
|
||||||
|
const boundFunction = basic.func.bind(customThisValue);
|
||||||
|
expect(boundFunction()).toBe(customThisValue);
|
||||||
|
|
||||||
|
const boundFunction2 = basic2.func.bind(otherCustomThisValue);
|
||||||
|
expect(boundFunction2()).toBe(otherCustomThisValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("derived classes behavior", () => {
|
||||||
|
class Base {
|
||||||
|
baseFunction() {
|
||||||
|
expect(arrowFromGlobalFunction()).toBeUndefined();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Derived extends Base {
|
||||||
|
constructor(value) {
|
||||||
|
expect(arrowFromGlobalFunction()).toBeUndefined();
|
||||||
|
const arrowMadeBeforeSuper = () => {
|
||||||
|
expect(this).toBeInstanceOf(Derived);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
super();
|
||||||
|
expect(arrowMadeBeforeSuper()).toBe(this);
|
||||||
|
|
||||||
|
this.arrowMadeBeforeSuper = arrowMadeBeforeSuper;
|
||||||
|
this.arrowMadeAfterSuper = () => {
|
||||||
|
expect(this).toBeInstanceOf(Derived);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
derivedFunction() {
|
||||||
|
expect(arrowFromGlobalFunction()).toBeUndefined();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("can create derived with arrow functions using this before super", () => {
|
||||||
|
const testDerived = new Derived(-89);
|
||||||
|
expect(testDerived.arrowMadeBeforeSuper()).toBe(testDerived);
|
||||||
|
expect(testDerived.arrowMadeAfterSuper()).toBe(testDerived);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("base and derived functions get correct this values", () => {
|
||||||
|
const derived = new Derived(12);
|
||||||
|
|
||||||
|
expect(derived.derivedFunction()).toBe(derived);
|
||||||
|
expect(derived.baseFunction()).toBe(derived);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can bind derived and base functions", () => {
|
||||||
|
const derived = new Derived(846);
|
||||||
|
|
||||||
|
const boundDerivedFunction = derived.derivedFunction.bind(customThisValue);
|
||||||
|
expect(boundDerivedFunction()).toBe(customThisValue);
|
||||||
|
|
||||||
|
const boundBaseFunction = derived.baseFunction.bind(otherCustomThisValue);
|
||||||
|
expect(boundBaseFunction()).toBe(otherCustomThisValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("proxy behavior", () => {
|
||||||
|
test("with no handler it makes no difference", () => {
|
||||||
|
const globalArrowProxyNoHandler = new Proxy(globalArrow, {});
|
||||||
|
expect(globalArrowProxyNoHandler()).toBe(globalThisValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("proxy around global arrow still gives correct this value", () => {
|
||||||
|
let lastThisArg = null;
|
||||||
|
|
||||||
|
const handler = {
|
||||||
|
apply(target, thisArg, argArray) {
|
||||||
|
expect(target).toBe(globalArrow);
|
||||||
|
lastThisArg = thisArg;
|
||||||
|
expect(this).toBe(handler);
|
||||||
|
|
||||||
|
return target(...argArray);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const globalArrowProxy = new Proxy(globalArrow, handler);
|
||||||
|
expect(globalArrowProxy()).toBe(globalThisValue);
|
||||||
|
expect(lastThisArg).toBeUndefined();
|
||||||
|
|
||||||
|
const boundProxy = globalArrowProxy.bind(customThisValue);
|
||||||
|
expect(boundProxy()).toBe(globalThisValue);
|
||||||
|
expect(lastThisArg).toBe(customThisValue);
|
||||||
|
|
||||||
|
expect(globalArrowProxy.call(15)).toBe(globalThisValue);
|
||||||
|
expect(lastThisArg).toBe(15);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("derived classes which access this before super should fail", () => {
|
||||||
|
class Base {}
|
||||||
|
|
||||||
|
test("direct access of this should throw reference error", () => {
|
||||||
|
class IncorrectConstructor extends Base {
|
||||||
|
constructor() {
|
||||||
|
this.something = "this will fail";
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
new IncorrectConstructor();
|
||||||
|
}).toThrowWithMessage(ReferenceError, "|this| has not been initialized");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("access of this via a arrow function", () => {
|
||||||
|
class IncorrectConstructor extends Base {
|
||||||
|
constructor() {
|
||||||
|
const arrow = () => this;
|
||||||
|
arrow();
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
new IncorrectConstructor();
|
||||||
|
}).toThrowWithMessage(ReferenceError, "|this| has not been initialized");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("access of this via a eval", () => {
|
||||||
|
class IncorrectConstructor extends Base {
|
||||||
|
constructor() {
|
||||||
|
eval("this.foo = 'bar'");
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
new IncorrectConstructor();
|
||||||
|
}).toThrowWithMessage(ReferenceError, "|this| has not been initialized");
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip("access of this via a eval in arrow function", () => {
|
||||||
|
class IncorrectConstructor extends Base {
|
||||||
|
constructor() {
|
||||||
|
const arrow = () => eval("() => this")();
|
||||||
|
arrow();
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
new IncorrectConstructor();
|
||||||
|
}).toThrowWithMessage(ReferenceError, "|this| has not been initialized");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("access of this via arrow function even if bound with something else", () => {
|
||||||
|
class IncorrectConstructor extends Base {
|
||||||
|
constructor() {
|
||||||
|
const arrow = () => this;
|
||||||
|
const boundArrow = arrow.bind(customThisValue);
|
||||||
|
boundArrow();
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
new IncorrectConstructor();
|
||||||
|
}).toThrowWithMessage(ReferenceError, "|this| has not been initialized");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("in strict mode primitive this values are not converted to objects", () => {
|
||||||
|
const array = [true, false];
|
||||||
|
|
||||||
|
// Technically the comma is implementation defined here. (Also for tests below.)
|
||||||
|
expect(array.toLocaleString()).toBe("true,false");
|
||||||
|
|
||||||
|
test("directly overwriting toString", () => {
|
||||||
|
let count = 0;
|
||||||
|
Boolean.prototype.toString = function () {
|
||||||
|
count++;
|
||||||
|
return typeof this;
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(array.toLocaleString()).toBe("boolean,boolean");
|
||||||
|
expect(count).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("overwriting toString with a getter", () => {
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
Object.defineProperty(Boolean.prototype, "toString", {
|
||||||
|
get() {
|
||||||
|
count++;
|
||||||
|
const that = typeof this;
|
||||||
|
return function () {
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(array.toLocaleString()).toBe("boolean,boolean");
|
||||||
|
expect(count).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
446
Userland/Libraries/LibJS/Tests/this-value.js
Normal file
446
Userland/Libraries/LibJS/Tests/this-value.js
Normal file
|
@ -0,0 +1,446 @@
|
||||||
|
// Note the globalThisValue and globalObject do not need to be the same.
|
||||||
|
const globalThisValue = this;
|
||||||
|
const globalObject = (0, eval)("this");
|
||||||
|
|
||||||
|
// These tests are done in global state to ensure that is possible
|
||||||
|
const globalArrow = () => {
|
||||||
|
expect(this).toBe(globalThisValue);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
function globalFunction() {
|
||||||
|
expect(this).toBe(globalObject);
|
||||||
|
|
||||||
|
expect(globalArrow()).toBe(globalThisValue);
|
||||||
|
|
||||||
|
const arrowInGlobalFunction = () => this;
|
||||||
|
expect(arrowInGlobalFunction()).toBe(globalObject);
|
||||||
|
|
||||||
|
return arrowInGlobalFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(globalArrow()).toBe(globalThisValue);
|
||||||
|
expect(globalFunction()()).toBe(globalObject);
|
||||||
|
|
||||||
|
const arrowFromGlobalFunction = globalFunction();
|
||||||
|
|
||||||
|
const customThisValue = {
|
||||||
|
isCustomThis: true,
|
||||||
|
variant: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const otherCustomThisValue = {
|
||||||
|
isCustomThis: true,
|
||||||
|
variant: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("describe with arrow function", () => {
|
||||||
|
expect(this).toBe(globalThisValue);
|
||||||
|
|
||||||
|
test("nested test with normal function should get global object", function () {
|
||||||
|
expect(this).toBe(globalObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("nested test with arrow function should get same this value as enclosing function", () => {
|
||||||
|
expect(this).toBe(globalThisValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("describe with normal function", function () {
|
||||||
|
expect(this).toBe(globalObject);
|
||||||
|
test("nested test with normal function should get global object", function () {
|
||||||
|
expect(this).toBe(globalObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("nested test with arrow function should get same this value as enclosing function", () => {
|
||||||
|
expect(this).toBe(globalObject);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("basic behavior", () => {
|
||||||
|
expect(this).toBe(globalThisValue);
|
||||||
|
|
||||||
|
expect(arrowFromGlobalFunction()).toBe(globalObject);
|
||||||
|
|
||||||
|
expect(customThisValue).not.toBe(otherCustomThisValue);
|
||||||
|
|
||||||
|
test("binding arrow function does not influence this value", () => {
|
||||||
|
const boundGlobalArrow = globalArrow.bind({ shouldNotBeHere: true });
|
||||||
|
|
||||||
|
expect(boundGlobalArrow()).toBe(globalThisValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
function functionInArrow() {
|
||||||
|
expect(arrowFromGlobalFunction()).toBe(globalObject);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
function functionWithArrow() {
|
||||||
|
expect(arrowFromGlobalFunction()).toBe(globalObject);
|
||||||
|
return () => {
|
||||||
|
expect(arrowFromGlobalFunction()).toBe(globalObject);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function strictFunction() {
|
||||||
|
"use strict";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
test("functions get globalObject as this value", () => {
|
||||||
|
expect(functionInArrow()).toBe(globalObject);
|
||||||
|
expect(functionWithArrow()()).toBe(globalObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("strict functions get undefined as this value", () => {
|
||||||
|
expect(strictFunction()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bound function gets overwritten this value", () => {
|
||||||
|
const boundFunction = functionInArrow.bind(customThisValue);
|
||||||
|
expect(boundFunction()).toBe(customThisValue);
|
||||||
|
|
||||||
|
const boundFunctionWithArrow = functionWithArrow.bind(customThisValue);
|
||||||
|
expect(boundFunctionWithArrow()()).toBe(customThisValue);
|
||||||
|
|
||||||
|
// However we cannot bind the arrow function itself
|
||||||
|
const failingArrowBound = boundFunctionWithArrow().bind(otherCustomThisValue);
|
||||||
|
expect(failingArrowBound()).toBe(customThisValue);
|
||||||
|
|
||||||
|
const boundStrictFunction = strictFunction.bind(customThisValue);
|
||||||
|
expect(boundStrictFunction()).toBe(customThisValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("functions on created objects", () => {
|
||||||
|
const obj = {
|
||||||
|
func: function () {
|
||||||
|
expect(arrowFromGlobalFunction()).toBe(globalObject);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
funcWithArrow: function () {
|
||||||
|
expect(arrowFromGlobalFunction()).toBe(globalObject);
|
||||||
|
return () => this;
|
||||||
|
},
|
||||||
|
|
||||||
|
arrow: () => {
|
||||||
|
expect(arrowFromGlobalFunction()).toBe(globalObject);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
otherProperty: "yes",
|
||||||
|
};
|
||||||
|
|
||||||
|
test("function get this value of associated object", () => {
|
||||||
|
expect(obj.func()).toBe(obj);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("arrow function on object get above this value", () => {
|
||||||
|
expect(obj.arrow()).toBe(globalThisValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("arrow function from normal function from object has object as this value", () => {
|
||||||
|
expect(obj.funcWithArrow()()).toBe(obj);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bound overwrites value of normal object function", () => {
|
||||||
|
const boundFunction = obj.func.bind(customThisValue);
|
||||||
|
expect(boundFunction()).toBe(customThisValue);
|
||||||
|
|
||||||
|
const boundFunctionWithArrow = obj.funcWithArrow.bind(customThisValue);
|
||||||
|
expect(boundFunctionWithArrow()()).toBe(customThisValue);
|
||||||
|
|
||||||
|
const boundArrowFunction = obj.arrow.bind(customThisValue);
|
||||||
|
expect(boundArrowFunction()).toBe(globalThisValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("also works for object defined in function", () => {
|
||||||
|
(function () {
|
||||||
|
expect(arrowFromGlobalFunction()).toBe(globalObject);
|
||||||
|
|
||||||
|
// It is bound below
|
||||||
|
expect(this).toBe(customThisValue);
|
||||||
|
|
||||||
|
const obj2 = {
|
||||||
|
func: function () {
|
||||||
|
expect(arrowFromGlobalFunction()).toBe(globalObject);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
arrow: () => {
|
||||||
|
expect(arrowFromGlobalFunction()).toBe(globalObject);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
otherProperty: "also",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(obj2.func()).toBe(obj2);
|
||||||
|
expect(obj2.arrow()).toBe(customThisValue);
|
||||||
|
}.bind(customThisValue)());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("behavior with classes", () => {
|
||||||
|
class Basic {
|
||||||
|
constructor(value) {
|
||||||
|
expect(this).toBeInstanceOf(Basic);
|
||||||
|
this.arrowFunctionInClass = () => {
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.value = value;
|
||||||
|
|
||||||
|
expect(arrowFromGlobalFunction()).toBe(globalObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
func() {
|
||||||
|
expect(arrowFromGlobalFunction()).toBe(globalObject);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const basic = new Basic(14);
|
||||||
|
const basic2 = new Basic(457);
|
||||||
|
|
||||||
|
expect(basic).not.toBe(basic2);
|
||||||
|
|
||||||
|
test("calling functions on class should give instance as this value", () => {
|
||||||
|
expect(basic.func()).toBe(basic);
|
||||||
|
expect(basic2.func()).toBe(basic2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("calling arrow function created in constructor should give instance as this value", () => {
|
||||||
|
expect(basic.arrowFunctionInClass()).toBe(basic);
|
||||||
|
expect(basic2.arrowFunctionInClass()).toBe(basic2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can bind function in class", () => {
|
||||||
|
const boundFunction = basic.func.bind(customThisValue);
|
||||||
|
expect(boundFunction()).toBe(customThisValue);
|
||||||
|
|
||||||
|
const boundFunction2 = basic2.func.bind(otherCustomThisValue);
|
||||||
|
expect(boundFunction2()).toBe(otherCustomThisValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("derived classes behavior", () => {
|
||||||
|
class Base {
|
||||||
|
baseFunction() {
|
||||||
|
expect(arrowFromGlobalFunction()).toBe(globalObject);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Derived extends Base {
|
||||||
|
constructor(value) {
|
||||||
|
expect(arrowFromGlobalFunction()).toBe(globalObject);
|
||||||
|
const arrowMadeBeforeSuper = () => {
|
||||||
|
expect(this).toBeInstanceOf(Derived);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
super();
|
||||||
|
expect(arrowMadeBeforeSuper()).toBe(this);
|
||||||
|
|
||||||
|
this.arrowMadeBeforeSuper = arrowMadeBeforeSuper;
|
||||||
|
this.arrowMadeAfterSuper = () => {
|
||||||
|
expect(this).toBeInstanceOf(Derived);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
derivedFunction() {
|
||||||
|
expect(arrowFromGlobalFunction()).toBe(globalObject);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("can create derived with arrow functions using this before super", () => {
|
||||||
|
const testDerived = new Derived(-89);
|
||||||
|
expect(testDerived.arrowMadeBeforeSuper()).toBe(testDerived);
|
||||||
|
expect(testDerived.arrowMadeAfterSuper()).toBe(testDerived);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("base and derived functions get correct this values", () => {
|
||||||
|
const derived = new Derived(12);
|
||||||
|
|
||||||
|
expect(derived.derivedFunction()).toBe(derived);
|
||||||
|
expect(derived.baseFunction()).toBe(derived);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can bind derived and base functions", () => {
|
||||||
|
const derived = new Derived(846);
|
||||||
|
|
||||||
|
const boundDerivedFunction = derived.derivedFunction.bind(customThisValue);
|
||||||
|
expect(boundDerivedFunction()).toBe(customThisValue);
|
||||||
|
|
||||||
|
const boundBaseFunction = derived.baseFunction.bind(otherCustomThisValue);
|
||||||
|
expect(boundBaseFunction()).toBe(otherCustomThisValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("proxy behavior", () => {
|
||||||
|
test("with no handler it makes no difference", () => {
|
||||||
|
const globalArrowProxyNoHandler = new Proxy(globalArrow, {});
|
||||||
|
expect(globalArrowProxyNoHandler()).toBe(globalThisValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("proxy around global arrow still gives correct this value", () => {
|
||||||
|
let lastThisArg = null;
|
||||||
|
|
||||||
|
const handler = {
|
||||||
|
apply(target, thisArg, argArray) {
|
||||||
|
expect(target).toBe(globalArrow);
|
||||||
|
lastThisArg = thisArg;
|
||||||
|
expect(this).toBe(handler);
|
||||||
|
|
||||||
|
return target(...argArray);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const globalArrowProxy = new Proxy(globalArrow, handler);
|
||||||
|
expect(globalArrowProxy()).toBe(globalThisValue);
|
||||||
|
expect(lastThisArg).toBeUndefined();
|
||||||
|
|
||||||
|
const boundProxy = globalArrowProxy.bind(customThisValue);
|
||||||
|
expect(boundProxy()).toBe(globalThisValue);
|
||||||
|
expect(lastThisArg).toBe(customThisValue);
|
||||||
|
|
||||||
|
expect(globalArrowProxy.call(15)).toBe(globalThisValue);
|
||||||
|
expect(lastThisArg).toBe(15);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("derived classes which access this before super should fail", () => {
|
||||||
|
class Base {}
|
||||||
|
|
||||||
|
test("direct access of this should throw reference error", () => {
|
||||||
|
class IncorrectConstructor extends Base {
|
||||||
|
constructor() {
|
||||||
|
this.something = "this will fail";
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
new IncorrectConstructor();
|
||||||
|
}).toThrowWithMessage(ReferenceError, "|this| has not been initialized");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("access of this via a arrow function", () => {
|
||||||
|
class IncorrectConstructor extends Base {
|
||||||
|
constructor() {
|
||||||
|
const arrow = () => this;
|
||||||
|
arrow();
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
new IncorrectConstructor();
|
||||||
|
}).toThrowWithMessage(ReferenceError, "|this| has not been initialized");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("access of this via a eval", () => {
|
||||||
|
class IncorrectConstructor extends Base {
|
||||||
|
constructor() {
|
||||||
|
eval("this.foo = 'bar'");
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
new IncorrectConstructor();
|
||||||
|
}).toThrowWithMessage(ReferenceError, "|this| has not been initialized");
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip("access of this via a eval in arrow function", () => {
|
||||||
|
class IncorrectConstructor extends Base {
|
||||||
|
constructor() {
|
||||||
|
const arrow = () => eval("() => this")();
|
||||||
|
arrow();
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
new IncorrectConstructor();
|
||||||
|
}).toThrowWithMessage(ReferenceError, "|this| has not been initialized");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("access of this via arrow function even if bound with something else", () => {
|
||||||
|
class IncorrectConstructor extends Base {
|
||||||
|
constructor() {
|
||||||
|
const arrow = () => this;
|
||||||
|
const boundArrow = arrow.bind(customThisValue);
|
||||||
|
boundArrow();
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
new IncorrectConstructor();
|
||||||
|
}).toThrowWithMessage(ReferenceError, "|this| has not been initialized");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("with statements", () => {
|
||||||
|
test("this value is still the global object", () => {
|
||||||
|
const obj = { haveValue: true, hello: "friends" };
|
||||||
|
with (obj) {
|
||||||
|
expect(this).toBe(globalThisValue);
|
||||||
|
expect(hello).toBe("friends");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("with gets this value form outer scope", () => {
|
||||||
|
const obj = { haveValue: true, hello: "friends" };
|
||||||
|
|
||||||
|
function callme() {
|
||||||
|
with (obj) {
|
||||||
|
expect(this).toBe(customThisValue);
|
||||||
|
expect(hello).toBe("friends");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const boundMe = callme.bind(customThisValue);
|
||||||
|
boundMe();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("in non strict mode primitive this values are converted to objects", () => {
|
||||||
|
const array = [true, false];
|
||||||
|
|
||||||
|
// Technically the comma is implementation defined here. (Also for tests below.)
|
||||||
|
expect(array.toLocaleString()).toBe("true,false");
|
||||||
|
|
||||||
|
test("directly overwriting toString", () => {
|
||||||
|
let count = 0;
|
||||||
|
Boolean.prototype.toString = function () {
|
||||||
|
count++;
|
||||||
|
return typeof this;
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(array.toLocaleString()).toBe("object,object");
|
||||||
|
expect(count).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("overwriting toString with a getter", () => {
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
Object.defineProperty(Boolean.prototype, "toString", {
|
||||||
|
get() {
|
||||||
|
count++;
|
||||||
|
const that = typeof this;
|
||||||
|
return function () {
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(array.toLocaleString()).toBe("object,object");
|
||||||
|
expect(count).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue