Reputation: 8661
I totally missed the ES6 revolution and I'm returning to JavaScript after 7 years, to find a host of very strange things happening.
One in particular is the way Function.prototype.bind()
handles class constructors.
Consider this:
// an ES6 class
class class1 {
constructor (p) {
this.property = p;
}
}
var class2 = class1.bind(passer_by);
var class3 = class2.bind(passer_by,3);
class2() // exception, calling a constructor like a function
class3() // idem
console.log (new class1(1)) // class1 {property: 1}
console.log (new class2(2)) // class1 {property: 2}
console.log (new class3() ) // class1 {property: 3}
// An ES5-style pseudo-class
function pseudoclass1 (p) {
this.property = p;
}
var property = 0;
var passer_by = { huh:"???" }
var pseudoclass2 = pseudoclass1.bind(passer_by);
var pseudoclass3 = pseudoclass1.bind(passer_by,3);
pseudoclass1(1); console.log (property) // 1 (this references window)
pseudoclass2(2); console.log (passer_by) // Object { huh: "???", property: 2 }
pseudoclass3() ; console.log (passer_by) // Object { huh: "???", property: 3 }
console.log (new pseudoclass1(1)) // pseudoclass1 {property: 1}
console.log (new pseudoclass2(2)) // pseudoclass1 {property: 2}
console.log (new pseudoclass3() ) // pseudoclass1 {property: 3}
Apparently class2
and class3
are identified as constructors, and class3
is a partial application of class1
that can generate instances with a fixed value of the first parameter.
On the other hand, though they can still act as (poor man's) constructors, the ES5-style functions are indeed served the value of this
set by bind()
, as can be seen when they act on the hapless passer_by
instead of clobbering global variables as does the unbound pseudoclass1
.
Obviously, all these constructors somehow access a value of this
that allows them to construct an object. And yet their this
are supposedly bound to another object.
So I guess there must be some mechanism at work to feed the proper this
to a constructor instead of whatever parameter was passed to bind()
.
Now my problem is, I can find bits of lore about it here and there, even some code apparently from some version of Chrome's V8 (where the function bind() itself seems to do something special about constructors), or a discussion about a cryptic FNop function inserted in the prototype chain, and, if I may add, the occasional piece of cargo cult bu[beep]it.
But what I can't find is an explanation about what is actually going on here, a rationale as to why such a mechanism has been implemented (I mean, with the new spread operator and destructuring and whatnot, wouldn't it be possible to produce the same result (applying some arguments to a constructor) without having to put a moderately documented hack into bind()
?), and its scope (it works for constructors, but are there other sorts of functions that are being fed something else than the value passed to bind()
?)
I tried to read both the 2015 and 2022 ECMA 262 specifications, but had to stop when my brains started leaking out of my ears. I traced back the call stack as:
19.2.3.2
9.4.1.3
9.4.1.2
7.3.13
where something is said about constructors, in a way: "If newTarget is not passed, this operation is equivalent to: new F(...argumentsList)". Aha. So this pseudo-recursive call should allow to emulate a new
somehow... Erf...
I'd be grateful if some kind and savvy soul could give me a better idea of what is going on, show me which part(s) of the ECMA specs deal with this mechanism, or more generally point me in the right direction.
I'm tired of banging my head against a wall, truth be told. This bit of Chrome code that seems to indicate bind()
is doing something special for constructors is just incomprehensible for me. So I would at least like an explanation about it, if everything else fails.
Upvotes: 0
Views: 195
Reputation: 816790
This doesn't have anything to do with classes specifically but with how .bind
works.
You have been on the right track. The most relevan section here is 9.4.1.2.
Just as a recap: ECMAScript distinguishes between two types of functions: callable functions and constructable functions. Function expressions/declarations are both, whereas e.g. class
constructors are only constructable and arrow functions are only callable.
In the specification this is represented by function's internal [[Call]]
and [[Construct]]
methods.
new
will trigger the invocation of the internal [[Construct]]
method.
.bind
will return a new function object with different implementations for [[Call]]
and [[Construct]]
. So what does the "bound" version of [[Construct]]
look like?
9.4.1.2 [[Construct]] ( argumentsList, newTarget )
When the [[Construct]] internal method of a bound function exotic object, F that was created using the bind function is called with a list of arguments argumentsList and newTarget, the following steps are taken:
- Let target be F.[[BoundTargetFunction]].
- Assert: IsConstructor(target) is true.
- Let boundArgs be F.[[BoundArguments]].
- Let args be a new list containing the same values as the list boundArgs in the same order followed by the same values as the list argumentsList in the same order.
- If SameValue(F, newTarget) is true, set newTarget to target.
- Return ? Construct(target, args, newTarget).
What this means is that the bound constructor will "construct" the original function (F.[[BoundTargetFunction]]
) with the bound arguments (F.[[BoundArguments]]
) and the passed in arguments (argumentsList
), but it completely ignores the bound this
value (which would be F.[[BoundThis]]
).
but are there other sorts of functions that are being fed something else than the value passed to bind() ?
Yes, arrow functions. Arrow functions do not have their own this
binding (the value from the closest this
providing environment is used instead), so bound arrow functions also ignore the bound this
value.
Upvotes: 1