0

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.

kuroi neko
  • 8,479
  • 1
  • 19
  • 43
  • In `new class1(1)`, `this` has no affect on deciding what constructor to call, because `this` is the thing being created. What is leading you to expect `new (class1.bind(null))(1)` to use `null` instead of the bound function itself (the object returned from `.bind(null)`, not the argument passed when you called it) when deciding what is going to run when constructing the instance? – loganfsmyth Jul 28 '21 at 05:02
  • I don't really expect anything, I just would like to know how that works. The way normal bound functions behave, the value set by `bind()` is used. When invoked through `new`, it's different. I see terabytes of gloss about "this", and not a word about its value in a `new` invocation context. I don't even see a bit of specification or official rationale that talks about it. And I find the interface of `bind()` a bit confusing (the "this" value is ignored for constructors). I've modified my example to try to make that clearer. – kuroi neko Jul 28 '21 at 06:44

1 Answers1

1

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:

  1. Let target be F.[[BoundTargetFunction]].
  2. Assert: IsConstructor(target) is true.
  3. Let boundArgs be F.[[BoundArguments]].
  4. 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.
  5. If SameValue(F, newTarget) is true, set newTarget to target.
  6. 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.

Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
  • Thank you so much, Sir. I wouldn't want to overstay my welcome, but would you be OK to elaborate some more if I edited my post to ask for some clarifications, or should I rather post new questions? – kuroi neko Jul 28 '21 at 09:05
  • Depends on the extend of it :D You can also just comment on my answer with your questions. – Felix Kling Jul 28 '21 at 09:12
  • Well, I would very much like to test a naive re-implementation of `bind()` just to see what the intrinsic `bind()` does that a few spread operators and a call to apply() can't achieve. I mean, just at a conceptual level. I assume `bind()` will perform very useful sanity checks and probably run faster than a handmade replacement. It's just to get my bearings. There are so many fairytales about how JS engines are supposed to work sloshing around the Internet that I feel the need to get to the bottom of this story. – kuroi neko Jul 28 '21 at 09:21
  • @kuroineko: A lot of these things are just for convenience. You could also implement `new` yourself or the way `class` works (at last the basic behavior). Also keep in mind that `.bind` existed way before spread arguments and rest parameters or `Reflect`. It might be easier to implement now, but I don't think there is an easy way to distinguish between *constructor* calls and *"normal"* calls. – Felix Kling Jul 28 '21 at 09:33
  • Couldn't agree more. I didn't mean to sound dismissive or anything. I appreciated bind() back in the ES5 days, but I don't remember people hijacking it to do some kind of obscure lambda-calculus :D. I suppose getting rid of `bind()` but still using `call()` or `apply()` might do the trick. These two are still able to perform the bit of deep magic that sends the value of `this` where the runtime wants it, aren't they? Anyway, I'll see how that turns out and post a new question if I come up with useful results. – kuroi neko Jul 28 '21 at 09:47
  • In general I agree, but there is a lot of nuance: Yes, you can probably replace a call to `.bind` with a new function that simply uses `.call()` or `.apply()`. But those cannot be used with `class` constructors. So you could use `function(...args) { return new Cls(...boundArgs, ...args) }` (instead of a call to `.bind`) but that function would also be callable without `new` (even though it will still produce the same result), unlike class constructors bound with `.bind`... as I said, nuances. Do I really care about that? No. But that doesn't mean that it's not important in some other context. – Felix Kling Jul 28 '21 at 10:17
  • I've come up with enough silly code to follow with another question. Now I just have to write it down. Looking forward to more enlightenment :) – kuroi neko Jul 28 '21 at 11:21
  • 1
    "*are there other sorts of functions that are being fed something else? - Yes, arrow functions.*" - I disagree there. `bind` doesn't care whether a function is an arrow function or not. It still does feed the bound thisValue to the call. It's just that the arrow function ignores the thisArgument, in the same way as it ignores it when it's called as a method or using `.call()`. – Bergi Jul 28 '21 at 15:48
  • @Bergi: Fair point. The reason why `this` is not the same as the one passed to `.bind` is different, but they fall into the same category of "doesn't work with .bind like 'normal' functions do". – Felix Kling Jul 28 '21 at 19:18