從this機制看bind的實現

前言

從本文你可以得到:javascript

  • 理解this的綁定機制及其優先級
  • 學會使用apply/call實現bind函數
  • 學會不使用apply/call實現bind函數

廢話很少說,咱們一塊兒來看看this的綁定機制。前端

this綁定機制

開局上結論this有四種綁定模式分別是默認綁定、隱式綁定、顯式綁定、new綁定。java

他們之間的優先級關係爲:new綁定 > 顯式綁定 > 隱式綁定 > 默認綁定面試

讓咱們來看一些例子分清這幾種綁定:app

例子1:默認綁定函數

// 默認綁定
var str = 'hello world'
function log() {
    console.log(this.str) 
}

// 此時this默認指向window
log() // hello world

// 在嚴格模式下this默認指向undefined的
'use strict'
var str2 = 'hello world'
function log() {
    console.log(this.str2) 
}

// 此時this指向undefined
log() // 報錯TypeError,由於程序從undefined中獲取str2
複製代碼

例子2:隱式綁定post

// 隱式綁定通常發生在函數做爲對象屬性調用時
var bar = 'hello'
function foo() {
    console.log(this.bar)
}

var obj = {
    bar: 'world',
    foo: foo
}

foo() // hello,this指向window因此輸出hello
obj.foo() // world,this隱式綁定了obj,這時候this指向obj因此輸出world
複製代碼

例子3:顯式綁定ui

// 顯式綁定就是咱們常談的apply,call,bind
var bar = 'hello'
var context = {
    bar: 'world'
}
function foo() {
    console.log(this.bar);
}

foo() // hello
foo.call(context) // world 可見此時this的指向已經變成了context
複製代碼

例子4:new綁定this

new綁定比較特殊,new大部分狀況下是建立一個新的對象,並將this指向這個新對象,最後返回這個對象。spa

function Foo(bar) {
    this.bar = bar
}

// 建立一個新的對象,並將this指向這個對象,將這個對象返回賦值給foo
var foo = new Foo(3);
foo.bar // 3
複製代碼

說完this的綁定類型,咱們考慮下下面的代碼的輸出

var context = {
    bar: 2
}

function Foo() {
    this.bar = 'new bar'
}

var FooWithContext = Foo.bind(context);
var foo = new FooWithContext();

// 考慮下面代碼的輸出
console.log(foo.bar) 
console.log(context.bar)

// 結果是:new bar 2
/** * 咱們能夠發現雖然將使用bind函數將this綁定到context上, * 但被new調用的Foo,他的this並無綁定到context上。 */
複製代碼

四種this綁定的優先級驗證

從上述例子2能夠推斷隱式綁定優先級是高於默認綁定的,因此這裏咱們只推導後續三種的綁定的優先級關係。

顯式綁定和new綁定

例子5:

// 咱們先驗證隱式綁定和顯式綁定的優先級關係
var context = {
    bar: 1
}

function foo() {
    // 對bar進行賦值
    this.bar = 3;
}

// 進行顯式綁定
var fooWithContext = foo.bind(context);
var instance = new fooWithContext();

console.log(context.bar); // 1
console.log(instance.bar); // 3

// 可見foo並無改變context.bar的值而是建立了一個新對象,符合咱們對new綁定的描述
複製代碼

根據上面的列子咱們能夠得出結論new綁定 > 顯式綁定

另外幾種綁定的優先級狀況

根據例子2和例子3,咱們能夠輕鬆推導出隱式綁定和顯式綁定要優先級高於默認綁定

咱們驗證一下隱式綁定和顯式綁定的優先級關係。

例子6:

var obj = {
    bar: 2
}

var context = {
    bar: 3
}

function foo () {
    this.bar = 4
}

// 將foo的this綁定到context上
var fooWithContext = foo.bind(context);
// 將綁定後的函數,賦值給obj的屬性foo
obj.foo = fooWithContext;
obj.foo();

console.log(obj.bar); // 2 並無改變obj.bar的值
console.log(context.bar); // 4 context.bar的值發生了改變
複製代碼

可見顯式綁定的this優先級要高於隱式綁定

最後咱們即可以得出結論new綁定 > 顯式綁定 > 隱式綁定 > 默認綁定。

用apply實現bind

剛剛說了那麼多this的綁定問題,這到底和咱們實現bind有什麼關係?

咱們來看看一段簡單的bind的實現代碼:

Function.prototype.bind(context, ...args) {
   const fn = this;
   return (...innerArgs) => {
       return fn.call(context, ...args, ...innerArgs)
   }
}
複製代碼

這個bind函數,在大部分狀況都是能正常工做的,可是咱們考慮以下場景:

function foo() {
    this.bar = 3
}

var context = {
    bar: 4
}

var fooWithContext = foo.bind(context);
var fooInstance = new fooWithContext();

console.log(context.bar) // 3
複製代碼

能夠看到,被new調用後的foo,在運行時this依然指向context,這不符合咱們剛剛根據原生方法推斷的綁定優先級:new綁定 > 顯式綁定

因此咱們在實現bind的時候,須要考慮維護new調用的狀況

咱們來看看如何實現一個真正的bind:

Function.prototype.bind(context, ..args) {
     var fToBind = this;
     
     // 先聲明一個空函數,用途後面介紹
     var fNop = function() {};
     
     var fBound = function(...innerArgs) {
         // 若是被new調用,this應該是fBound的實例
         if(this instanceof fBound) {
             /** * cover住new調用的狀況 * 因此其實咱們這裏要模擬fToBind被new調用的狀況,並返回 * 咱們使用new建立的對象替換掉bind傳進來的context */
           return fToBind.call(this, ...args, ...innerArgs)
         } else {
            // 非new調用狀況下的正常返回
            return fToBind.call(context, ...args, ...innerArgs)
         }
     }
     
     // 除了維護new的this綁定,咱們還須要維護new致使的原型鏈變化
     // 執行new後返回的對象的原型鏈會指向fToBind
     // 可是咱們調用bind後實際返回的是fBound,因此咱們這裏須要替換掉fBound的原型
     
       fNop.prototype = this.prototype;
       // fBound.prototype.__proto__ = fNop.prototype
       fBound.prototype = new fNop();
       /** * 這樣當new調用fBound後,實例依然能訪問fToBind的原型方法 * 爲何不直接fBound.prototype = this.prototype呢 * 考慮下將fBound返回後,給fBound添加實例方法的狀況 * 即fBound.prototype.anotherMethod = function() {} * 若是將fToBind的原型直接賦值給fBound的原型,添加原型方法就會 * 污染源方法即fToBind的原型 */
     return fBound
 }

複製代碼

到這裏咱們就實現了一個符合原生表現的bind函數,可是有時候架不住有人問那不用apply和call如何實現bind呢?接下來咱們使用隱式綁定來實現一個bind

不使用apply和call實現bind

咱們剛剛分析完實現bind的實現須要注意的點,這裏就不重複說明了,咱們看看如何使用隱式綁定來模仿bind。

// 咱們把關注點放在如何替換call方法上
Function.prototype.bind(context, ...args) {
    var fToBind = this;
    var fNop = function() {};
    var fBound = function(...innerArgs) {
        // 咱們將fToBind賦值給context一個屬性上。
        context.__fn = fToBind;
        if(this instanceof fBound) {
            // 模擬new調用,建立一個新對象,新對象的原型鏈指向fBound的原型
            var instance = Object.create(fBound);
            instance.__fn = fToBind;
            var result = instance.__fn(...args, ...innerArgs);
            delete instance.__fn;
            // new調用時,若是構造函數返回了對象,使用返回的對象替換this
            if(result) return result;
            return instance;
        } else {
            // 在__fn沒有顯式綁定的狀況下,__fn運行時this指向context
            var result = context.__fn(...args, ...innerArgs);
            // 調用完後將context的__fn屬性刪除
            delete context.__fn;
            return result;
      }
    }
    
    fNop.prototype = this.prototype;
    fBound.prototype = new fNop();
    return fBound;
}
複製代碼

到這裏不使用apply實現的bind就大功告成了

總結

我來總結下一共有哪些點須要認清楚:

  • this有四種綁定模式,默認綁定、隱式綁定、顯式綁定、new綁定
  • this四種綁定的優先級關係:new綁定 > 顯式綁定 > 隱式綁定 > 默認綁定
  • 實現bind須要額外維護new綁定的狀況

看了這麼多可能會有朋友問,箭頭函數呢?

歡迎閱讀我其餘文章:

參考資料:

相關文章
相關標籤/搜索