關於JavaScript中this的軟繫結

酥風發表於2018-05-22

首先,什麼是軟繫結?

所謂軟繫結,是和硬繫結相對應的一個詞,在詳細解釋軟繫結之前,我們先來看看硬繫結。在JavaScript中,this的繫結是動態的,在函式被呼叫的時候繫結,它指向什麼完全取決於函式在哪裡呼叫,情況比較複雜,光是繫結規則就有預設繫結、隱式繫結、顯式繫結、new繫結等,而硬繫結是顯式繫結中的一種,通常情況下是通過呼叫函式的apply()call()或者ES5裡提供的bind()方法來實現硬繫結的。

硬繫結有什麼問題,為什麼需要軟繫結

上述三個方法好是好,可以按照自己的想法將函式的this強制繫結到指定的物件上(除了使用new繫結可以改變硬繫結外),但是硬繫結存在一個問題,就是會降低函式的靈活性,並且在硬繫結之後無法再使用隱式繫結或者顯式繫結來修改this的指向。

在這種情況下,被稱為軟繫結的實現就出現了,也就是說,通過軟繫結,我們希望this在預設情況下不再指向全域性物件(非嚴格模式)或undefined(嚴格模式),而是指向兩者之外的一個物件(這點和硬繫結的效果相同),但是同時又保留了隱式繫結和顯式繫結在之後可以修改this指向的能力。

軟繫結的具體實現

在這裡,我用的是《你不知道的JavaScript 上》中的軟繫結的程式碼實現:

if(!Function.prototype.softBind){
    Function.prototype.softBind=function(obj){
        var fn=this;
        var args=Array.prototype.slice.call(arguments,1);
        var bound=function(){
            return fn.apply(
                (!this||this===(window||global))?obj:this,
                args.concat.apply(args,arguments)
            );
        };
        bound.prototype=Object.create(fn.prototype);
        return bound;
    };
}
複製程式碼

我們先來看一下效果,之後再討論它的實現。

function foo(){
    console.log("name: "+this.name);
}

var obj1={name:"obj1"},
    obj2={name:"obj2"},
    obj3={name:"obj3"};

var fooOBJ=foo.softBind(obj1);
fooOBJ();//"name: obj1" 在這裡軟繫結生效了,成功修改了this的指向,將this繫結到了obj1上

obj2.foo=foo.softBind(obj1);
obj2.foo();//"name: obj2" 在這裡軟繫結的this指向成功被隱式繫結修改了,繫結到了obj2上

fooOBJ.call(obj3);//"name: obj3" 在這裡軟繫結的this指向成功被硬繫結修改了,繫結到了obj3上

setTimeout(obj2.foo,1000);//"name: obj1"
/*回撥函式相當於一個隱式的傳參,如果沒有軟繫結的話,這裡將會應用預設繫結將this繫結到全域性環
境上,但有軟繫結,這裡this還是指向obj1*/
複製程式碼

可以看到軟繫結生效了。下面我們來具體看一下softBind()的實現。

在第一行,先通過判斷,如果函式的原型上沒有softBind()這個方法,則新增它,然後通過Array.prototype.slice.call(arguments,1)獲取傳入的外部引數,這裡這樣做其實為了函式柯里化,也就是說,允許在軟繫結的時候,事先設定好一些引數,在呼叫函式的時候再傳入另一些引數(關於函式柯里化大家可以去網上搜一下詳細的講解)最後返回一個bound函式形成一個閉包,這時候,在函式呼叫softBind()之後,得到的就是bound函式,例如上面的var fooOBJ=foo.softBind(obj1)

bound函式中,首先會判斷呼叫軟繫結之後的函式(如fooOBJ)的呼叫位置,或者說它的this的指向,如果!this(this指向undefined)或者this===(window||global)(this指向全域性物件),那麼就將函式的this繫結到傳入softBind中的引數obj上。如果此時this不指向undefind或者全域性物件,那麼就將this繫結到現在正在指向的函式(即隱式繫結或顯式繫結)。fn.apply的第二個引數則是執行foo所需要的引數,由上面的args(外部引數)和內部的arguments(內部引數)連線成,也就是上面說的柯里化。

其實在第一遍看這個函式時,也有點迷,有一些疑問,比如var fn=this這句,在foo通過foo.softBind()呼叫softBind的時候,fn到底指向誰呢?是指向foo還是指向softBind?我們可以寫個demo測試,然後可以很清晰地看出fn指向什麼:

var a=2;
function foo(){
}
foo.a=3;
Function.prototype.softBind=function(){
    var fn=this;
    return function(){
        console.log(fn.a);
    }
};
Function.prototype.a=4;
Function.prototype.softBind.a=5;

foo.softBind()();//3
Function.prototype.softBind()();//4
複製程式碼

可以看出,fn(或者說this)的指向還是遵循this的繫結規則的,softBind函式定義在Function的原型Function.prototype中,但是JavaScript中函式永遠不會“屬於”某個物件(不像其他語言如java中類裡面定義的方法那樣),只是物件內部引用了這個函式,所以在通過下面兩種方式呼叫時,fn(或者說this)分別隱式繫結到了fooFunction.prototype,所以分別輸出3和4。後面的fn.apply()也就相當於foo.apply()

第一次在掘金寫部落格,以後多寫點吧,也算是對知識點的一個總結,同時也希望能幫到大家。emmm 就這樣。

相關文章