JS中this的繫結規則

WLv發表於2019-04-28

前言

我們明白每個函式的 this 是在呼叫 時被繫結的,完全取決於函式的呼叫位置(也就是函式的呼叫方法)。 在理解 this 的繫結過程之前,首先要理解呼叫位置:呼叫位置就是函式在程式碼中被呼叫的 位置(而不是宣告的位置)。 最重要的是要分析呼叫棧(就是為了到達當前執行位置所呼叫的所有函式)。我們關心的 呼叫位置就在當前正在執行的函式的前一個呼叫中。

呼叫位置

下面我們來看看到底什麼是呼叫棧和呼叫位置:

function baz() {
// 當前呼叫棧是:baz
// 因此,當前呼叫位置是全域性作用域
console.log( "baz" );
bar(); // <-- bar 的呼叫位置
}
function bar() {
// 當前呼叫棧是 baz -> bar
// 因此,當前呼叫位置在 baz 中
console.log( "bar" );
foo(); // <-- foo 的呼叫位置
}
function foo() {
// 當前呼叫棧是 baz -> bar -> foo
// 因此,當前呼叫位置在 bar 中
console.log( "foo" );
}
baz(); // <-- baz 的呼叫位置
複製程式碼

繫結規則 (注:皆不考慮嚴格模式)

1. 預設繫結

首先要介紹的是最常用的函式呼叫型別:獨立函式呼叫。可以把這條規則看作是無法應用 其他規則時的預設規則。

思考一下下面的程式碼:

function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
複製程式碼

我們可以看到當呼叫 foo() 時,this.a 被解析成了全域性變數 a。為什麼?因為在本 例中,函式呼叫時應用了 this 的預設繫結,因此 this 指向全域性物件

2. 隱式繫結

另一條需要考慮的規則是呼叫位置是否有上下文物件,或者說是否被某個物件擁有或者包 含,不過這種說法可能會造成一些誤導。

思考下面的程式碼:

function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
複製程式碼

當函式引 用(注:foo是引用函式,既指向function foo(){···}這個函式,而foo()才是呼叫函式)有上下文物件時,隱式繫結規則會把函式呼叫中的 this 繫結到這個上下文物件。因為調 用 foo() 時 this 被繫結到 obj,因此 this.a 和 obj.a 是一樣的。

物件屬性引用鏈中只有最頂層或者說最後一層會影響呼叫位置。舉例來說:

function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
複製程式碼

隱式丟失 (如果到這有點暈可以先跳過這個)

這裡我們還要談到隱式丟失這個問題。一個最常見的 this 繫結問題就是被隱式繫結的函式會丟失繫結物件,也就是說它會應用預設繫結,從而把 this 繫結到全域性物件。

思考下面的程式碼:

function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函式別名!
var a = "oops, global"; // a 是全域性物件的屬性
bar(); // "oops, global"
複製程式碼

雖然 bar 是 obj.foo 的一個引用,但是實際上,它引用的是 foo 函式本身(注:var bar = obj.foo;相當於給這個函式又取了一個名字,所以bar();=foo();),因此此時的 bar() 其實是一個不帶任何修飾的函式呼叫,因此應用了預設繫結。

3. 顯式繫結

JavaScript 提供的絕大多數函式以及你自 己建立的所有函式都可以使用 call(..) 和 apply(..) 方法。 這兩個方法是如何工作的呢?它們的第一個引數是一個物件,它們會把這個物件繫結到 this,接著在呼叫函式時指定這個 this。因為你可以直接指定 this 的繫結物件,因此我 們稱之為顯式繫結。

思考下面的程式碼:

function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
複製程式碼

通過 foo.call(..),我們可以在呼叫 foo 時強制把它的 this 繫結到 obj 上。 (注:從 this 繫結的角度來說,call(..) 和 apply(..)還有bind(...) 是一樣的,它們的區別體現 在其他的引數上,但是現在我們不用考慮這些。)

4. new繫結

在 JavaScript 中,建構函式只是一些 使用 new 操作符時被呼叫的函式。它們並不會屬於某個類,也不會例項化一個類。實際上, 它們甚至都不能說是一種特殊的函式型別,它們只是被 new 操作符呼叫的普通函式而已

使用 new 來呼叫函式,或者說發生建構函式呼叫時,會自動執行下面的操作。(這裡我們可以不用管第二步)

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

思考下面的程式碼:

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

使用 new 來呼叫 foo(..) 時,我們會構造一個新物件並把它繫結到 foo(..) 呼叫中的 this 上。new 是最後一種可以影響函式呼叫時 this 繫結行為的方法,我們稱之為 new 繫結。

最後:優先順序

現在我們已經瞭解了函式呼叫中 this 繫結的四條規則,你需要做的就是找到函式的呼叫位 置並判斷應當應用哪條規則。但是,如果某個呼叫位置可以應用多條規則該怎麼辦?為了 解決這個問題就必須給這些規則設定優先順序。

這裡我就不做討論了,直接給出結論:

new繫結 > 顯示繫結 > 隱式繫結 > 預設繫結

有興趣的同學可以自行去驗證。

參考書籍:《你不知道的JavaScript(上卷)》

相關文章