Javascript Reference Type

2023-02-17 10:53 更新

深入的語言特性

本文所講的是一個高階主題,能幫你更好地理解一些邊緣情況。

這僅是錦上添花。許多經驗豐富的開發(fā)者不甚了了也過得不錯。如果你想了解代碼運行的本質,那就繼續(xù)讀下去吧。

一個動態(tài)執(zhí)行的方法調用可能會丟失 this。

例如:

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

user.hi(); // 正常運行

// 現在讓我們基于 name 來選擇調用 user.hi 或 user.bye
(user.name == "John" ? user.hi : user.bye)(); // Error!

在最后一行有個在 user.hi 和 user.bye 中做選擇的條件(三元)運算符。當前情形下的結果是 user.hi。

接著該方法被通過 () 立刻調用。但是并不能正常工作!

如你所見,此處調用導致了一個錯誤,因為在該調用中 "this" 的值變成了 undefined。

這樣是能工作的(對象.方法):

user.hi();

這就無法工作了(被評估的方法):

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

為什么呢?欲知緣何,且讓我們深入 obj.method() 調用運行的本質。

Reference type 解讀

仔細看的話,我們可能注意到 obj.method() 語句中的兩個操作:

  1. 首先,點 ?'.'? 取了屬性 ?obj.method? 的值。
  2. 接著 ?()? 執(zhí)行了它。

那么,this 的信息是怎么從第一部分傳遞到第二部分的呢?

如果我們將這些操作放在不同的行,this 必定是會丟失的:

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

// 把獲取方法和調用方法拆成兩行
let hi = user.hi;
hi(); // 報錯了,因為 this 的值是 undefined

這里 hi = user.hi 把函數賦值給了一個變量,接下來在最后一行它是完全獨立的,所以這里沒有 this。

為確保 user.hi() 調用正常運行,JavaScript 玩了個小把戲 —— 點 '.' 返回的不是一個函數,而是一個特殊的 Reference Type 的值。

Reference Type 是 ECMA 中的一個“規(guī)范類型”。我們不能直接使用它,但它被用在 JavaScript 語言內部。

Reference Type 的值是一個三個值的組合 (base, name, strict),其中:

  • ?base? 是對象。
  • ?name? 是屬性名。
  • ?strict? 在 ?use strict? 模式下為 true。

對屬性 user.hi 訪問的結果不是一個函數,而是一個 Reference Type 的值。對于 user.hi,在嚴格模式下是:

// Reference Type 的值
(user, "hi", true)

當 () 被在 Reference Type 上調用時,它們會接收到關于對象和對象的方法的完整信息,然后可以設置正確的 this(在此處 =user)。

Reference Type 是一個特殊的“中間人”內部類型,目的是從 . 傳遞信息給 () 調用。

任何例如賦值 hi = user.hi 等其他的操作,都會將 Reference Type 作為一個整體丟棄掉,而會取 user.hi(一個函數)的值并繼續(xù)傳遞。所以任何后續(xù)操作都“丟失”了 this。

因此,this 的值僅在函數直接被通過點符號 obj.method() 或方括號 obj['method']() 語法(此處它們作用相同)調用時才被正確傳遞。還有很多種解決這個問題的方式,例如 func.bind()。

總結

Reference Type 是語言內部的一個類型。

讀取一個屬性,例如在 obj.method() 中,. 返回的準確來說不是屬性的值,而是一個特殊的 “Reference Type” 值,其中儲存著屬性的值和它的來源對象。

這是為了隨后的方法調用 () 獲取來源對象,然后將 this 設為它。

對于所有其它操作,Reference Type 會自動變成屬性的值(在我們這個情況下是一個函數)。

這整個機制對我們是不可見的。它僅在一些微妙的情況下才重要,例如使用表達式從對象動態(tài)地獲取一個方法時。

任務


檢查語法

重要程度: 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)()

然后我們還可以看到,這樣的聯(lián)合表達式在語法上是將對象 { go: ... } 作為參數為 (user.go) 的函數。這發(fā)生在 let user 的同一行上,因此 user 對象是甚至還沒有被定義,因此出現了錯誤。

如果我們插入該分號,一切都變得正常:

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

(user.go)() // John

要注意的是,(user.go) 外邊這層括號在這沒有任何作用。通常用它們來設置操作的順序,但在這里點符號 . 總是會先執(zhí)行,所以并沒有什么影響。分號是唯一重要的。


解釋 "this" 的值

重要程度: 3

在下面的代碼中,我們試圖連續(xù)調用 obj.go() 方法 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. 它是一個常規(guī)的方法調用。
  2. 同樣,括號沒有改變執(zhí)行的順序,點符號總是先執(zhí)行。
  3. 這里我們有一個更復雜的 ?(expression)()? 調用。這個調用就像被分成了兩行(代碼)一樣:
  4. f = obj.go; // 計算函數表達式
    f();        // 調用

    這里的 f() 是作為一個沒有(設定)this 的函數執(zhí)行的。

  5. 與 ?(3)? 相類似,在括號 ?()? 的左邊也有一個表達式。

要解釋 (3) 和 (4) 得到這種結果的原因,我們需要回顧一下屬性訪問器(點符號或方括號)返回的是引用類型的值。

除了方法調用之外的任何操作(如賦值 = 或 ||),都會把它轉換為一個不包含允許設置 this 信息的普通值。


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號