你不知道的JavaScript-this繫結

vuestar發表於2018-02-07

this是在執行時繫結的,並不是在編寫時繫結,它的上下文取決於函式呼叫時的各種條件。this的繫結和函式宣告的位置沒有任何關係,只取決於函式的呼叫方式。 當一個函式被呼叫時,會建立一個活動記錄(有時也稱為執行上下文)。這個記錄會包含函式在哪裡被呼叫(呼叫棧)。函式的呼叫方式、傳入的引數等資訊。this就是這個記錄的一個屬性,會在函式執行的過程中用到。

繫結規則

預設繫結

獨立函式呼叫,無法應用其它規則時的預設規則。

如果使用嚴格模式,則不能將全域性物件用於預設繫結,因此this會繫結到undefined。

隱式繫結

呼叫位置是否有上下文物件,或者說是否被某個物件擁有或者包含。如下所示,呼叫位置會使用obj上下文引用函式,因此可以說函式被呼叫時obj物件“擁有”或者“包含”它。

function foo() {
    console.log(this.a);
}

var obj = {
    a: 2,
    foo: foo
};

obj.foo(); // 2
複製程式碼

這種情況同樣適用於foo存在於obj的原型鏈上,如下所示:

function foo() {
    console.log(this.a);
}

var fo = {
    foo: foo
};

// 建立一個物件obj,使之原型指向fo
var obj = Object.create(fo);
obj.a = 2;

obj.foo(); // 2
複製程式碼

物件屬性引用鏈中只有上一層或者最後一層在呼叫位置中起作用,如下

function foo() {
    console.log(this.a);
}

var obj2 = {
    a: 42,
    foo: foo
};

var obj1 = {
    a: 2,
    obj2: obj2
};

obj1.obj2.foo(); // 42
複製程式碼

隱式丟失

一個最常見的this繫結問題就是被隱式繫結的函式會丟失繫結物件,也就是說它會應用預設繫結

思考下面的程式碼:

function foo() {
    console.log(this.a);
}

var obj = {
    a: 42,
    foo: foo
};

var bar = obj.foo;

var a = "oops";

bar(); // oops
複製程式碼

雖然bar是obj.foo的一個引用,但實際上,它引用的是foo函式本身,因此此時的bar()其實是一個不帶任何修飾的函式呼叫,因此應用了預設繫結。

一種更出乎意料的情況發生在傳入回撥函式時:

function foo() {
    console.log(this.a);
}

function doFoo(fn) {
    fn();
}

var obj = {
    a: 2,
    foo: foo
};

var a = "oops";

doFoo(obj.foo); // oops
複製程式碼

引數傳遞其實是一種隱式傳遞,因此我們傳入函式時也會被隱式賦值,所以結果和上個例子一樣。

顯示繫結

隱式繫結時,必須在一個物件內部包含一個指向函式的屬性,並通過這個屬性間接引用函式,從而把this間接繫結到這個物件上。如果我們不想在物件內部包含函式引用,而想在某個物件上強制呼叫函式,該怎麼辦呢?答案就是callapply!

這兩個方法第一個引數是一個物件,是給this準備的,接著在呼叫函式時將其繫結到this。因為你可以直接指定this的繫結物件,因此稱之為顯示繫結

呼叫callapply時,如果你傳入了一個原始值(字串、布林、數字)來當做this的繫結物件,這個原始值會轉化為它的物件形式。這通常被稱為“裝箱”。

可以重複使用的輔助函式

function foo(something) {
    return this.a + somthing;
}

// 簡單的輔助繫結函式
function bind(fn, obj) {
    return function() {
        return fn.apply(obj, arguments);
    };
}

var obj = {a: 2};

var bar = bind(foo, obj);

bar(3); // 5
複製程式碼

由於這是一種非常常用的模式,因此在Es5中提供了內建的方法Function.prototype.bind

第三方庫的許多函式,以及JavaScript語言中許多新的內建函式,都提供了一個可選的引數,通常被稱為“上下文”,其作用和bind一樣,確保你的回撥函式可以使用指定的this。

function foo(num) {
    console.log(num, this.a);
}

var obj = {
    a: 2
};

[1,2,3].forEach(foo, obj);
複製程式碼

這些函式實際上就是通過call或者apply實現了顯示繫結,這樣你可以少些一些程式碼。

new繫結

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

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

思考下面程式碼:

function foo(a) {
    this.a = a;
}

var bar = new foo(2);
console.log(bar.a); // 2
複製程式碼

使用new來呼叫foo時,會建立一個新物件,並繫結到foo()呼叫中的this上。

優先順序

  1. new繫結
  2. 顯示繫結
  3. 隱式繫結
  4. 預設繫結

箭頭函式

箭頭函式不使用this的四種標準規則,而是根據外層作用域來決定this。

function foo() {
    return (a) => {
        console.log(this.a);
    };
}

var obj1 = {a: 2};
var obj2 = {a: 3};

var bar = foo.call(obj1);
bar.call(obj2); // 2, 不是3
複製程式碼

foo()內部建立的箭頭函式會捕獲呼叫時foo()的this。由於foo()的this繫結到obj1,bar的this也會繫結到obj1,箭頭函式的繫結無法被修改。(new也不行!)

相關文章