this
determined by execution context
I am used to the peculiarities of this
in JavaScript. In the following example, this
is determined by the execution context. Even though the getProtoPropViaThis
function is defined on x
, the value of this
is determined by how the function is called:
const xProto = {
protoProp: "x",
};
const x = {
getProtoPropViaThis() {
return this.protoProp;
},
}
Object.setPrototypeOf(x, xProto);
const yProto = {
protoProp: "y",
};
const y = {
getProtoPropViaThis: x.getProtoPropViaThis,
}
Object.setPrototypeOf(y, yProto);
console.log( x.getProtoPropViaThis() ); // Output: x
console.log( y.getProtoPropViaThis() ); // Output: y
super
not affected by execution context?
I've been using super
for a while now, but always in the context of classes. So I was surprised recently when I read an article that demonstrated, tangentially, that super
does not appear to follow the same rules as this
. Somehow, in a way I don't fully understand (despite reading the ECMAScript 2021 language docs several times), super
manages to hang on to its original reference:
const xProto = {
protoProp: "x",
};
const x = {
getProtoPropViaSuper() {
return super.protoProp;
},
}
Object.setPrototypeOf(x, xProto);
const yProto = {
protoProp: "y",
};
const y = {
getProtoPropViaSuper: x.getProtoPropViaSuper,
}
Object.setPrototypeOf(y, yProto);
console.log( x.getProtoPropViaSuper() ); // Output: x
console.log( y.getProtoPropViaSuper() ); // Output: x
Note that y
does not have x
or xProto
anywhere in its prototype chain, and yet it is still xProto
's property that is accessed when calling y.getProtoPropViaSuper()
Difference evident by Object.assign
For another example, the following does not work at all:
const xProto = {
protoProp: "x",
};
const x = Object.assign(Object.create(xProto), {
getProtoPropViaSuper() {
return super.protoProp;
},
});
console.log(x.getProtoPropViaSuper());
The value of super
is Object's prototype, not x
's, so the example above just prints undefined
.
Following the docs
12.3.7.1 Runtime Semantics: Evaluation
SuperProperty :super. IdentifierName
- Let env be GetThisEnvironment().
- Let actualThis be ? env.GetThisBinding().
- Let propertyKey be StringValue of IdentifierName.
- If the code matched by this SuperProperty is strict mode code, let strict be true; else let strict be false.
- Return ? MakeSuperPropertyReference(actualThis, propertyKey, strict).
12.3.7.3 MakeSuperPropertyReference ( actualThis, propertyKey, strict )
The abstract operation MakeSuperPropertyReference takes arguments actualThis, propertyKey, and strict. It performs the following steps when called:
- Let env be GetThisEnvironment().
- Assert: env.HasSuperBinding() is true.
- Let baseValue be ? env.GetSuperBase().
- Let bv be ? RequireObjectCoercible(baseValue).
- Return a value of type Reference that is a Super Reference whose base value component is bv, whose referenced name component is propertyKey, whose thisValue component is actualThis, and whose strict reference flag is strict.
- Let envRec be the function Environment Record for which the method was invoked.
- Let home be envRec.[[HomeObject]].
- If home has the value undefined, return undefined.
- Assert: Type(home) is Object.
- Return ? home.[GetPrototypeOf].
And finally:
8.1.1.3 Function Environment Records, Table 17
[[HomeObject]]
: If the associated function has super property accesses and is not an ArrowFunction, [[HomeObject]] is the object that the function is bound to as a method. The default value for [[HomeObject]] is undefined.
Following this documentation, it seems as if getProtoPropViaSuper
should be bound to y
as a method, but perhaps that binding is somehow stored when the x
object is created and retained even when the function is assigned to y
. However, I haven't been able to parse where or when that's happening from these docs.
I would really appreciate it if someone could explain this behavior in plain language. How does super
determine its value? How does it appear to hold onto its original super
context? If it's holding onto the original object, it seems like that could cause memory leaks as the original object could not be garbage collected. But perhaps the super
reference is determined at quasi-compile time? (I say "quasi" because the reference can still be changed by Object.setPrototypeOf
)