读懂Underscore.js【2】


另一个基础函数

_bind .bind(function, object, arguments])

_.bind = function(func, context) {
    var args, bound;
    if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));//【1】【2】
    if (!_.isFunction(func)) throw new TypeError;
    args = slice.call(arguments, 2);
    return bound = function() {//【3】
        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;
    };
};

【1】Function.prototype.apply Function.prototype.apply (thisArg, argArray)
apply 和 call 功能类似,只不过只接受两个参数,第二个参数 argArray 是一个数组,作为函数的参数传入。

【2】这一句的意思是,如果支持原生的bind() Function.prototype.bind (thisArg , arg1 , arg2, …), 就调用原生的,并把当前的参数传入,第一个参数是func,之后的参数依次传入。
slice = Array.prototype.slice.call Array.prototype.slice.call
call将指定函数Function作为thisArg对象的方法来调用,将参数args传递给Function,返回值为Function的返回值。
这句是取第一位以后的arguments,这个arguments并不是数组,但是有个length属性,所以能用Array.prototype.slice调用
具体到这里,伪代码可以简略解释如下:

Array.prototype.slice = function(start,end){
	var result = new Array();
	//注释部分是处理参数为负数的情况,可以掠过
	//len = this.length;
	//start = start < 0 ? max(len + start) : min(start,len);
	//end = end ? len : end;
	//end = end < 0 ? max((len + end),0) : min(end,len);
	for(var i = start; i < end; i++){
		result.push(this[i]);
	}
	return result;
}

这里与内部实现有出入,不要深究细节,明白意思就行了。总之就是将当前函数的agruments转成数组

【3】这段代码有点复杂
先上一个简单版本,绑定一个函数到一个对象上:

_.bind = function(func, obj) {
    var aArgs = slice.call(arguments, 2);
    return function() {
        return func.apply(obj, args.concat(slice.call(arguments)));
    }
};

功能就是把bind()时传入的参数与原来的参数合并,返回一个函数,更详细的解释这里有
更多阅读参考partial application, 还有 JavaScript currying

Github上根据这个Issue, 为了更像原生的bind(), 需要支持new一个被绑定的函数。 在MDN的建议下,Pull request #282让这段代码变成这么复杂了

_.bind = function(func, obj) {
    if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
    var aArgs = slice.call(arguments, 2),
        fNOP = function () {},
        fBound = function () {
            return func.apply(this instanceof fNOP ? this : obj, aArgs.concat(slice.call(arguments)));
        };

    fNOP.prototype = func.prototype;
    fBound.prototype = new fNOP();
    return fBound;
};

最终版本:

var ctor = function(){};

_.bind = function bind(func, context) {
    var bound, args;

    // 如果原生支持,就用原生的.bind()
    if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));

    // 如果没有传入function参数,抛出异常
    if (!_.isFunction(func)) throw new TypeError;

    // 支持绑定 function 和 context 后面的参数 (所以用 .slice(2))
    args = slice.call(arguments, 2);

    // 返回绑定后的函数
    return bound = function() {

        // 如果没有用new关键字,(this instanceof bound)就为假
        // 此时为正常的调用 bound(),bound 函数中的 `this` 和 arguments
        // 都已经绑定,传入参数,直接调用函数就可以了。
        if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));

        // 如果使用了new关键字,`new bound()` 
        // (this instanceof bound)为真,此时模拟函数的构造函数
        // (JavaScript中通过 new 关键字方式调用的函数都被认为是构造函数。)
        // 具体步骤就是:创建一个对象A,将A的prototype指向原函数的prototype
        // 执行原函数
        // 如果原函数没有显式的return一个对象,则隐式的返回A对象的实例
        ctor.prototype = func.prototype;

        // ctor是空函数,不进行任何操作
        // 只创建一个实例对象
        var self = new ctor; 

        // 用这个新创建的实例作为 `this` 调用原函数,并传入相应的参数
        // 原函数作为新实例的构造函数执行
        var result = func.apply(self, args.concat(slice.call(arguments)));

        // 执行的结果是 object 就返回,不是的话返回这个实例对象
        // 因为标准规定 `new xxx` 操作必须返回对象
        if (Object(result) === result) return result;
        return self;
    };
};

扩展阅读:
underscore functions 测试用例
Understanding the code of _.bind
underscore中的function类函数解析
深入理解JavaScript系列(2):揭秘命名函数表达式
深入理解JavaScript系列(5):强大的原型和原型链
JavaScript 秘密花园 构造函数
深入理解JavaScript系列(26):设计模式之构造函数模式
深入理解JavaScript系列(18):面向对象编程之ECMAScript实现

updated:2013年1月6日 19:03:00 星期日