理解 JavaScript 中的 this

4Ark發表於2019-01-16

前言

理解this是我們要深入理解 JavaScript 中必不可少的一個步驟,同時只有理解了 this,你才能更加清晰地寫出與自己預期一致的 JavaScript 程式碼。

本文是這系列的第三篇,往期文章:

  1. 理解 JavaScript 中的作用域
  2. 理解 JavaScript 中的閉包

什麼是 this

消除誤解

在解釋什麼是this之前,需要先糾正大部分人對this的誤解,常見的誤解有:

  1. 指向函式自身。
  2. 指向它所在的作用域。

關於為何會誤解的原因這裡不多講,這裡只給出結論,有興趣可以自行查詢資料。

this 在任何情況下都不指向函式的詞法作用域。你不能使用 this 來引用一個詞法作用域內部的東西。

this 到底是什麼

排除了一些錯誤理解之後,我們來看看 this到底是一種什麼樣的機制。

this是在執行時(runtime)進行繫結的,而不是在編寫時繫結的,它的上下文(物件)取決於函式呼叫時的各種條件。this的繫結和函式宣告的位置沒有任何關係,只取決於函式的呼叫方式

當一個函式被呼叫時,會建立一個活動記錄(有時候也稱為執行上下文)。這個記錄會包含函式在哪裡被呼叫(呼叫棧)、函式的呼叫方法、傳入的引數等資訊。this就是記錄的其中一個屬性,會在函式執行的過程中用到。(PS:所以this並不等價於執行上下文)

this 全面解析

前面 我們排除了一些對於 this的錯誤理解並且明白了每個函式的this是在呼叫時被繫結的,完全取決於函式的呼叫位置。

呼叫位置

通常來說,尋找呼叫位置就是尋找“函式被呼叫的位置“,其中最重要的是要分析呼叫棧(就是為了到達當前執行位置所呼叫的所有函式)。我們關心的呼叫位置就在當前正在執行的函式的前一個呼叫中。

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

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

你可以把呼叫棧想象成一個函式呼叫鏈, 就像我們在前面程式碼段的註釋中所寫的一樣。但是這種方法非常麻煩並且容易出錯。 另一個檢視呼叫棧的方法是使用瀏覽器的除錯工具。 絕大多數現代桌面瀏覽器都內建了開發者工具,其中包含 JavaScript 偵錯程式。

繫結規則

在找到呼叫位置後,則需要判定程式碼屬於下面四種繫結規則中的哪一種,然後才能對this進行繫結。 注意: this繫結的是上下文物件,並不是函式自身也不是函式的詞法作用域

預設繫結

這是最常見的函式呼叫型別:獨立函式呼叫

對函式直接使用而不帶任何修飾的函式引用進行呼叫,簡單點一個函式直接是func()這樣呼叫,不同於通過物件屬性呼叫例如obj.func(),也沒有通過new關鍵字new Function(),也沒有通過applycallbind強制改變this指向。

當被用作獨立函式呼叫時(不論這個函式在哪被呼叫,不管全域性還是其他函式內),this預設指向到Window。(注意:在嚴格模式下this不再預設指向全域性,而是undefined)。

示例程式碼:

function foo(){
    console.log(this.name);
}
var name = "window";
foo(); // window
複製程式碼

隱式繫結

函式被某個物件擁有或者包含,也就是函式被作為物件的屬性所引用,例如obj.func(),此時this會繫結到該物件上,這就是隱式繫結。

示例程式碼:

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

隱式丟失

大部分的this繫結問題就是被“隱式繫結”的函式會丟失繫結物件,也就是說它會應用“預設繫結”,從而把this繫結到Windowundefined上,這取決於是否是嚴格模式。

最常見的情況就是把物件方法作為回撥函式進行傳遞時:

var obj = {
    name : "obj",
    foo : function(){
        console.log(this.name);
    }
}
var name = "window";
setTimeout(obj.foo,1000); // 一秒後輸出 window
複製程式碼

顯式繫結

我們可以通過applycallbind方法來顯示地修改this的指向。

關於這三個方法的定義(它們第一個引數都是接受this的繫結物件):

  1. apply:呼叫函式,第二個引數傳入一個引數陣列。
  2. call:呼叫函式,其餘引數正常傳遞。
  3. bind:返回一個已經繫結this的函式,其餘引數正常傳遞。

比如我們可以使用bind方法解決上一節“隱式丟失”中的例子:

var obj = {
    name : "obj",
    foo : function(){
        console.log(this.name);
    }
}
var name = "window";
setTimeout(obj.foo.bind(obj),1000); // 一秒後輸出 obj
複製程式碼

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繫結=顯示繫結>隱式繫結>預設繫結

判斷this: 現在我們可以根據優先順序來判斷函式在某個呼叫位置應用的是哪條規則。可以按照下面的順序來進行判斷:

  1. 使用new繫結,this繫結的是新建立的物件。

    var bar = new foo();
    複製程式碼
  2. 通過call之類的顯式繫結,this繫結的是指定的物件。

    var bar = foo.call(obj2);
    複製程式碼
  3. 在某個上下文物件中呼叫(隱式繫結),this 繫結的是那個上下文物件。

    var bar = obj1.foo();
    複製程式碼
  4. 如果都不是的話,使用預設繫結。this繫結到Windowundefined上,這取決於是否是嚴格模式。

    var bar = foo();
    複製程式碼

    對於正常的函式呼叫來說,理解了這些知識你就可以明白 this 的繫結原理了。

this詞法

ES6 中介紹了一種無法使用上面四條規則的特殊函式型別:箭頭函式

箭頭函式不使用 this 的四種標準規則,而是根據外層(函式或者全域性)作用域來決定 this。(而傳統的this與函式作用域沒有任何關係,它只與呼叫位置的上下文物件有關)。

重要:

  • 箭頭函式最常用於回撥函式中,例如事件處理器或者定時器.
  • 箭頭函式可以像bind 一樣確保函式的this被繫結到指定物件
  • 箭頭函式用更常見的詞法作用域取代了傳統的this機制。

示例程式碼:

var obj = {
    name : "obj",
    foo : function(){
        setTimeout(()=>{
            console.log(console.log(this.name)); // obj
        },1000);
    }
}
obj.foo();
複製程式碼

這在 ES6 之前是這樣解決的:

var obj = {
    name : "obj",
    foo : function(){
        var self = this;
        setTimeout(function(){
            console.log(console.log(self.name)); // obj
        },1000);
    }
}
obj.foo();
複製程式碼

總結

總之如果要判斷一個執行中函式的this繫結,就需要找到這個函式的直接呼叫位置。找到之後就可以順序應用下面這四條規則來判斷this的繫結物件。

  1. 由new呼叫?繫結到新建立的物件。
  2. 由call或者apply(或者bind)呼叫?繫結到指定的物件。
  3. 由上下文物件呼叫?繫結到那個上下文物件。
  4. 預設:在嚴格模式下繫結到undefined,否則繫結到全域性物件。

ES6 中的箭頭函式並不會使用四條標準的繫結規則,而是根據當前的詞法作用域來決定 this,具體來說,箭頭函式會繼承外層函式呼叫的 this繫結(無論 this繫結到什麼)。這其實和 ES6 之前程式碼中的 self = this 機制一樣。

注:此文為原創文章,如需轉載,請註明出處。

相關文章