3

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

  1. Let env be GetThisEnvironment().
  2. Let actualThis be ? env.GetThisBinding().
  3. Let propertyKey be StringValue of IdentifierName.
  4. If the code matched by this SuperProperty is strict mode code, let strict be true; else let strict be false.
  5. 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:

  1. Let env be GetThisEnvironment().
  2. Assert: env.HasSuperBinding() is true.
  3. Let baseValue be ? env.GetSuperBase().
  4. Let bv be ? RequireObjectCoercible(baseValue).
  5. 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.

8.1.1.3.5 GetSuperBase ( )

  1. Let envRec be the function Environment Record for which the method was invoked.
  2. Let home be envRec.[[HomeObject]].
  3. If home has the value undefined, return undefined.
  4. Assert: Type(home) is Object.
  5. 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)

JDB
  • 25,172
  • 5
  • 72
  • 123
  • 1
    Don't use `__proto__` as it's [deprecated](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto) – limido Oct 17 '20 at 05:39
  • Thanks, I'm aware. It's just easier to demo this way. The question isn't about proto – JDB Oct 17 '20 at 16:02
  • 1
    @limido Rewritten to avoid the `__proto__` usage. Not sure that it helped the question, but hopefully it's less of a distraction now. – JDB Oct 19 '20 at 15:11
  • I started to think maybe it was a Chromium bug, but I double-checked in Firefox and Safari and get the same behavior, so I guess this is standards-compliant. I just don't understand the standard. – JDB Oct 19 '20 at 16:23
  • 2
    From the link you now added: "As super looks for parent methods in **[[HomeObject]]**.[[Prototype]], that means it searches sayHiMixin.[[Prototype]], not User.[[Prototype]]." Also see [this question referencing specs](https://stackoverflow.com/questions/51165046/why-homeobject-is-different-in-shorthand-syntax-of-method) – limido Oct 19 '20 at 17:55
  • @limido - Thank you! The answer on that question gave me the missing puzzle piece... how `[[HomeObject]]` is set. – JDB Oct 19 '20 at 18:01

1 Answers1

3

perhaps that binding is somehow stored when the x object is created and retained even when the function is assigned to y.

Yes, precisely that is what happens. The getProtoPropViaSuper method basically closes over the object it was defined in. This is stored in the internal [[HomeObject]] slot of the function itself, which is why it is kept if you assign the method to a different object, or - more importantly - inherit it on an a different object1. For method definitions in object literals, it's the object created by the literal; for method definitions in classes, it's the class's .prototype object.

1: For why it needs to be a static reference and not something invocation-dependent like Object.getPrototypeOf(this), see here.

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.

No, it's not causing any more memory leaks than other closures. Sure, the method prevents its home object from being garbage collected, but given that the home object is a prototype object - in normal usage at least - which is also referenced in the prototype chain of the objects on which the method is normally called, this is not an issue.

I would really appreciate it if someone could explain this behavior in plain language. How does super determine its value?

It takes the prototype of its bound home object, which is the object that the method was defined in. Notice however that accessing a property on super doesn't return an ordinary reference to the property of that object, but a special reference which when called (a method) will take the this value of the current scope as the this argument to the method call, not the prototype object. In short,

const x = {
  method() {
    super.something(a, b);
  }
}

desugars to

const x = {
  method() {
    Object.getPrototypeOf(x).something.call(this, a, b);
  }
}

and

class X {
  method() {
    super.something(a, b);
  }
}

desugars to

class X {
  method() {
    Object.getPrototypeOf(X.prototype).something.call(this, a, b);
  }
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thanks. All of the documentation I've read up to this point uses some vague hand-wavy language to say that `super` goes to the object's prototype, but obviously there's a caveat to that... it goes to the prototype of the object in which the function was initially defined. Also, the extra bit about `this` is a helpful callout. Interesting that `Object.setPrototypeOf` will update the `[[HomeObject]]` reference. I guess that's part of the reason that function is so slow. – JDB Oct 19 '20 at 18:41
  • @JDBstillremembersMonica No, it won't update the *[[HomeObject]]* of the function, but given that `super` looks up the prototype of the *[[HomeObject]]* you can of course change what you will get by changing the prototype of the *[[HomeObject]]*. – Bergi Oct 19 '20 at 18:43
  • @JDBstillremembersMonica Originally it was [designed to be more flexible](https://stackoverflow.com/a/27511897/1048572) even… – Bergi Oct 19 '20 at 18:48
  • [Exposing HomeObject](https://hackernoon.com/exposing-homeobject-e61061cbfe17) is a fascinating deep dive into how Chromium actually stores the _[[HomeObject]]_... it's a hidden symbol property on the function. With a little hacking of the source code, you can make it public. Pretty neat. – JDB Oct 20 '20 at 16:49