0

This question is a follow-up to this one. For some reason I'm coming back to JS after 7 years and boy, I can hardly recognize the dear old beast.

Purely for educational purpose, I decided to rewrite naive implementations of the various things Function.prototype.bind() allows to do. It's just an exercise to try to understand what's going on and spark a few questions.

I would be happy to stand corrected for all the mistakes and misunderstandings in my examples and comments.

Also, this code is not mine. I got the idea from a blog and only slightly tweaked it, but unfortunately I lost track of the source. If anyone recognizes the original, I'll be happy to give due credit. Meanwhile, I apologize for the blunder.

Naive binding

The initial idea is simply to do what lambda calculus savvies apparently call a "partial application", i.e. fixing the value of the first parameters of a function, that also accepts an implicit "this" first parameter, like so:

Function.prototype.naive_bind = function (fixed_this, ...fixed_args) {
    const fun = this; // close on the fixed args and the bound function
    return function(...free_args) { // leave the rest of the parameters free
        return fun.call(fixed_this, ...fixed_args, ...free_args);
    }
}

Binding constructors (1st round)

class class1 {
    constructor (p1, p2) {
        this.p1 = p1;
        this.p2 = p2;
    }
}

var innocent_bystander = { "huh?":"what?" }

var class2 = class1.naive_bind(innocent_bystander);
class2 (1,2)                 // exception: class constructor must be invoked with new (as expected)
console.log(new class2(1,2)) // exception: class constructor must be invoked with new (Rats!)

function pseudoclass1 (p1, p2) {
    this.p1 = p1;
    this.p2 = p2;
}
var pseudoclass2 = pseudoclass1.naive_bind(innocent_bystander);
pseudoclass2 (1,2)                 // same exception (as expected)
console.log(new pseudoclass2(1,2)) // same again (at least it's consistent...)

Tonnerre de Brest ! Apparently the runtime is not happy with a mere wrapper based on Function.prototype.call(). It seems the real bind() is adding the dollop of secret sauce needed to give the generated function the appropriate "constructor" flavour (Apparently by "constructor" the ECMA 262 specification does not mean a class constructor, but any function that can be invoked with "new" and use "this" to populate the properties and methods of a freshly created object)

Binding other functions

var purefunction1 = console.log
var purefunction2 = purefunction1.naive_bind (null, "Sure,", "but")
purefunction2 ("you can still", "bind pure", "functions")
// sure, but you can still bind pure functions

// ...and make animals talk (by binding simple methods)
var cat = { speech: "meow" }
var dog = { speech: "woof" }
var fish= { speech: "-" }

var talk = function(count) { console.log ((this.speech+", ").repeat(count-1) + this.speech + "!") }

talk.naive_bind (cat,1)(); // meow! 
talk.naive_bind (dog,1)(); // woof!
talk.naive_bind (cat)(3) // meow, meow, meow!
talk.naive_bind (fish)(10) // -, -, -, -, -, -, -, -, -, -!

// and bind arrow functions
// ("this" is wasted on them, but their parameters can still be bound)

var surprise = (a,b,c) => console.log (this.surprise, a,b,c)
var puzzlement = surprise.naive_bind(innocent_bystander, "don't", "worry");

// "this" refers to window, so we get our own function as first output.
surprise ("where", "am", "I?")  // function surprise(a, b, c) where am I?
// the bound value of this is ignored, but the other arguments are working fine
puzzlement("dude")              // function surprise(a, b, c) don't worry dude        

Apparently, everything works as expected. Or did I miss something?

Binding constructors (2nd round)

We apparently can't get away with passing a wrapper to new, but we can try invoking new directly. Since the value of this is provided by new, the construction-specialized wrapper has only to worry about real constructor parameters.

 Function.prototype.constructor_bind = function (...fixed_args) {
    const fun = this;
    return function(...free_args) {
        return new fun (...fixed_args, ...free_args);
    }
}

var class1_ctor = class1.constructor_bind(1);
console.log (class1_ctor(2)) // class1 { p1:1, p2:2 }

var monster = (a,b) => console.log ("boooh", a, b)
var abomination = monster.constructor_bind(1);
console.log (abomination(2)) // exception: fun is not a constructor (as expected)

Well, that seems to cut it. I imagine the real bind() is far safer and faster, but at least we can reproduce the basic functionalities, namely:

  • providing a fixed value of this to methods
  • doing partial applications on any legit function, though constructors require a specific variant

edit: questions removed to comply with SO policy.

Replaced by just one, the one that matches the title of this post, and that I tried to explore by providing a naive and possibly erroneous equivalent of what I thought the real function did:

Please help me understand how the native Function.prototype.bind() method works, according to the version 6.0 of the ECMAScript specification.

kuroi neko
  • 8,479
  • 1
  • 19
  • 43
  • Please one question per question. – VLAZ Jul 28 '21 at 14:03
  • Well, I plan on editing the post if I get elements to close the final list. – kuroi neko Jul 28 '21 at 14:28
  • 1
    1. Maybe. But probably not. 2. Yes. 3. for functions that don't use `this`, `.bind()` can just serve as partial allocation. There is hardly anything OOP specific in `.bind()`. 4. the performance between what and what exactly? Also see: [Which is faster](https://ericlippert.com/2012/12/17/performance-rant/). 5. Asking multiple and/or open-ended questions is not within the scope of SO. See [help/on-topic]. – VLAZ Jul 28 '21 at 14:33
  • OK, I removed alml the ending questions. Will that do now? – kuroi neko Jul 28 '21 at 15:38
  • "*Please help me understand how this function works.*" what you created or the native `.bind()`? – VLAZ Jul 28 '21 at 15:48
  • 1
    "*Please help me understand how this function works.*" - which function? That native `bind`, your `naive_bind`, or your `constructor_bind`? And what do you not understand about them? It seems you wrote them yourself, and they work as expected, so it's really unclear what you are asking for. – Bergi Jul 28 '21 at 15:54
  • As the title says: "An attempt at understanding what ECMAScript-6 Function.prototype.bind() actually does". English is not my native language, so I apologize if that was unclear. How should I put it? I just tried to provide naive implementations of what I thought the original function did, as a way to test my understanding of its real purpose (about which I am unsure). – kuroi neko Jul 28 '21 at 16:18
  • @kuroineko But that's not a question. If you meant to ask "*Is my attempt at replicating the behavior of `.bind` correct?*", then the simple answer would be **Yes**. – Bergi Jul 28 '21 at 16:34
  • Is it possible to have something a bit more informative than just "'yes" or "no"? I am a JavaScript ignorant, I hardly understand 5% of what has been added to the specs since I last had a look at the language, 7 years ago. So surely there are many people around that can spot errors, approximations, wrong interpretations in all this little essay I wrote? That would really help me. – kuroi neko Jul 28 '21 at 16:40
  • No, there's not much other than a "Yes". There are hardly any mistakes in the code you wrote. Notice that `bind` didn't really change between ES5 and ES6, it always worked like that. So if you feel you're missing 95% of something, please ask more specific questions about that. – Bergi Jul 28 '21 at 16:45
  • Well, to be honest I never tested `bind()` on a constructor when the ES5 standard ruled supreme, 7 years ago. I don't remember anyone using `bind()` for other things than a convenient alternative to the ritual `var that/self/_this = this`. Anyway, thanks for the helping hand. I'll see if I can come up with something more specific next time :) – kuroi neko Jul 28 '21 at 16:53

1 Answers1

1

The only bit you have been missing is the introduction of new.target in ES6, which a) makes it possible to distinguish between [[call]] and [[construct]] in a function and b) needs to be forwarded in the new call.

So a more complete polyfill might look like this:

Function.prototype.bind = function (fixed_this, ...fixed_args) {
    const fun = this;
    return function(...free_args) {
        return new.target != null
            ? Reflect.construct(fun, [...fixed_args, ...free_args], new.target)
            : fun.call(fixed_this, ...fixed_args, ...free_args);
    }
}

Some other details would involve that fun is asserted to be a function object, and that the returned bound function has a special .name, an accurate .length, and no .prototype. You can find these things in the spec, which apparently you've been reading already.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thank you, dear Sir. That's what I call a useful bit of lore. I had completely missed the fact that `new.target` was an actual value you could read, I thought it was just pseudo-code like the rest. And now I'm off to look at what this Reflect thing is supposed to do :) – kuroi neko Jul 28 '21 at 17:02
  • `Reflect.construct` is a close wrapper over *[[construct]]*. Just like `.call` allows you to do `fun(...fixed_args, ...free_args)` but also specify a `this`Argument, it lets you do `new fun(...fixed_args, ...free_args)` but also specify a newTarget. – Bergi Jul 28 '21 at 17:06
  • Marvellous. I'll have a go at this... mmm... interesting spec just to make sure, but that seems perfectly logical. Any chance of an extra advice about the efficiency side of things? I'm not splitting hairs there, I'd just like an informed opinion to make sure these spread operators and shallow copies (and possibly other hidden things I have no clue about) are not inefficient to the point of making these simple alternatives impractical. – kuroi neko Jul 28 '21 at 17:13
  • 1
    No, both speed and memory consumption is linear in the number of values involved, just like in the native implementation. It maybe slower by some (probably small) constant factor, but that's it. – Bergi Jul 28 '21 at 19:34
  • Thanks again for your kindness and your patience. – kuroi neko Jul 28 '21 at 19:42