underscore.js是一个1500行左右的Javascript函数式工具库,里面提供了很多实用的、耦合度极低的函数,用来方便的操作Javascript中的数组、对象和函数,它支持函数式和面向对象链式的编程风格,还提供了一个精巧的模板引擎。理解underscore.js的源码和思想,不管是新手,还是工作了一段时间的人,都会上升一个巨大的台阶。虽然我不搞前端,但是在一个星期的阅读分析过程中,仍然受益匪浅,决定把一些自认为很有意义的部分记录下来。
在开始分析函数绑定之前,有必要深入理解Javascript中的构造函数,因为这是在函数绑定中会碰到的一个问题。
下面是我们都很清楚的知识:
1. 使用new调用的函数即为构造函数,无论函数名是否首字母大写。
2. 使用new调用构造函数时,构造函数中的执行上下文this指向的是此构造函数将要生成的实例对象,用调用普通函数的方式调用构造函数时,this和调用的上下文环境有关。
3. 构造函数有一个prototype属性,这和它的实例对象的__proto__属性都是指向同一个对象,即原型对象,其原型对象有一个constructor属性,指向构造函数。
但是如果出现下面这些情况,结果又会如何:
构造函数中出现return
function A() {
this.name = 'a';
return 22;
}
var obj = new A();
console.log(obj); //结果为 A {name: "a"}
说明return 22;
被忽略了,最终还是return this;
但是,上面return了一个非object类型,如果return一个非null的object类型,那么结果又不同了:
function A() {
this.name = 'a';
return {age: 22}; //return任何typeof 为object的类型,null除外
}
var obj = new A();
console.log(obj); //结果为 A {age: 22}
可以看到这时return {age: 22}
产生了作用。
所以可以得出一个结论:用new调用构造函数,如果显式return了一个非object类型,则会被忽略;如果return了一个非null的object类型的变量或值,它将会成为新生成的实例。
下面的代码模拟了js引擎对构造函数的处理过程:
function A() {
this.name = 'a';
if(!A.prototype.speak) {
A.prototype.speak = function() {
alert('Hello');
}
}
return {age: 22};
}
function handleConstructor(Ctor) {
var obj = {}; //最终生成的实例对象
obj.__proto__ = Ctor.prototype; //保证原型链的正确
var result = Ctor.apply(obj); //Ctor中的this此时为obj,执行Ctor构造函数,可能有return值
if(typeof result === 'object' && result !== null) //非null的object类型才返回
return result;
return obj;
}
var a = handleConstructor(A);
console.log(a); //{age: 22} 和new A()是一样的
Function.prototype.bind作用是指定一个执行上下文,生成一个新函数,但是原函数的执行上下文并没有发生改变。bind第二个参数开始是调用函数的参数。
function A(name, age) {
this.name = name;
this.age = age;
if(!A.prototype.greet) {
A.prototype.greet = function() {
console.log(this.name + ' ' + this.age);
}
}
}
var p = new A('a', 22);
var p2 = new A('b', 23);
var greet = p.greet;
greet();//this现在指向window,name和age为undefined
将p的greet绑定到p2上:
var greet = p.greet.bind(p2);
greet(); //b 23
p.greet();//a 22 原函数的this并没有改变
如果调用bind的函数是一个构造函数,则bind的第一个参数也就是函数内部this的指向,会被忽略,通过上面对构造函数的分析,这点很好理解,构造函数的this指向的是将要生成的实例对象,如果能够修改this将引发混乱。
underscore的bind强化了原生的bind,能够处理构造函数的情况,以及构造函数中出现return的情况。
//第一个参数是待处理函数,第二个参数是新指定的执行上下文,后面的参数都是待处理函数的参数
_.bind = function(func, context) {
//有原生,就用原生的。
if (nativeBind && nativeBind === func.bind) return nativeBind.apply(func, slice.call(arguments, 1));
if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
//待处理函数的参数
var args = slice.call(arguments, 2);
var bound = function() { //这里有两批参数,外围函数的加上闭包的
return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));
};
return bound;
};
最后那个bound可能会烧得脑袋痛,但是我们已经知道需要判断待处理的函数是否是用作构造函数,所以executeBound就是干这件事的。
//sourceFunc 就是待处理函数,它的执行上下文(context)等待被指定,它的参数args即将被填入其中
//boundFunc是上面的_.bind函数中return出来的函数bound,bound函数有可能通过bound()方式调用,也可能通过new bound()调用,它的执行上下文为callingContext
var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
//instanceof判断的是 是否在原型链上,new出来的实例肯定在构造函数的原型链上
//如果boundFunc不是new的方式,则sourceFunc填入参数执行完事了
if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
//如果boundFunc是new方式调用,
//这行可以理解为self就是sourceFunc构造出来的实例,因为它们在一条原型链上
var self = baseCreate(sourceFunc.prototype); //baseCreate当成Object.create就好了
//把sourceFunc绑在实例self上执行,self就是最终构造的实例对象
var result = sourceFunc.apply(self, args); //构造函数一般没有显式的return,如果有的话,两种情况处理
//如果构造函数返回了一个非null对象,则就返回这个对象
if (_.isObject(result)) return result;
//否则应该返回之前的实例self
return self;
};
_.bindAll = function(obj) {//obj, methodName1, methodName2, methodName3...
var i, length = arguments.length, key;
if (length <= 1) throw new Error('bindAll must be passed function names');
for (i = 1; i < length; i++) {
key = arguments[i];
obj[key] = _.bind(obj[key], obj);
}
return obj;
};
看过了_.bind,那么这个_.bindAll(obj, func1,func2…)就非常好理解了,简单来说,就是把obj上的一堆方法绑死在obj上,以后不管把obj上的这些方法给谁引用,执行上下文都不会改变,都是obj,也就是说this被固定住 了,不再是 谁调用此函数,谁就是this。
obj[key] = _.bind(obj[key], obj);
这一行实现了绑死的效果,受它的启发,如果没有underscore,原生也可以实现这个效果:
function A(name, age) {
this.name = name;
this.age = age;
if(!A.prototype.greet) {
A.prototype.greet = function() {
alert(this.name + ' ' + this.age);
}
}
}
var p = new A('a', 22);
var p2 = new A('b', 23);
p.greet = p.greet.bind(p);//将greet的this绑死在p上。
var greet = p.greet; //将p.greet给一个新变量引用,this还是p
p2.greet = p.greet; //
greet(); // a 22
p2.greet(); //a 22
虽然在调用bind之前,greet中的this已经是指向p的,但是此时的this是会变动的,将p.greet方法中的this强制指向p,再覆盖原来的p.greet,也就是将this绑死在了p上,以后无论怎样去改变greet的调用者,this都不会变化,都指向p。
分析一下文档上的例子,是一个按钮的view:
<button id="btn">buttonbutton>
<script>
var buttonView = {
label : 'underscore',
onClick: function(){ alert('clicked: ' + this.label); },
};
var btn = document.getElementById('btn');
btn.addEventListener('click', buttonView.onClick);
script>
此时点击按钮,出现 clicked: undefined,这是一个this的使用陷阱,虽然看不到addEventListener的源码,但是buttonView.onClick在传入addEventListener之后,执行上下文肯定不是buttonView了。
//使用原生的bind将this绑死在buttonView上。
buttonView.onClick = buttonView.onClick.bind(buttonView);
现在就不会出现上面的问题了。
前面在_.bind的源码中,可以看到里面参数有个连接的过程concat,即外围的参数和内部闭包的参数连接到了一起,那么完全可以在调用外围函数的时候传入一部分参数占个位子,调用内部函数的时候传入剩下一部分参数,从而实现一次FP不完全调用(或者叫做偏函数)的过程,用原生的举个例子:
function add(a, b, c) {
return a + b + c;
}
var addOne = add.bind(null, 1);
var addOneAndTwo = addOne.bind(null, 2);
console.log(add(1,2,3));
console.log(addOne(2,3));
console.log(addOneAndTwo(3));
可以看到,这样子只能靠左边占位,在某些情况下并不能满足需求,underscore封装了一个更加通用的偏函数可以使用 _ 实现任意位置的占位:
_.partial = function(func) {
//调用外围函数传入的占位参数,可能会有下划线表示跳过不填,比如 _, 'arg1', _, 'arg3'
var boundArgs = slice.call(arguments, 1);
var bound = function() {
var position = 0,
length = boundArgs.length;
//arguments为调用内部闭包传入的参数 args为最终参数
var args = Array(length); //args先预设为占位参数的数量,后面可能还会变长
for (var i = 0; i < length; i++) {
//如果占位参数是下划线占位符,则用arguments中的参数填补
args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i];
}
//如果arguemnts参数还没填完,接着填
while (position < arguments.length) args.push(arguments[position++]);
//还是要小心构造函数的情况
return executeBound(func, bound, this, this, args);
};
return bound;
};
function sub(a, b) {
return a - b;
}
var subOne = _.partial(sub, _, 1);
console.log(subOne(10)); //9
obj.method = obj.method.bind(obj)
;