手动实现bind函数(附MDN提供的Polyfill方案解析)

update: 2018-06-08javascript

原文连接

为何要本身去实现一个bind函数?html

bind()函数在 ECMA-262 第五版才被加入;它可能没法在全部浏览器上运行。

因此,为了理想主义和世界和平(全部浏览器上都能为所欲为调用它),必要的时候须要咱们本身去实现一个bind。那么,一个bind函数须要具有什么功能呢?java

bind函数的核心做用:绑定this、初始化参数

绑定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来将slicethis指向argumentsargs就是调用bind时传入的初始化参数(剔除了第一个参数oThis)。将args与绑定函数执行时的实参arguments经过concat连起来做为参数传入,就实现了bind函数初始化参数的效果。函数

bind函数的另一个也是最主要的做用:绑定this指向,就是经过将调用bind时的thisself)指向指定的oThis来完成。这样当咱们要使用bind绑定某个对象时,执行绑定函数,它的this就永远固定为指定的对象了~oop

遇到new操做符的时候呢

到这里,咱们已经能够用上面的版原本使用大部分场景了。可是~

可是,这种方案就像前面说的,它会永远地为绑定函数固定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,分类处理

咱们知道检测一个对象是否经过某个构造函数使用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,将fBoundprototype原型对象指向C的prototype原型对象(上例中就是self),这样的话若是将fBound做为构造函数(使用new操做符)实例化一个对象,那么这个对象也是C的实例,this instanceof self就会返回true。这时就将self指向新建立的对象的this上就能够达到原生bind的效果了(再也不固定指定的this)。不然,才使用oThis,即绑定指定的this

可是这样作会有什么影响?将fBoundprototype原型对象直接指向selfprototype原型对象,那么当修改fBoundprototype对象时,self(上述C函数)的prototype对象也会被修改!!考虑到这个问题,咱们须要另一个function来帮咱们作个中间人来避免这个问题,咱们看看MDN是怎么实现bind的。

MDN提供的Polyfill方案 及 YUI库extend函数实现

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(经过实例化的方式)。这时,修改fBoundprototype对象,就不会影响到selfprototype对象啦~并且fNOP是空对象,因此几乎不占内存。

其实这个思路也是YUI库如何实现继承的方法。他的extend函数以下:

function extend(Child, Parent) {
    var F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
}

最后一步是将Childconstructor指回Child

总结

实现一个原生的函数,最重要的是理清楚它的做用和功能,而后逐一去实现它们包括细节,基本上就不会有问题~

这里用到的一些关于prototypeinstanceof的具体含义,能够参考阮一峰老师的 prototype 对象,相信对你理解JavaScript的原型链和继承会有帮助~

好啦就这样,晚安美梦了各位🌛✨

相关文章
相关标签/搜索