oi3j3w
Last Updated: February 25, 2016
·
13.39K
· jimmyhchan

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

1 Response
Add your response

9887

Interesting stuff, thanks for sharing!

over 1 year ago ·