深究Function.prototype.bind

老臘肉學長發表於2018-05-16

前言

在讀這篇文章之前,希望你對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的影響

謝謝你能看到這裡。

原文摘自我的個人部落格,歡迎進來踩踩。