update: 2018-06-08javascript
原文连接
为何要本身去实现一个bind函数?html
bind()函数在 ECMA-262 第五版才被加入;它可能没法在全部浏览器上运行。
因此,为了理想主义和世界和平(全部浏览器上都能为所欲为调用它),必要的时候须要咱们本身去实现一个bind
。那么,一个bind
函数须要具有什么功能呢?java
绑定this、定义初始化参数是它存在的主要意义和价值。MDN对它的定义以下:git
语法:fun.bind(thisArg[, arg1[, arg2[, ...]]])bind()方法建立一个新的函数, 当被调用时,将其this关键字设置为提供的值(thisArg)。github
被调用时,arg一、arg2等参数将置于实参以前传递给被绑定的方法。数组
它返回由指定的this值和初始化参数改造的原函数拷贝。浏览器
鉴于这两个核心做用,咱们能够来实现一个简单版看看:app
if (!Function.prototype.bind) { Function.prototype.bind = function (oThis) { if (typeof this !== 'function') { return } let self = this let args = Array.prototype.slice.call(arguments, 1) return function () { return self.apply(oThis, args.concat(Array.prototype.slice.call(arguments))) //这里的arguments是执行绑定函数时的实参 } } }
因为arguments
是类数组对象,不拥有数组的slice
方法,因此须要经过call
来将slice
的this
指向arguments
。args
就是调用bind
时传入的初始化参数(剔除了第一个参数oThis
)。将args
与绑定函数执行时的实参arguments
经过concat
连起来做为参数传入,就实现了bind
函数初始化参数的效果。函数
bind
函数的另一个也是最主要的做用:绑定this
指向,就是经过将调用bind
时的this
(self
)指向指定的oThis
来完成。这样当咱们要使用bind
绑定某个对象时,执行绑定函数,它的this
就永远固定为指定的对象了~oop
到这里,咱们已经能够用上面的版原本使用大部分场景了。可是~
可是,这种方案就像前面说的,它会永远地为绑定函数固定this
为指定的对象。若是你仔细看过MDN关于bind
的描述,你会发现还有一个状况除外:
thisArg:当使用new 操做符调用绑定函数时,该参数无效。一个绑定函数也能使用new操做符建立对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
咱们能够经过一个示例来试试看原生的bind
对于使用new
的状况是如何的:
function animal(name) { this.name = name } let obj = {} let cat = animal.bind(obj) cat('lily') console.log(obj.name) //lily let tom = new cat('tom') console.log(obj.name) //lily console.log(tom.name) //tom
试验结果发现,obj.name
依然是lily
而没有变成tom
,因此就像MDN描述的那样,若是绑定函数cat
是经过new
操做符来建立实例对象的话,this
会指向建立的新对象tom
,而再也不固定绑定指定的对象obj
。
而上面的简易版却没有这样的能力,它能作到的只是永久地绑定指定的this
(有兴趣的胖友能够在控制台使用简易版bind
试下这个例子看看结果)。这显然不能很好地替代原生的bind
函数~
那么,如何才能区分绑定函数有没有经过new
操做符来建立一个实例对象,从而进行分类处理呢?
咱们知道检测一个对象是否经过某个构造函数使用new
实例化出来的最快的方式是经过 instanceof:
A instanceof B //验证A是否为B的实例
那么,咱们就能够这样来实现这个bind
:
if (!Function.prototype.bind) { Function.prototype.bind = function (oThis) { if (typeof this !== 'function') { return } let self = this let args = Array.prototype.slice.call(arguments, 1) let fBound = function() { let _this = this instanceof self ? this : oThis //检测是否使用new建立 return self.apply(_this, args.concat(Array.prototype.slice.call(arguments))) } if (this.prototype) { fBound.prototype = this.prototype } return fBound } }
假设咱们将调用bind
的函数称为C,将fBound
的prototype
原型对象指向C的prototype
原型对象(上例中就是self
),这样的话若是将fBound
做为构造函数(使用new
操做符)实例化一个对象,那么这个对象也是C的实例,this instanceof self
就会返回true。这时就将self
指向新建立的对象的this
上就能够达到原生bind
的效果了(再也不固定指定的this
)。不然,才使用oThis
,即绑定指定的this
。
可是这样作会有什么影响?将fBound
的prototype
原型对象直接指向self
的prototype
原型对象,那么当修改fBound
的prototype
对象时,self
(上述C函数)的prototype
对象也会被修改!!考虑到这个问题,咱们须要另一个function
来帮咱们作个中间人来避免这个问题,咱们看看MDN是怎么实现bind
的。
MDN针对bind
没有被普遍支持的兼容性提供了一个实现方案:
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),//这里的arguments是跟oThis一块儿传进来的实参 fToBind = this, fNOP = function() {}, fBound = function() { return fToBind.apply(this instanceof fNOP ? this : oThis, // 获取调用时(fBound)的传参.bind 返回的函数入参每每是这么传递的 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; }; }
发现了吗,和上面通过改造的方案相比,最主要的差别就在于它定义了一个空的function fNOP
,经过fNOP
来传递原型对象给fBound
(经过实例化的方式)。这时,修改fBound
的prototype
对象,就不会影响到self
的prototype
对象啦~并且fNOP
是空对象,因此几乎不占内存。
其实这个思路也是YUI库如何实现继承的方法。他的extend
函数以下:
function extend(Child, Parent) { var F = function(){}; F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; }
最后一步是将Child
的constructor
指回Child
。
实现一个原生的函数,最重要的是理清楚它的做用和功能,而后逐一去实现它们包括细节,基本上就不会有问题~
这里用到的一些关于prototype
和instanceof
的具体含义,能够参考阮一峰老师的 prototype 对象,相信对你理解JavaScript的原型链和继承会有帮助~
好啦就这样,晚安美梦了各位🌛✨