这篇文章上次修改于 238 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

手写bind原理

原理

改变函数的this指向,但此时不会执行函数。改变this指向的还有call、apply,与bind区别是绑定时执行了函数。bind执行后返回的是绑定后的函数,而call和apply返回的是函数执行后的结果。

apply与call的区别是传参方式不同,前者是以数组的形式,后者以列表的形式。

bind的实现

bind方法在Function对象上的原型对象上且只接受第一个参数this,其余参数通过arguments保存在bind函数作用域。

if(!Function.prototype.bind){        // ①
  Function.prototype.bind = function(targetThis) {
    if(typeof this !== 'function') return;        // ②
    var args = Array.prototype.slice.call(arguments, 1),         // ③
      oldFunc = this,        // ④
      Fn = function() {},        // ⑤
      FBund = function() {        // ⑥
        return oldFunc.apply(            // ⑦
          this instanceof FBund ? this : targetThis,            // ⑧
          args.cancat(Array.prototype.slice.call(arguments))            // ⑨
        )
      }
    if (this.prototype) {
      Fn.prototype = this.prototype;        // ⑩
    }
    FBund.prototype = new Fn();            // ①①
    return FBund;
  }
}

个人理解

① 判断Function对象原型上是否存在bind方法。
② 判断需要绑定的对象是不是一个函数。
③ 利用Array.prototype.slice.call将arguments类数组转化为纯数组,并从数组索引为1的位置截取数组赋予args变量。
④ 保存未绑定前的this。
⑤ 声明一个空函数(用来保存原函数的原型对象)。
⑥ 声明一个绑定this后需返回的新函数。
⑦ 给新函数绑定this,并传参。
⑧ 判断执行环境,若new了新函数,就返回新函数的实例对象this,否则就是新函数直接执行,this不变。

instanceof干了什么???

  • instanceof是用来判断某构造函数的原型对象是否出现在某对象的原型链上。这里是判断this对象的原型链上是否存在FBund的原型。

new操作时又发生了什么???

  • new时会在函数顶部隐式生成一个this对象,并将该函数原型对象的地址引用给this对象的__proto__属性,接着函数会执行,并通过this.xxx=xxx的形式向this对象添加属性,一般函数执行完后返回会是this对象,也就是我们理解的实例对象,此时new操作完成。
// 隐式执行,不可见
function Xxx() {
    this = {
        __proto__: Xxx.prototype
    }
        ......
        ......
        ......
    return this; // 此处返回的不一定是this对象
}

new的返回结果不一定就是this实例。当被new的构造函数里有return结果,并且返回的是对象或数组或函数时,new的返回值就不是隐式生成的this实例了,而是"构造函数"里返回的结果。this的原型链是函数的原型链的引用(浅拷贝),两者指向同一存储空间,即"Object.getPrototypeOf(<函数实例>) === 函数.prototype"

// 用js实现一个new方法
function _new(_constructor) {
    var obj = {};
    var args = Array.prototype.slice.call(arguments, 1);
    obj.__proto__ = _constructor.prototype;
    let res = _constructor.apply(obj, args); // 返回构造函数执行的结果
    if(res && (typeof res === 'object' || typeof res === 'function') {
        return res;
    }
    retrun obj;
}

⑨ 参数数组拼接
⑩ 判断调用者原型是否存在,并将原型付赋给空函数Fn的原型。

Function.prototype.bind(),当这样调用bind时,bind函数的this指向Function.prototype,所以this.prototype就等价于Function.prototype.prototype为undefined,此处判断为了维护原型关系

①① new一个Fn函数,将得到的实例对象赋予FBund函数的原型,这样返回的新函数就有了原函数的原型对象。

这时又要思考了,为什么非要new一哈,将Fn的实例对象返给FBund原型呢???

  • new操作上面解释过了,会返回包含__proto__属性的新对象,__proto__存放着调用者的prototype和constructor,这时bind执行完后返回的新函数的原型链上,就包含了调用者的原型和构造函数,而且改变新函数的原型链也不会影响到父级的原型状态。若直接FBund.prototype=this.prototype,这是浅拷贝,改变FBund新函数的原型属性就会影响到调用者(原函数)的原型属性。

注意

一个函数能通过bind改变一次this指向,重复绑定的this都指向第一次的bind绑定的对象,不管后续用的是bind、还是call或apply,都无法改变this,除了new操作。重复bind所传入的实参(不包括第一个参数)会叠加。???又发生了什么呢

当第一次bind后返回的是一个新的函数FBund,第二次bind的时,也就是对FBund进行bind,bind后就有用吗?除了实参数叠加了,并没啥用,此时再调用bind后的方法,就会执行⑦标记的操作,不难看出原函数绑定的还是第一次bind传入的this,参数累加就是后续concat拼接的。

总结

bind运用到了很多基础知识,作用域、闭包、继承、原型链、new的过程、instanceof原理等。理解起来是很复杂,但只要理解了,就会发现,真香!!!可能会有写的不对的或需要补充的地方,欢迎小伙伴留下评论。