2022 年 9 月 21 日

參考類型

深入探討語言功能

本文涵蓋進階主題,以更深入了解某些邊緣案例。

這並不重要。許多經驗豐富的開發人員在不知道的情況下也能過得很好。如果您想知道底層運作方式,請繼續閱讀。

動態評估的方法呼叫可能會遺失 this

例如

let user = {
  name: "John",
  hi() { alert(this.name); },
  bye() { alert("Bye"); }
};

user.hi(); // works

// now let's call user.hi or user.bye depending on the name
(user.name == "John" ? user.hi : user.bye)(); // Error!

在最後一行有一個條件運算子,用於選擇 user.hiuser.bye。在此情況下,結果為 user.hi

然後立即使用括號 () 呼叫方法。但它無法正常運作!

如您所見,呼叫會導致錯誤,因為呼叫內部 "this" 的值變為 undefined

這是有用的(物件點方法)

user.hi();

這沒用(評估方法)

(user.name == "John" ? user.hi : user.bye)(); // Error!

為什麼?如果我們想了解為什麼會發生這種情況,讓我們深入探討一下 obj.method() 呼叫是如何運作的。

參考類型說明

仔細觀察,我們可能會注意到 obj.method() 陳述式中有兩個運算。

  1. 首先,點號 '.' 會擷取屬性 obj.method
  2. 然後括號 () 會執行它。

那麼,關於 this 的資訊是如何從第一個部分傳遞到第二個部分的?

如果我們將這些運算放在不同的行中,那麼 this 肯定會遺失

let user = {
  name: "John",
  hi() { alert(this.name); }
};

// split getting and calling the method in two lines
let hi = user.hi;
hi(); // Error, because this is undefined

這裡 hi = user.hi 會將函式放入變數中,然後在最後一行中,它會完全獨立,因此沒有 this

為了讓 user.hi() 呼叫運作,JavaScript 使用了一個技巧,點號 '.' 傳回的不是函式,而是特殊 參考類型 的值。

參考類型是一種「規格類型」。我們無法明確使用它,但語言內部會使用它。

參考類型的值是一個三值組合 (base, name, strict),其中

  • base 是物件。
  • name 是屬性名稱。
  • 如果 use strict 生效,則 strict 為 true。

屬性存取 user.hi 的結果不是函式,而是參考類型的值。對於嚴格模式中的 user.hi,它是

// Reference Type value
(user, "hi", true)

當在參考類型上呼叫括號 () 時,它們會接收有關物件及其方法的完整資訊,並可以設定正確的 this(在本例中為 user)。

參考類型是一種特殊的「中介」內部類型,其目的是將資訊從點號 . 傳遞到呼叫括號 ()

任何其他運算,例如指定 hi = user.hi,都會捨棄整個參考類型,取得 user.hi(函式)的值並傳遞它。因此,任何後續運算都會「遺失」this

因此,結果上,只有在使用點號 obj.method() 或方括號 obj['method']() 語法(它們在此處執行相同的動作)直接呼叫函式時,才會正確傳遞 this 的值。有各種方法可以解決此問題,例如 func.bind()

摘要

參考類型是語言的內部類型。

讀取屬性,例如 obj.method() 中的點號 .,傳回的不是屬性值,而是儲存屬性值和從中取得屬性值的物件的特殊「參考類型」值。

這是為了讓後續的方法呼叫 () 取得物件並將 this 設定為它。

對於所有其他運算,參考類型會自動變成屬性值(在本例中為函式)。

整個機制對我們來說是隱藏的。它只在微妙的情況下才重要,例如使用表達式從物件動態取得方法時。

任務

重要性:2

這段程式碼的結果是什麼?

let user = {
  name: "John",
  go: function() { alert(this.name) }
}

(user.go)()

附註:這裡有一個陷阱 :)

錯誤!

試試看

let user = {
  name: "John",
  go: function() { alert(this.name) }
}

(user.go)() // error!

大多數瀏覽器中的錯誤訊息並未提供太多關於錯誤原因的線索。

錯誤發生是因為在 user = {...} 之後缺少分號。

JavaScript 沒有在括號 (user.go)() 之前自動插入分號,因此它會將程式碼讀取為

let user = { go:... }(user.go)()

然後我們也可以看到,這樣的聯合表達式在語法上是將物件 { go: ... } 作為函式呼叫,並以 (user.go) 作為參數。而且這也發生在與 let user 相同的行中,因此 user 物件甚至尚未定義,因此會產生錯誤。

如果我們插入分號,一切就沒問題了

let user = {
  name: "John",
  go: function() { alert(this.name) }
};

(user.go)() // John

請注意,(user.go) 周圍的括號在此處沒有作用。它們通常會設定運算順序,但這裡的點 . 仍然會先運算,因此沒有效果。只有分號這件事才有關係。

重要性:3

在以下程式碼中,我們打算連續呼叫 obj.go() 方法 4 次。

但是呼叫 (1)(2) 的方式與 (3)(4) 不同。為什麼?

let obj, method;

obj = {
  go: function() { alert(this); }
};

obj.go();               // (1) [object Object]

(obj.go)();             // (2) [object Object]

(method = obj.go)();    // (3) undefined

(obj.go || obj.stop)(); // (4) undefined

以下是說明。

  1. 這是一個常規的物件方法呼叫。

  2. 相同,括號不會改變這裡的運算順序,點運算子 . 仍然會先運算。

  3. 這裡我們有一個更複雜的呼叫 (expression)()。呼叫的運作方式就像它被分成兩行

    f = obj.go; // calculate the expression
    f();        // call what we have

    這裡 f() 被執行為一個函式,沒有 this

  4. (3) 類似,在括號 () 的左側,我們有一個表達式。

要說明 (3)(4) 的行為,我們需要回想一下屬性存取器(點或方括號)會傳回一個參考型別的值。

除了方法呼叫(例如賦值 =||)之外,對它的任何運算都會將它轉換為一個普通的值,而這個值不包含允許設定 this 的資訊。

教學地圖

留言

在留言前請先閱讀這段內容…
  • 如果你有建議要如何改進 - 請 提交 GitHub 問題 或提交拉取請求,而不是留言。
  • 如果你無法理解文章中的某個部分 - 請詳細說明。
  • 要插入幾行程式碼,請使用 <code> 標籤,要插入多行程式碼,請將它們包在 <pre> 標籤中,要插入超過 10 行程式碼,請使用沙盒 (plnkrjsbincodepen…)