2024 年 3 月 31 日

物件轉換為原始類型

當物件加入 obj1 + obj2、減去 obj1 - obj2 或使用 alert(obj) 印出時,會發生什麼事?

JavaScript 不允許你自訂運算子在物件上的運作方式。不像其他程式語言,例如 Ruby 或 C++,我們無法實作一個特殊的物件方法來處理加法(或其他運算子)。

在這些運算中,物件會自動轉換為原始類型,然後運算會在這些原始類型上進行,並產生一個原始類型值。

這是一個重要的限制:obj1 + obj2(或其他數學運算)的結果不能是另一個物件!

例如,我們無法建立代表向量或矩陣(或成就或其他任何東西)的物件,將它們相加並期待一個「加總」的物件作為結果。這種架構上的壯舉會自動「出局」。

因此,由於我們在技術上無法在此處執行太多操作,因此在實際專案中沒有物件數學運算。當它發生時(除了少數例外),那是因為編碼錯誤。

在本章中,我們將介紹物件如何轉換為基本型別以及如何自訂它。

我們有兩個目的

  1. 它將使我們能夠在編碼錯誤的情況下了解正在發生什麼事,當這種運算意外發生時。
  2. 有例外,這種運算有可能發生且看起來不錯。例如,減去或比較日期(Date 物件)。我們稍後會遇到它們。

轉換規則

在章節 型別轉換 中,我們已經看過基本型別的數字、字串和布林轉換規則。但我們為物件留了一個缺口。現在,由於我們瞭解方法和符號,因此可以填補它。

  1. 沒有轉換為布林。在布林文中,所有物件都是 true,就這麼簡單。只存在數字和字串轉換。
  2. 當我們減去物件或套用數學函數時,就會發生數字轉換。例如,Date 物件(將在章節 日期和時間 中介紹)可以相減,而 date1 - date2 的結果是兩個日期之間的時間差。
  3. 至於字串轉換,通常發生在我們使用 alert(obj) 輸出物件以及類似的情況下。

我們可以使用特殊物件方法自行實作字串和數字轉換。

現在讓我們深入技術細節,因為這是深入探討此主題的唯一方法。

提示

JavaScript 如何決定套用哪種轉換?

有三種型別轉換,發生在各種情況下。它們稱為「提示」,如 規格 中所述

"字串"

對於物件到字串的轉換,當我們對物件執行運算時,該運算預期一個字串,例如 alert

// output
alert(obj);

// using object as a property key
anotherObj[obj] = 123;
「數字」

對於物件轉換為數字,例如我們在做數學運算時

// explicit conversion
let num = Number(obj);

// maths (except binary plus)
let n = +obj; // unary plus
let delta = date1 - date2;

// less/greater comparison
let greater = user1 > user2;

大多數內建的數學函數也包含此類轉換。

「預設」

在罕見的情況下,當運算子「不確定」預期的類型時會發生。

例如,二元加號 + 可以同時用於字串(串接它們)和數字(將它們相加)。因此,如果二元加號取得物件作為引數,它會使用 「預設」 提示來轉換它。

此外,如果使用 == 將物件與字串、數字或符號進行比較,也不清楚應該進行哪種轉換,因此會使用 「預設」 提示。

// binary plus uses the "default" hint
let total = obj1 + obj2;

// obj == number uses the "default" hint
if (user == 1) { ... };

大於和小於比較運算子(例如 < >)也可以同時用於字串和數字。儘管如此,它們使用 「數字」 提示,而不是 「預設」。那是出於歷史原因。

不過,在實際應用中,事情會簡單一些。

除了 Date 物件(我們稍後會學習)之外,所有內建物件都以與 「數字」 相同的方式實作 「預設」 轉換。我們可能也應該這麼做。

儘管如此,了解所有 3 個提示仍然很重要,我們很快就會明白原因。

為了進行轉換,JavaScript 會嘗試尋找並呼叫三個物件方法

  1. 呼叫 obj[Symbol.toPrimitive](提示) - 如果存在具有符號鍵 Symbol.toPrimitive(系統符號)的方法,則使用此方法
  2. 否則,如果提示是 「字串」
    • 嘗試呼叫 obj.toString()obj.valueOf(),無論存在哪一個。
  3. 否則,如果提示是 「數字」「預設」
    • 嘗試呼叫 obj.valueOf()obj.toString(),無論存在哪一個。

Symbol.toPrimitive

讓我們從第一個方法開始。有一個名為 Symbol.toPrimitive 的內建符號,應該用於命名轉換方法,如下所示

obj[Symbol.toPrimitive] = function(hint) {
  // here goes the code to convert this object to a primitive
  // it must return a primitive value
  // hint = one of "string", "number", "default"
};

如果 Symbol.toPrimitive 方法存在,則它會用於所有提示,並且不需要更多方法。

例如,這裡的 user 物件實作了它

let user = {
  name: "John",
  money: 1000,

  [Symbol.toPrimitive](hint) {
    alert(`hint: ${hint}`);
    return hint == "string" ? `{name: "${this.name}"}` : this.money;
  }
};

// conversions demo:
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500

從程式碼中可以看到,user 會根據轉換成為自我描述的字串或金額。單一方法 user[Symbol.toPrimitive] 處理所有轉換案例。

toString/valueOf

如果沒有 Symbol.toPrimitive,JavaScript 會嘗試尋找方法 toStringvalueOf

  • 對於 "string" 提示:呼叫 toString 方法,如果它不存在或如果它傳回物件而不是原始值,則呼叫 valueOf(因此 toString 優先用於字串轉換)。
  • 對於其他提示:呼叫 valueOf,如果它不存在或如果它傳回物件而不是原始值,則呼叫 toString(因此 valueOf 優先用於數學)。

方法 toStringvalueOf 來自古代。它們不是符號(符號在很久以前不存在),而是「常規」字串命名方法。它們提供了另一種「舊式」實現轉換的方法。

這些方法必須傳回原始值。如果 toStringvalueOf 傳回物件,則會忽略它(與沒有方法相同)。

預設情況下,純粹物件有下列 toStringvalueOf 方法

  • toString 方法傳回字串 "[object Object]"
  • valueOf 方法傳回物件本身。

以下是範例

let user = {name: "John"};

alert(user); // [object Object]
alert(user.valueOf() === user); // true

因此,如果我們嘗試將物件用作字串,例如在 alert 中,那麼預設情況下我們會看到 [object Object]

預設 valueOf 在此僅出於完整性的目的而提及,以避免任何混淆。如您所見,它會傳回物件本身,因此會被忽略。不要問我為什麼,那是出於歷史原因。因此,我們可以假設它不存在。

讓我們實作這些方法以自訂轉換。

例如,這裡的 user 使用 toStringvalueOf 的組合來執行與上述相同的工作,而不是 Symbol.toPrimitive

let user = {
  name: "John",
  money: 1000,

  // for hint="string"
  toString() {
    return `{name: "${this.name}"}`;
  },

  // for hint="number" or "default"
  valueOf() {
    return this.money;
  }

};

alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500

正如我們所見,行為與使用 Symbol.toPrimitive 的前一個範例相同。

我們通常想要一個單一的「萬用」位置來處理所有原始轉換。在這種情況下,我們只能實作 toString,如下所示

let user = {
  name: "John",

  toString() {
    return this.name;
  }
};

alert(user); // toString -> John
alert(user + 500); // toString -> John500

在沒有 Symbol.toPrimitivevalueOf 的情況下,toString 將處理所有原始轉換。

轉換可以傳回任何原始類型

關於所有原始轉換方法,需要知道的重要一點是,它們不一定會傳回「提示」的原始值。

無法控制 toString 是否完全傳回字串,或 Symbol.toPrimitive 方法是否為提示 "number" 傳回數字。

唯一強制執行的項目:這些方法必須回傳基本型別,而非物件。

歷史備註

基於歷史因素,如果 toStringvalueOf 回傳物件,則不會產生錯誤,但此類值會被忽略(就像該方法不存在一樣)。這是因為在遠古時代,JavaScript 中沒有良好的「錯誤」概念。

相反地,Symbol.toPrimitive 較為嚴格,它必須回傳基本型別,否則會產生錯誤。

進一步轉換

正如我們所知,許多運算子與函式會執行型別轉換,例如乘法 * 會將運算元轉換為數字。

如果我們傳遞物件作為引數,則會有兩個計算階段

  1. 物件會轉換為基本型別(使用上述規則)。
  2. 如果進一步計算有需要,則結果的基本型別也會被轉換。

例如

let obj = {
  // toString handles all conversions in the absence of other methods
  toString() {
    return "2";
  }
};

alert(obj * 2); // 4, object converted to primitive "2", then multiplication made it a number
  1. 乘法 obj * 2 會先將物件轉換為基本型別(也就是字串 "2")。
  2. 然後 "2" * 2 會變成 2 * 2(字串會轉換為數字)。

在相同情況下,二元加法會串接字串,因為它很樂意接受字串

let obj = {
  toString() {
    return "2";
  }
};

alert(obj + 2); // "22" ("2" + 2), conversion to primitive returned a string => concatenation

摘要

物件轉基本型別的轉換會由許多內建函式與運算子自動呼叫,這些函式與運算子預期值為基本型別。

有 3 種類型(提示)

  • "string"(用於 alert 和其他需要字串的運算)
  • "number"(用於數學運算)
  • "default"(少數運算子,通常物件會以與 "number" 相同的方式實作它)

規格明確說明哪些運算子使用哪些提示。

轉換演算法為

  1. 如果方法存在,則呼叫 obj[Symbol.toPrimitive](hint)
  2. 否則,如果提示是 「字串」
    • 嘗試呼叫 obj.toString()obj.valueOf(),無論存在哪一個。
  3. 否則,如果提示是 「數字」「預設」
    • 嘗試呼叫 obj.valueOf()obj.toString(),無論存在哪一個。

所有這些方法必須回傳基本型別才能運作(如果已定義)。

在實務上,通常只要實作 obj.toString() 就足夠了,它是一個「萬用」方法,用於字串轉換,它應該回傳物件的「人類可讀」表示,用於記錄或除錯目的。

教學課程地圖

留言

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