Reimplementing bind, call and apply post

Not long ago I wrote a post about Mimicking bind, call and apply. The feedback I got from some of my friends, although overall positive, was that it was just a little too complicated. This was especially evident in the call function. I attribute this complexity to the work I had to do to unpack the arguments the call and apply functions were invoked with. Let's face it, wrapping partial functions with partial functions can twist your understanding of what's happening pretty quickly.

Earlier today I was speaking about this problem with a friend of mine and he mentioned the ES6 spread operator. If, like me, you're not familiar with spread it's a new operator that is part of the ES6 proposal. Here is spread's summary from MDN:

The spread operator allows an expression to be expanded in places where multiple arguments (for function calls) or multiple elements (for array literals) are expected.

You can use spread in function calls f(...iterableObj); and in array literals [...iterableObj, 4, 5, 6]. Spread is going to be really useful in our scripts as it's going to simplify our code by removing a lot of the hoops we have to jump through to produce concise and expressive code. If you can't tell, I'm pretty stoked about spread.

Armed with the spread operator, it's now possible to completely reimplement bind, call and apply from first principles.

bind

The bind function is the most complicated of the three functions and rightfully so in my opinion. It does a lot of work. The approach I took this time was to make a copy of the context (if provided) and then make the target function a property of the copied context. bind returns a closure, that when executed, merges the arguments the function was bound with to the arguments the closure was called with and then executes the target function belonging to the copied context with those arguments.

function bind(context, fn) {
    function copy(context) {
        var keys = Object.keys(context),
            copy = {};
        keys.forEach(function (key) {
            this[key] = context[key];
        }, copy);
        return copy;
    }

    function mergeArgs (boundArgs, callArgs) {
        var merged = [];
        merged.push(...boundArgs, ...callArgs);
        return merged;
    }

    var boundArgs = [].slice.call(arguments, 2),
        o = context ? copy(context) : {};

    o.fn = fn;
    return function () {
        var callArgs = [].slice.call(arguments, 0)
        o.fn(...mergeArgs(boundArgs, callArgs));
    };
}


function greet(greeting, name) {
    console.log(greeting + " " + name + "!");
}

var morningGreeter = bind(void 0, greet, 'Good Morning');

morningGreeter('Alice');
// => Good Morning Alice!

morningGreeter('Bob');
// => Good Morning Bob!


function sayHello(smallTalk) {
  console.log('Hello ' + this.name + ", " + smallTalk);
}

var me = {
  name: 'Colin',
  fn: function () {
    console.log("Hello World");
  }
};

var talkToMe = bind(me, sayHello);
talkToMe('How about that sports ball?');
// => Hello Colin, How about that sports ball?

// binding `me` to `sayHello` didn't clobber `me.fn`
me.fn();
// => Hello World

call and apply

Once bind was complete implementing call and apply was really really simple.

function call(context, fn) {
    var args = [].slice.call(arguments, 2);
    return bind(context, fn)(...args);
}

function apply(context, fn, args) {
    args = args || [];
    return bind(context, fn)(...args);
}

call(me, sayHello, "Some weather we're getting!");
// => Hello Colin, Some weather we're getting!"

apply(me, sayHello, ["Recent news events have been surprising wouldn't you say?"]);
// => "Hello Colin, Recent news events have been surpristing wouldn't you say?"

There you have it, using spread I was able to simplify the code enormously and completely reimplement three context manipulating functions.

Categories: javascript

Tags: javascript, interesting but not useful