Last Updated: February 25, 2016
·
31.76K
· 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

Interesting stuff, thanks for sharing!

over 1 year ago ·

Have a fresh tip? Share with Coderwall community!

Post
Post a tip
Did you know?

JavaScript on a page runs with the privileges of that origin. One altered library, tag manager, or snippet can read inputs, change what people see, and call your APIs as the user.

Security insights from Cside