My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Saturday, February 20, 2010

Function.prototype.bind

Quick post about a fast Function.prototype.bind implementation.

Function.prototype.bind


function bind(context:Object[, arg1, ..., argN]):Function {
return a callback able to execute this function
passing context as reference via this
}

In few words, if we have a generic object, we don't necessary need to attach a function to create a method:

// a generic function
function name(name) {
if (name == null) return this.name;
this.name = name;
return this;
}

// a generic object
var wr = {name:"WebReflection"};

// a callback with a "trapped" object
var fn = name.bind(wr);

alert([

// wr has not been affected
wr.name, // WebReflection

// fn can always be called
fn(), // WebReflection

fn("test") === wr, // true
fn(), // test
wr.name // test

].join("\n"));

Designed to ensure the same context whatever way we decide to use the callback, included call, apply, setTimeout, DOM events, etc, Function.prototype.bind is one of the most useful/powerful JavaScript concept.
Unfortunately, this will be standard only with newer browser ...

Optimized bind for every browser


Update: as I have said I code what I need, I rarely google for simple tasks like this. Most of this code, in any case, has been readapted and tested to reach best performances, based on this proposal.

if (Function.prototype.bind == null) {

Function.prototype.bind = (function (slice){

// (C) WebReflection - Mit Style License
function bind(context) {

var self = this; // "trapped" function reference

// only if there is more than an argument
// we are interested into more complex operations
// this will speed up common bind creation
// avoiding useless slices over arguments
if (1 < arguments.length) {
// extra arguments to send by default
var $arguments = slice.call(arguments, 1);
return function () {
return self.apply(
context,
// thanks @kangax for this suggestion
arguments.length ?
// concat arguments with those received
$arguments.concat(slice.call(arguments)) :
// send just arguments, no concat, no slice
$arguments
);
};
}
// optimized callback
return function () {
// speed up when function is called without arguments
return arguments.length ? self.apply(context, arguments) : self.call(context);
};
}

// the named function
return bind;

}(Array.prototype.slice));
}


Why Bother

The concatenation optimization is something rarely present in whatever framework/library I have seen. While it makes the code a bit bigger than usual, performances will be the best for most common cases, those where optional arguments are not passed at all.
A simple test case, one of my favorites, able to solve the classic IE problem with extra arguments passed to setInterval/Timeout:

(function(){

var callback = function(time){
if (1000 < new Date - time)
return alert("1 second with a default argument");
setTimeout(callback, 15);
}.bind(null, new Date);

setTimeout(callback, 15);

}());

11 comments:

Michael Haufe ("TNO") said...

Why not use this instead of manipulating the arguments object?

if(!Function.prototype.bind){
    Function.prototype.bind = function(context){
        if(!context)
            return this;
        var fun = this;
        return function(){
            return fun.apply(context, arguments);
        };
    };
}

Andrea Giammarchi said...

Michael, that is not standard, bind accepts a thisArg plus, optionally, one or more arguments.

The check !context is poor, if I want to pass the current global object I do believe null should be accepted.

If I want to pass a persistent condition, true or false, I don't see why I should not be able to bind this value.

Regards

Andrea Giammarchi said...

The check !context is poor, if I want to pass the current global object I do believe null should be accepted.

errata, null will result into null, undefined context, so I guess the check should be context == null for ES5.

Updated

joseanpg said...

What do you think about the implementation of Garrett Smith?
<>
Function.prototype.bind = function(context){
var fn = this,
ap, concat, args,
isPartial = arguments.length > 1;
// Strategy 1: just bind, not a partialApply
if(!isPartial) {
return function() {
if(arguments.length !== 0) {
return fn.apply(context, arguments);
} else {
return fn.call(context); // faster in Firefox.
}
};
} else {
// Strategy 2: partialApply
ap = Array.prototype,
args = ap.slice.call(arguments, 1);
concat = ap.concat;
return function() {
return fn.apply(context,
arguments.length === 0 ? args :
concat.apply(args, arguments));
};
}
};

Andrea Giammarchi said...

@joseanpg there are several bind implementation out there. My aim is to provide the fastest one avoiding every redundant operation and using all it's possible to speed up both creation and execution.

In your example there are several useless steps.

isPartial is created and passed via "not", why? That variable is not useful, the if could simply contain: if (1 < arguments.length) ... same case, no need for a variable, netirher a "not" operation.

Same is for checks agains integer, 0 is falsy since C language and it's accepted in C as well as boolean.
if (0) { falsy }
There is no need to strictly compare via === or !==
An if/else statement is usually slower than a ternary operation.
Specially in JS where we have no bytecode, it means lots of redundant code as well to surround those 2 cases.

return arguments.length ? apply : call

This is all we need.
In Strategy 2 every time there is one or more arguments, we need to lookup for the global Array, and access its prototype.

This is both slower and not safe. Global Constructor prototypes should be always cached in advance so that it's easier to access the method we are looking for, and it's safer, since if another library will replace Array, its prototype, or one of its method we trust for our purpose, we could be in troubles.
Finally, since concat already accepts an array to concat, there is no need at all to cache concat and use apply, specially because if an argument is an array, this will become flat.

var a = [[1, 2], 3];
var b = [[4, 5], 6];
alert(a.concat(b).join("-"));
//1,2-3-4,5-6

alert(a.concat.apply(a, b).join("-"));
// 1,2-3-4-5-6

As you can see, that is simply an error, able to make arguments inconsistent (if I passed an array as first argument, I meant it!)

joseanpg said...

Very good advice:

"...specially because if an argument is an array, this will become flat.

var a = [[1, 2], 3];
var b = [[4, 5], 6];
alert(a.concat(b).join("-"));//1,2-3-4,5-6
alert(a.concat.apply(a, b).join("-"));// 1,2-3-4-5-6

As you can see, that is simply an error, able to make arguments inconsistent"


Is
bind_researched

making that mistake?

Perhaps could be fixed using call instead of apply:

alert(a.concat.call(a, b).join("-"));//1,2-3-4,5-6

Andrea Giammarchi said...

call is fine, but it wont recognize arguments as an array, so it won't concatenate arguments as expected ;)

joseanpg said...

Undoubtedly the best option is

boundArgs.concat(_slice.call(arguments))

But the following would be valid too

_concat.call(boundArgs,_slice.call(arguments))

Do you agree?

By the way, it seems that bind_even_newer also suffers the problem of flattening concat.apply, what do you think?

Andrea Giammarchi said...

sure, that would be valid.

What do I think?

I think I have both the fastest and more stable version here ;)

joseanpg said...

I agree :D

Ruslan said...

I've found another part of the problem with Function.prototype.bind: http://www.angrycoding.com/2011/09/to-bind-or-not-to-bind-that-is-in.html