深究Function.prototype.bind

3,080 阅读3分钟

前言

在读这篇文章之前,希望你对Function.prototype.bind有所了解。

如果还没有的话,强烈推荐去看看MDN上关于它的介绍,飞机票

主要有以下两个特征:

  1. 多次bind,仅第一次的bind传入的绑定this生效
  2. 使用new 操作bind返回的构造函数,曾经绑定的this会失效

bind的polyfill

MDN上为了向下兼容给出了bind的polyfill,先把代码贴出来:

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 does not have a prototype property
      fNOP.prototype = this.prototype; 
    }
    fBound.prototype = new fNOP();

    return fBound;
  };
}

一段示例代码

var o1 = { a: 1 }
var o2 = { b: 2 }
var f = function () {
    console.log(this)
    console.log([].slice.call(arguments))
}

var f1 = f.bind(o1, 1, 2) // A行
var f2 = f1.bind(o2, 3, 4) // B行

f2(5, 6) // C行

学习方法有正向也有反向,我们从运行代码来解释这段polyfill

分析

接下来将会从执行上下文栈来解析这段代码运行的整个过程。 如果对“执行上下文栈”还不了解的话,推荐看我的另一篇文章——执行上下文

1. 刚开始时的全局执行上下文:

  1. 变量对象:o1,o2,f,f1,f2
  2. 作用域链:目前为空
  3. this,指向window

2. A行执行时加入的执行上下文:

  1. 变量对象:oThis === o1,aArgs === [1, 2],fToBind === f,fNOP,fBound
  2. 作用域链:全局执行上下文
  3. this,指向f
  4. 返回的f1,指向变量对象的fBound,它的原型链:fBound.prototype.proto === f.prototype

3. B行执行时加入的执行上下文:

  1. 变量对象:oThis === o2,aArgs === [3, 4],fToBind === f1,fNOP,fBound
  2. 作用域链:全局执行上下文
  3. this,指向f1
  4. 返回的f2,指向变量对象的fBound,它的原型链:fBound.prototype.proto === f1.prototype

4. C行执行时加入的执行上下文:

  1. 变量对象:arguments
  2. 作用域链:比较复杂,看下面说明
  3. this,指向window

C行其实会执行两次函数

第一次:

  1. 变量对象:arguments === [5, 6]
  2. 作用域链:B行的执行上下文(闭包)、全局执行上下文
  3. this,指向window
f2(5, 6) === return f1.apply(o2, [3, 4, 5, 6])

第二次:

  1. 变量对象:arguments === [3, 4, 5, 6]
  2. 作用域链:A行的执行上下文(闭包)、全局执行上下文
  3. this,指向o2
return f1.apply(o2, [3, 4, 5, 6])  === return f.apply(o1, [1, 2, 3, 4, 5, 6]

5. 结果

所以f2(5, 6)的打印的结果就是

{a: 1}
[1, 2, 3, 4, 5, 6]

可以直接放到chrome的开发者工具里运行得到结果。

两处亮点

1. 维护原型关系

这里使用的是“原型式继承”,可以参考我的另一篇文章——类相关

在这里的作用是,把原函数(f)的原型保留下来,以供第二个亮点使用。

2. bind不影响new

我想你一定很疑惑fBound里的这段代码

this instanceof fNOP ? this : oThis

其实这里的作用就是为了bind返回的函数不影响new操作符创建对象(也就是this被忽略)。

如果再执行以下语句,再上门的基础上修改f:

var f = function () {
    this.c = 3
    console.log(this)
    console.log([].slice.call(arguments))
}

var f2Obj = new f2(5, 6);

// 运行过程,下面的this指将要创建的新对象:
f2(5, 6) === return f1.apply(this, [3, 4, 5, 6] === return f.apply(this, [1, 2, 3, 4, 5, 6]

// 结果(在chrome上执行)
打印:
f {c: 3}
[1, 2, 3, 4, 5, 6]

并且 f2Obj.c === 3

总结

不由得感叹这个polyfill的作者,思维太缜密了。我只能通过解析执行上下文一步一步来了解整个设计思路。

  1. 借助闭包保存每次bind传入的参数,包括thisArg和args
  2. 返回的fBound形成调用链,每一个fBound都引用上一个fBound,尾端是原函数
  3. 使用原型式继承的方式使new操作符创建新对象时候不受曾经绑定的this的影响

谢谢你能看到这里。

原文摘自我的个人博客,欢迎进来踩踩。