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可以实现不完全调用或者偏函数。
相关文章推荐
- underscore.js源码解析【函数】
- underscore.js源码解析【'_'对象定义及内部函数】
- underscore.js源码解析【对象】
- underscore.js源码解析【数组】
- underscore.js源码解析【集合】
- Underscore.js template()函数全解析
- underscore.js源码解析之命名空间
- underscore.js 源码分析5 基础函数和each函数的使用
- underscore.js源码解析之类型判断
- underscore.js,jquery.js源码阅读
- underscorejs 源码走读笔记
- Underscore.js 1.3.3 源码分析收藏
- underscore.js 解读(敲源码)
- 【 js 性能优化】【源码学习】underscore throttle 与 debounce 节流
- underscore.js中的节流函数debounce及trottle
- Underscore.js源码分析(一)
- Vue.js解析(三)【从Vue.js源码角度再看数据绑定】
- Underscore.js模板解析
- underscore.js 源码
- Underscore轻量级模板解析函数