Understanding _.bind
Why we need bind
From this
function hello(thing) {
console.log("Hello " + thing);
}
// this:
hello("world")
// desugars to:
hello.call(window, "world");
var person = {
name: "Brendan Eich",
hello: function(thing) {
console.log(this + " says hello " + thing);
}
}
// this:
person.hello("world")
// desugars to this:
person.hello.call(person, "world");
function hello(thing) {
console.log(this + " says hello " + thing);
}
person = { name: "Brendan Eich" }
person.hello = hello;
person.hello("world") // still desugars to person.hello.call(person, "world")
hello("world") // "[object DOMWindow]world"
The problem with this
(setTimeout)
The this
inside a window.setTimeout
is the global object (example from MDN)
function LateBloomer() {
this.petalCount = Math.ceil( Math.random() * 12 ) + 1;
}
// declare bloom after a delay of 1 second
LateBloomer.prototype.bloom = function() {
// inside of the callback `this` is the global object
window.setTimeout( this.declare.bind( this ), 1000 );
};
LateBloomer.prototype.declare = function() {
console.log('I am a beautiful flower with ' + this.petalCount + ' petals!');
};
The problem with this
(jQuery.on)
When using callbacks for eventHandlers or for $.ajax
in jQuery with a class library like Fiber, you'll quickly notice this
in jQuery tends to point to the element and not your Module's instance.
var TestClass = Fiber.extend(function(base) {
return {
init: function() {
this.value = 42;
this.attachEventListeners();
},
showThis: function() {
console.log(this.value);
},
attachEventListener: function() {
var that = this,
showThis_withBind = _.bind(this.showThis, this),
showThis_withProxy = $.proxy(this.showThis, this);
this._$el.on('click', function() {
// jquery changes this to the dom node
that.showThis();
showThis_withBind();
showThis_withProxy();
});
}
};
});
We have many, many ways to deal with this
?
- use
var that = this
- use
_.bind
- use
$.proxy
- use
_.bindall
- always use bound methods instead of anonymous callbacks
- use
call
/apply
- use coffeescripts
=>
Let's Implement Function.bind (underscore's flavor)
Basic (v1)
Here's a very basic implementation from Katz's blog Understanding Javascript Function Invocation and This
var bind = function(func, thisValue) {
return function() {
return func.apply(thisValue, arguments);
}
}
This implementation is what most people think about when referring to Function.bind. It's fantastically small and understandable.
Add isCallable(v2)
- If IsCallable(func) is false, then throw a TypeError exception.
A simple change to make it more resilient. See the MDN.
// let's add a check to make sure func is "callable"
// from mdn
var bind = function(func, thisValue) {
if (typeof func !== "function") {
// closest thing possible to the ECMAScript 5 internal IsCallable function
throw new TypeError("bind - what is trying to be bound is not callable");
}
return function() {
return func.apply(thisValue, arguments);
}
}
Support partial functions (v3)
- Let argList be an empty List.
- If this method was called with more than one argument then in left to right order starting with arg1 append each argument as the last element of argList
_.bind
actually accepts can take 2+ arguments. They act as pre-filled arguments. "Optionally, pass arguments to the function to pre-fill them, also known as partial application".
// ECMA: Function.prototype.bind (thisArg [, arg1 [, arg2, …]])
// Underscore: _.bind(function, object, [*arguments])
// fun fact: this is what _.partial does
var bind = function(func, thisValue) {
if (typeof func !== "function") {
// closest thing possible to the ECMAScript 5 internal IsCallable function
throw new TypeError("bind - what is trying to be bound is not callable");
}
// get the arguments of bind into an array
var bindArguments = Array.prototype.slice.call(arguments, 2); // 0th argument is the function, 1st is the context
return function() {
// get the arguments of the bound function into an array
var funcArgs = Array.prototype.slice.call(arguments);
// call the function pre-filling values bind time
return func.apply(thisValue, bindArguments.concat(funcArgs));
}
}
Support using the constructor (v4)
This implementation fails with function constructors (e.g. var Foo = function(){}, FooBound = _.bind(Foo, window); f = new FooBound();)
without this fix:
- f instanceOf FooBound === false
- f.constructor !== FooBound
var isObject = function(value) {
return !!(value && typeof value === 'object');
};
var bind = function(func, thisValue) {
if (typeof func !== "function") {
// closest thing possible to the ECMAScript 5 internal IsCallable function
throw new TypeError("bind - what is trying to be bound is not callable");
}
// get the arguments of bind into an array
var bindArguments = Array.prototype.slice.call(arguments, 2), // 0th argument is the function, 1st is the context
emptyFn = function(){};
var bound = function() {
// get the arguments of the bound function into an array
var funcArgs = Array.prototype.slice.call(arguments),
calledWithNew = this instanceof bound;
if (!calledWithNew) {
// call the function pre-filling values bind time
return func.apply(thisValue, bindArguments.concat(funcArgs));
} else {
// when used with new
// fix the prototype and constructor of the bound function
emptyFn.prototype = func.prototype;
var self = new emptyFn();
var result = func.apply(self, bindArguments.concat(funcArgs));
// Conform to ECMA for the return value when using new
return isObject(result) ? result : self;
}
};
return bound;
}
More stuff that can be done (v5)
- Default to using Native Function bind.
- fix the function's length (not implemented in underscore)
- Put
bind
on the Function.prototype (see MDN's shim) - store some unique Id so that you can remove bound functions from eventHandlers (see jQuery's Proxy)
Underscore.bind (v1.5.1)
_.bind = function(func, context) {
var args, bound;
if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
if (!_.isFunction(func)) throw new TypeError;
args = slice.call(arguments, 2);
return bound = function() {
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
ctor.prototype = func.prototype;
var self = new ctor;
ctor.prototype = null;
var result = func.apply(self, args.concat(slice.call(arguments)));
if (Object(result) === result) return result;
return self;
};
};
jQuery.proxy (v2.0.3)
// Bind a function to a context, optionally partially applying any
// arguments.
var proxy = function( fn, context ) {
var tmp, args, proxy;
if ( typeof context === "string" ) {
tmp = fn[ context ];
context = fn;
fn = tmp;
}
// Quick check to determine if target is callable, in the spec
// this throws a TypeError, but we will just return undefined.
if ( !jQuery.isFunction( fn ) ) {
return undefined;
}
// Simulated bind
args = slice.call( arguments, 2 );
proxy = function() {
return fn.apply( context || this, args.concat( slice.call( arguments ) ) );
};
// Set the guid of unique handler to the same of original handler, so it can be removed
proxy.guid = fn.guid = fn.guid || jQuery.guid++;
return proxy;
}
Bind all the things BindAll (underscore)
With the power of bind you'll start wanting to bind all the things.
_.bindAll = function(obj) {
var funcs = slice.call(arguments, 1);
if (funcs.length === 0) throw new Error("bindAll must be passed function names");
each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
return obj;
};
Bonus BindAll (underscore 1.4.4 Lodash)
function bindAll(object) {
var funcs = arguments.length > 1 ? concat.apply(arrayRef, nativeSlice.call(arguments, 1)) : functions(object),
index = -1,
length = funcs.length;
while (++index < length) {
var key = funcs[index];
object[key] = bind(object[key], object);
}
return object;
}
References
Written by Jimmy Chan
Related protips
1 Response
Interesting stuff, thanks for sharing!