bind函式polyfill原始碼解析

Nirvana-cn發表於2019-01-19

準備知識

使用new來呼叫函式會自動執行下面的操作:

  1. 建立一個全新的物件
  2. 這個新物件會被執行原型連線
  3. 這個新物件會繫結到函式呼叫的this
  4. 如果函式沒有返回其他物件,那麼new表示式中的函式呼叫會自動返回這個新物件

注意this繫結規則,new操作具有最高的優先順序

《你不知道的JavaScript(上卷)》提供了一個例子,bar被硬繫結到obj上,但是new bar(3) 並沒有像我們預計的那樣把obj.a修改為3。相反,new修改了硬繫結呼叫bar()中的this。因為使用了new繫結,我們得到了一個名字為baz的新物件,並且baz.a的值為3。

function foo(something) {
    this.a = something
}
var obj = {}
var bar = foo.bind(obj)
bar(2)
console.log(obj.a)  //2
var baz = new bar(3)
console.log(obj.a)  //2
console.log(baz.a)  //3
複製程式碼

instanceof運算子的第一個變數是一個物件,暫時稱為A;第二個變數一般是一個函式,暫時稱為B。

instanceof判斷準則:沿著A的__proto__這條線來找,同時沿著B的prototype這條線來找,如果兩條線能找到同一個引用,即同一個物件,那麼就返回true。

原始碼分析

MDN上提供的polyfill如下,主要的疑惑點應該就是 this instanceof fNOP 作用是什麼?

if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== `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 // 這段程式碼會判斷硬繫結函式是否是被new呼叫,如果是的話就會使用新建立的this替換硬繫結的this
                 ? 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
  }
}
複製程式碼

this instanceof fNOP 單獨看是看不明白的,需要結合以下程式碼才能說明它的作用

if (this.prototype) {
    fNOP.prototype = this.prototype; 
}
fBound.prototype = new fNOP()
複製程式碼

首先我們要清楚:bind(…)會返回一個硬編碼的新函式,它會把引數設定為this的上下文並呼叫原始函式。

重點就是bind(…)返回的是一個函式!函式!函式!這意味著可以對bind(…)返回的函式進行new操作,那麼問題就來了。

this繫結中new操作具有最高的優先順序,如果執行new操作,bind(…)應該不起作用,this應該指向new出來的新物件。

清楚了以上內容,我們開始閱讀程式碼。

if (typeof this !== `function`) {
      throw new TypeError(`Function.prototype.bind - what is trying to be bound is not callable`)
    }
複製程式碼

bind(…)必須由函式呼叫,所以以上程式碼對this的型別進行檢查,如果不是函式型別則丟擲錯誤。

var aArgs   = Array.prototype.slice.call(arguments, 1)
var fToBind = this
var fBound  = function() {
              return fToBind.apply(this instanceof fNOP // 這段程式碼會判斷硬繫結函式是否是被new呼叫,如果是的話就會使用新建立的this替換硬繫結的this
                     ? this
                     : oThis,
                     aArgs.concat(Array.prototype.slice.call(arguments)))
           }
複製程式碼

aArgs獲取傳入的其它引數,fToBind獲取需要硬繫結的函式,fBound為返回的繫結操作函式,我們先忽略fBound裡面的內容,繼續往下看。

if (this.prototype) {
    fNOP.prototype = this.prototype; 
}
fBound.prototype = new fNOP()
複製程式碼

我們假設對bind(…)返回的函式進行new操作(原型鏈如下),則this instanceof fNOP 為true,此時我們就知道執行了new操作,硬繫結的this不能生效,需要把this繫結到新生成的物件上。

bind函式polyfill原始碼解析

如果沒有進行new操作的話,就用apply模擬bind繫結,一切按照原計劃進行。

最後我們分析一下維護原型關係的重要性,例子如下:

function Foo(){
    console.log(this.a);
    this.a=1;
}
Foo.prototype.show=function() {console.log(this.a)};
Foo(); // undefined
var obj1=new Foo();
obj1.show();

var bar=Foo.bind({a:2});
bar(); // 2
var obj2=new bar();
obj2.show();
複製程式碼

因為bind函式內部保持了原型關係的繼承,所以物件obj2才能訪問到原型上的show方法。

** 注意:Foo.show()是錯誤的,因為Foo的原型指向的是Function.prototype,只有Foo的例項才能呼叫show方法。

相關文章