您的位置:首页 > Web前端 > JavaScript

underscore.js源码解析之函数绑定

2017-12-28 10:42 633 查看

1. 引言

  underscore.js是一个1500行左右的Javascript函数式工具库,里面提供了很多实用的、耦合度极低的函数,用来方便的操作Javascript中的数组、对象和函数,它支持函数式和面向对象链式的编程风格,还提供了一个精巧的模板引擎。理解underscore.js的源码和思想,不管是新手,还是工作了一段时间的人,都会上升一个巨大的台阶。虽然我不搞前端,但是在一个星期的阅读分析过程中,仍然受益匪浅,决定把一些自认为很有意义的部分记录下来。

2.构造函数的本质

  在开始分析函数绑定之前,有必要深入理解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()是一样的


3.原生bind

  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将引发混乱。

4. _.bind

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;
};


4 用_.bindAll固定this

_.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">button</button>
<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);


现在就不会出现上面的问题了。

5._partial不完全调用

  前面在_.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


3.总结

用new调用构造函数,如果显式return了一个非object类型,则会被忽略;如果return了一个非null的object类型的变量或值,它将会成为新生成的实例。

将this固定住的技巧,
obj.method = obj.method.bind(obj)
;

用bind可以实现不完全调用或者偏函数。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息