詳解bind

悶聲君發表於2018-11-22

本文主要詳細分解bind函式的polyfill實現。

覺得還行的老闆們可以給個gayhub的satr:github.com/flyFatSeal/…

一:bind基礎

bind函式的具體功能與apply,call函式相似都是改變函式體內的this物件,也就是擴充函式作用域。在MDN中是這樣介紹bind的

bind()方法建立一個新的函式, 當這個新函式被呼叫時其this置為提供的值,其引數列表前幾項置為建立時指定的引數序列。

由上可知,bind函式和apply,call函式不同在於bind函式執行後返回的是一個繫結了this物件的函式,而apply和call函式是直接執行

下面我們來看一個簡單的例子用來說明apply,call和bind的區別: 例 1:

var x = 'out'
var a = {
  x:'inner',
  func:function(){
    console.info('現在的所在的環境是',this.x)
  }
}

var b = a.func// inner
b.apply(a) // 現在的所在的環境是inner
b.call(a) // 現在的所在的環境是inner
b.bind(a) // 沒有輸出因為bind函式返回的是一個新的函式
typeof b.bind(a) === 'function' // true
b.bind(a)() // 現在的所在的環境是inner
複製程式碼

因此bind函式可以非同步執行,這是它區別於apply和bind的主要地方。

二:詳解polyfill

bind方法是ECMAScript 5才加入的新方法,因此存在著瀏覽器相容性問題,在具體執行中最好加入polyfill增加相容性。在MDN的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() {
          // this instanceof fNOP === true時,說明返回的fBound被當做new的建構函式呼叫
          return fToBind.apply(this instanceof fNOP
                 ? this
                 : oThis,
                 // 獲取呼叫時(fBound)的傳參.bind 返回的函式入參往往是這麼傳遞的
                 aArgs.concat(Array.prototype.slice.call(arguments)));
        };
    // 維護原型關係
    if (this.prototype) {
      // Function.prototype doesn't have a prototype property
      fNOP.prototype = this.prototype; 
    }
    // 下行的程式碼使fBound.prototype是fNOP的例項,因此
    // 返回的fBound若作為new的建構函式,new生成的新物件作為this傳入fBound,新物件的__proto__就是fNOP的例項
    fBound.prototype = new fNOP();
    return fBound;
  };
}
複製程式碼

剛看到MDN的polyfill時,基本處於懵逼狀態,後面細細琢磨才明白,要清晰bind的polyfill需要了解bind方法的操作流程,new操作符,和繼承原理才行。

主要疑惑:

  • bind方法的polyfill思路是什麼
  • 為什麼要this為function物件
  • 如何將外部的引數傳入
  • fNOP起到了什麼作用
  • 為何在fToBind.apply時要判斷對bind呼叫是否是new操作符

bind方法的polyfill思路

通過前面的介紹,bind方法與apply,call不同在於bind方法呼叫時返回的是一個新函式,而apply,call是立即執行,在不支援bind的瀏覽器環境下,需要用apply來模擬bind執行,核心在於bind是返回一個繫結this物件的函式,因此在polyfill中只需要返回一個函式,在返回的函式中通過apply方法繫結this物件和處理引數即可。

this型別判斷

bind方法返回的是一個繫結了this物件的函式,並且bind是Function的方法,在函式體上呼叫,因此要對bind呼叫時的this進行判斷如果不是function物件則丟擲錯誤。

if (typeof this !== 'function') {
      // 判斷呼叫bind方法的是否是函式。
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

複製程式碼

引數處理

在bind方法呼叫時需要傳入兩個引數,第一個是繫結的this物件,第二個是繫結的this物件的引數,因為bind方法返回一個新的函式,新的函式又可以傳遞引數。 所以在bind中一共有兩個地方的引數需要處理,呼叫bind方法時的引數,和bind方法呼叫返回新函式,新函式在執行時傳入的引數,這樣說有點抽象,來看一個例子。

var price =  function (a,b){
  //price.bind(obj,10) obj的value屬性值
  console.log('繫結this物件的價格是',this.value)
  //price.bind(obj,10)第二個引數的值
  console.log('呼叫bind方法傳入的價格是',a)
  //price.bind(obj,10)(20)呼叫bind方法執行後函式傳入的引數
  console.log('呼叫bind方法執行後的函式傳入的價格是',b)
}
var obj = {
  value:5
}
price.bind(obj,10)(20)
//繫結this物件的價格是 5
//呼叫bind方法傳入的價格是 10
//呼叫bind方法執行後的函式傳入的價格是 20

複製程式碼

回到具體的polyfill中,其中

var aArgs = Array.prototype.slice.call(arguments, 1)

這裡的aArgs變數就是用來儲存bind方法呼叫時傳入的引數,其中通過Array.prototype.slice.call可以把傳入的引數轉化為陣列,為什麼要轉化為陣列,因為在具體呼叫bind時,引數個數是不確定的,在不確定引數個數時需要使用apply方法,apply方法的第二引數接受一個陣列。而...slice.call(arguments, 1),則是把bind方法呼叫時傳入的引數從第二個開始轉化為陣列(因為bind方法的第一個引數是繫結的this物件)。
注意這裡處理的是bind(this,arguments)中的arguments,還有bind方法執行後的函式再呼叫時傳入的引數需要處理也就是bind(this,arguments)(fArgs)中的fArgs。

在polyfill中,是這樣處理fArgs的

fBound  = function() {
          return fToBind.apply(this instanceof fNOP
                 ? this
                 : oThis,
                 // 獲取呼叫時(fBound)的傳參.bind 返回的函式入參往往是這麼傳遞的
                 aArgs.concat(Array.prototype.slice.call(arguments)));
        };
複製程式碼

在返回函式中通過aArgs.concat(Array.prototype.slice.call(arguments)))一起把bind方法傳入的引數(aArgs)和fArgs組合成為一個新的陣列,作為apply方法的第二個引數。

fNOP函式解析

在polyfill中,fNOP作用類似於寄生組合繼承中的object.create()。作為一箇中間函式連結返回的新函式和原函式的原型鏈。也就是繼承原函式的方法和原型鏈。主要處理當返回的fBound被作為new的建構函式時原型鏈的繼承的情況,注意當這種情況發生時bind方法傳入繫結的this被忽略,引數傳遞不變,this使用原函式的this物件。

   fNOP = function() {},
        fBound  = function() {
          return fToBind.apply();
        };
    // 維護原型關係
    if (this.prototype) {
      // Function.prototype doesn't have a prototype property
      fNOP.prototype = this.prototype; 
    }
    // 下行的程式碼使fBound.prototype是fNOP的例項,因此
    // 返回的fBound若作為new的建構函式,new生成的新物件作為this傳入fBound,新物件的__proto__就是fNOP的例項
    fBound.prototype = new fNOP();
    return fBound;
複製程式碼

為何要處理new func.bind()這種情況呢?來看一下new操作的執行過程

new操作符解釋引用 sunshine小小倩這篇文章

var a = new myFunction("Li","Cherry");

new myFunction{
    var obj = {};
    obj.__proto__ = myFunction.prototype;
    var result = myFunction.call(obj,"Li","Cherry");
    return typeof result === 'obj'? result : obj;
}
複製程式碼

1.建立一個空物件 obj;
2.將新建立的空物件的隱式原型指向其建構函式的顯示原型。
3.使用 call 改變 this 的指向
4.如果無返回值或者返回一個非物件值,則將 obj 返回作為新物件;如果返回值是一個新物件的話那麼直接直接返回該物件。

可知在執行new操作時this的指向已經被改變了如果此時還是使用bind方法傳入的要繫結的this,那麼原函式的原型鏈就會被切斷,導致new出來的新物件無法繼承原函式的方法。所以當fToBind被當做建構函式使用時,放棄繫結傳入的this物件。

總結

由上可知,bind的polyfill主要處理了bind方法呼叫時引數傳遞問題,被當做建構函式使用時的繼承問題,如果對bind執行流程和繼承原理熟悉,bind的polyfill就可以一眼看穿了。

相關文章