深究Function.prototype.bind

前言

在讀這篇文章以前,但願你對Function.prototype.bind有所瞭解。git

若是尚未的話,強烈推薦去看看MDN上關於它的介紹,飛機票github

主要有如下兩個特徵:chrome

  1. 屢次bind,僅第一次的bind傳入的綁定this生效
  2. 使用new 操做bind返回的構造函數,曾經綁定的this會失效

bind的polyfill

MDN上爲了向下兼容給出了bind的polyfill,先把代碼貼出來:bash

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閉包

分析

接下來將會從執行上下文棧來解析這段代碼運行的整個過程。 若是對「執行上下文棧」還不瞭解的話,推薦看個人另外一篇文章——執行上下文app

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)的打印的結果就是ui

{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的影響

謝謝你能看到這裏。

原文摘自個人我的博客,歡迎進來踩踩。

相關文章
相關標籤/搜索