最近开始从新学习一波js,框架用久了有些时候以为这样子应该能够实现发现就真的实现了,可是为何这么写好像又说不太清楚,以前读了LucasHC以及冴羽的两篇关于bind的文章感受本身好像基础知识都还给体育老师了哈哈哈,因此危机感爆棚,赶忙重头复习一遍。本次主要围绕bind是什么;作了什么;本身怎么实现一个bind,这三个部分。其中会包含一些细节代码的探究,往下看就知道。git
bind()方法建立一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供以前提供一个给定的参数序列。
var result = fun.bind(thisArg[, arg1[, arg2[, ...]]]) result(newArg1, newArg2...)
没看懂没事接着往下看。github
从上面的介绍中能够看出三点。首先调用bind方法会返回一个新的函数(这个新的函数的函数体应该和fun是同样的)。同时bind中传递两个参数,第一个是this指向,即传入了什么this就等于什么。以下代码所示:数组
this.value = 2 var foo = { value: 1 } var bar = function() { console.log(this.value) } var result = bar.bind(foo) bar() // 2 result() // 1,即this === foo
第二个参数为一个序列,你能够传递任意数量的参数到其中。而且会预置到新函数参数以前。app
this.value = 2 var foo = { value: 1 }; var bar = function(name, age, school) { console.log(name) // 'An' console.log(age) // 22 console.log(school) // '家里蹲大学' } var result = bar.bind(foo, 'An') //预置了部分参数'An' result(22, '家里蹲大学') //这个参数会和预置的参数合并到一块儿放入bar中
咱们能够看出在最后调用 result(22, '家里蹲大学')
的时候,其内部已经包含了在调用bind的时候传入的 'An'
。框架
一句话总结:调用bind,就会返回一个新的函数。这个函数里面的this就指向bind的第一个参数,同时this后面的参数会提早传给这个新的函数。调用该新的函数时,再传递的参数会放到预置的参数后一块儿传递进新函数。函数
this.value = 2 var foo = { value: 1 }; var bar = function(name, age, school) { console.log(name) // 'An' console.log(age) // 22 console.log(school) // '家里蹲大学' console.log(this.value) // 1 } Function.prototype.bind = function(newThis) { var aArgs = Array.prototype.slice.call(arguments, 1) //拿到除了newThis以外的预置参数序列 var that = this return function() { return that.apply(newThis, aArgs.concat(Array.prototype.slice.call(arguments))) //绑定this同时将调用时传递的序列和预置序列进行合并 } } var result = bar.bind(foo, 'An') result(22, '家里蹲大学')
这里面有一个细节就是Array.prototype.slice.call(arguments, 1)
这句话,咱们知道arguments这个变量能够拿到函数调用时传递的参数,但不是一个数组,可是其具备一个length属性。为何如此调用就能够将其变为纯数组了呢。那么咱们就须要回到V8的源码来进行分析。#这个版本的源码为早期版本,内容相对少一些。学习
function ArraySlice(start, end) { var len = ToUint32(this.length); //须要传递this指向对象,那么call(arguments), //即可将this绑定到arguments,拿到其length属性。 var start_i = TO_INTEGER(start); var end_i = len; if (end !== void 0) end_i = TO_INTEGER(end); if (start_i < 0) { start_i += len; if (start_i < 0) start_i = 0; } else { if (start_i > len) start_i = len; } if (end_i < 0) { end_i += len; if (end_i < 0) end_i = 0; } else { if (end_i > len) end_i = len; } var result = []; if (end_i < start_i) return result; if (IS_ARRAY(this)) SmartSlice(this, start_i, end_i - start_i, len, result); else SimpleSlice(this, start_i, end_i - start_i, len, result); result.length = end_i - start_i; return result; };
从源码中能够看到经过call将arguments下的length属性赋给slice后,即可经过 start_i & end_i
来得到最后的数组,因此不须要传递进slice时就是一个纯数组最后也能够获得一个数组变量。测试
被用做构造函数时,this应指向new出来的实例,同时有prototype属性,其指向实例的原型。this
this.value = 2 var foo = { value: 1 }; var bar = function(name, age, school) { ... console.log('this.value', this.value) } Function.prototype.bind = function(newThis) { var aArgs = Array.prototype.slice.call(arguments, 1) var that = this //that始终指向bar var NoFunc = function() {} var resultFunc = function() { return that.apply(this instanceof that ? this : newThis, aArgs.concat(Array.prototype.slice.call(arguments))) } NoFunc.prototype = that.prototype //that指向bar resultFunc.prototype = new NoFunc() return resultFunc } var result = bar.bind(foo, 'An') result.prototype.name = 'Lsc' // 有prototype属性 var person = new result(22, '家里蹲大学') console.log('person', person.name) //'Lsc'
var NoFunc = function() {} ... NoFunc.prototype = that.prototype //that指向bar resultFunc.prototype = new NoFunc() return resultFunc
经过上面代码能够看出,that始终指向bar。同时返回的函数已经继承了that.prototype即bar.prototype。为何不直接让返回的函数的prototype属性resultFunc.prototype
等于为bar(that).prototype呢,这是由于任何new出来的实例均可以访问原型链。若是直接赋值那么new出来的对象能够直接修改bar函数的原型链,这也就是是原型链污染。因此咱们采用继承的方式(将构造函数的原型链赋值为父级构造函数的实例),让new出来的对象的原型链与bar脱离关系。prototype
如何判断当前this指向了哪里呢,经过第一点咱们已经知道,经过bind方法返回的新函数已经有了原型链,剩下须要咱们作的就是改变this的指向就能够模拟完成了。经过什么来判断当前被调用是以何种姿式呢。答案是instanceof
。
instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。
// 定义构造函数 function C(){} function D(){} var o = new C(); // true,由于 Object.getPrototypeOf(o) === C.prototype o instanceof C; // false,由于 D.prototype不在o的原型链上 o instanceof D;
从上面能够看出,instanceof
能够判断出一个对象是不是由这个函数new出来的,若是是new出来的,那么这个对象的原型链应为该函数的prototype.
因此咱们来看这段关键的返回的函数结构:
var resultFunc = function() { return that.apply(this instanceof that ? this : newThis, aArgs.concat(Array.prototype.slice.call(arguments))) }
在这其中咱们要先认清this instanceof that
中的this是bind函数被调用后,返回的新函数中的this。因此这个this可能执行在普通的做用域环境,同时也可能被new一下从而改变本身的指向。再看that,that始终指向了bar,同时其原型链that.prototype是一直存在的。因此若是如今这个新函数要作new操做,那么this指向了新函数,那么 this instanceof that === true
, 因此在apply中传入this为指向,即指向新函数。若是是普通调用,那么this不是被new出来的,即新函数不是做为构造函数,this instanceof that === false
就很显而易见了。这个时候是正常的bind调用。将调用的第一个参数做为this的指向便可。
if (!Function.prototype.bind) { Function.prototype.bind = function(oThis) { if (typeof this !== 'function') { // closest thing possible to the ECMAScript 5 // internal IsCallable function throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); } var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, fNOP = function() {}, fBound = function() { return fToBind.apply(this instanceof fNOP ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments))); }; if (this.prototype) { // Function.prototype doesn't have a prototype property fNOP.prototype = this.prototype; } fBound.prototype = new fNOP(); return fBound; }; }
能够看到,其首先作了当前是否支持bind的断定,不支持再实行兼容。同时判断调用这个方法的对象是不是个函数,若是不是则报错。
同时这个模拟的方法也有一些缺陷,可关注MDN上的Polyfill部分
模拟bind实现最大的一个缺陷是,模拟出来的函数中会一直存在prototype属性,可是原生的bind做为构造函数是没有prototype的,这点打印一下便可知。不过这样子new出来的实例没有原型链,那么它的意义是什么呢。若是哪天做者知道了意义会更新在这里的=。= 若是说错的地方欢迎指正,一块儿交流哈哈。