Skip to content

深究Function.prototype.bind #12

@coconilu

Description

@coconilu

前言

在读这篇文章之前,希望你对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 doesn't 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的思路:

bind的函数体维护5个变量——绑定的this(oThis)、当前函数(也就是调用bind的函数)、参数、返回的函数、用于维护原型关系的空函数

返回的函数

返回的函数的函数体里:使用apply调用当前函数,先通过this判断返回的函数是不是维护原型关系的空函数的实例(被new操作符调用),如果是的话,使用this作为apply绑定的this,如果不是的话,使用oThis作为apply绑定的this,参入的参数为拼接的。

因为直接调用返回函数的话,this会指向undefined或者window,所以需要维护原型,接着往下。

用于维护原型关系的空函数

过程是:把当前函数的原型赋值给维护原型关系的空函数的原型,然后使用原型链继承的方式返回一个实例并赋值给返回的函数,这样一来,返回的函数被new操作符调用的时候,可以通过this来判断是不是维护原型关系的空函数的实例。

这个逻辑是适合嵌套的。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions