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