當物件加入 obj1 + obj2
、減去 obj1 - obj2
或使用 alert(obj)
印出時,會發生什麼事?
JavaScript 不允許你自訂運算子在物件上的運作方式。不像其他程式語言,例如 Ruby 或 C++,我們無法實作一個特殊的物件方法來處理加法(或其他運算子)。
在這些運算中,物件會自動轉換為原始類型,然後運算會在這些原始類型上進行,並產生一個原始類型值。
這是一個重要的限制:obj1 + obj2
(或其他數學運算)的結果不能是另一個物件!
例如,我們無法建立代表向量或矩陣(或成就或其他任何東西)的物件,將它們相加並期待一個「加總」的物件作為結果。這種架構上的壯舉會自動「出局」。
因此,由於我們在技術上無法在此處執行太多操作,因此在實際專案中沒有物件數學運算。當它發生時(除了少數例外),那是因為編碼錯誤。
在本章中,我們將介紹物件如何轉換為基本型別以及如何自訂它。
我們有兩個目的
- 它將使我們能夠在編碼錯誤的情況下了解正在發生什麼事,當這種運算意外發生時。
- 有例外,這種運算有可能發生且看起來不錯。例如,減去或比較日期(
Date
物件)。我們稍後會遇到它們。
轉換規則
在章節 型別轉換 中,我們已經看過基本型別的數字、字串和布林轉換規則。但我們為物件留了一個缺口。現在,由於我們瞭解方法和符號,因此可以填補它。
- 沒有轉換為布林。在布林文中,所有物件都是
true
,就這麼簡單。只存在數字和字串轉換。 - 當我們減去物件或套用數學函數時,就會發生數字轉換。例如,
Date
物件(將在章節 日期和時間 中介紹)可以相減,而date1 - date2
的結果是兩個日期之間的時間差。 - 至於字串轉換,通常發生在我們使用
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 會嘗試尋找並呼叫三個物件方法
- 呼叫
obj[Symbol.toPrimitive](提示)
- 如果存在具有符號鍵Symbol.toPrimitive
(系統符號)的方法,則使用此方法 - 否則,如果提示是
「字串」
- 嘗試呼叫
obj.toString()
或obj.valueOf()
,無論存在哪一個。
- 嘗試呼叫
- 否則,如果提示是
「數字」
或「預設」
- 嘗試呼叫
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 會嘗試尋找方法 toString
和 valueOf
- 對於
"string"
提示:呼叫toString
方法,如果它不存在或如果它傳回物件而不是原始值,則呼叫valueOf
(因此toString
優先用於字串轉換)。 - 對於其他提示:呼叫
valueOf
,如果它不存在或如果它傳回物件而不是原始值,則呼叫toString
(因此valueOf
優先用於數學)。
方法 toString
和 valueOf
來自古代。它們不是符號(符號在很久以前不存在),而是「常規」字串命名方法。它們提供了另一種「舊式」實現轉換的方法。
這些方法必須傳回原始值。如果 toString
或 valueOf
傳回物件,則會忽略它(與沒有方法相同)。
預設情況下,純粹物件有下列 toString
和 valueOf
方法
toString
方法傳回字串"[object Object]"
。valueOf
方法傳回物件本身。
以下是範例
let user = {name: "John"};
alert(user); // [object Object]
alert(user.valueOf() === user); // true
因此,如果我們嘗試將物件用作字串,例如在 alert
中,那麼預設情況下我們會看到 [object Object]
。
預設 valueOf
在此僅出於完整性的目的而提及,以避免任何混淆。如您所見,它會傳回物件本身,因此會被忽略。不要問我為什麼,那是出於歷史原因。因此,我們可以假設它不存在。
讓我們實作這些方法以自訂轉換。
例如,這裡的 user
使用 toString
和 valueOf
的組合來執行與上述相同的工作,而不是 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.toPrimitive
和 valueOf
的情況下,toString
將處理所有原始轉換。
轉換可以傳回任何原始類型
關於所有原始轉換方法,需要知道的重要一點是,它們不一定會傳回「提示」的原始值。
無法控制 toString
是否完全傳回字串,或 Symbol.toPrimitive
方法是否為提示 "number"
傳回數字。
唯一強制執行的項目:這些方法必須回傳基本型別,而非物件。
基於歷史因素,如果 toString
或 valueOf
回傳物件,則不會產生錯誤,但此類值會被忽略(就像該方法不存在一樣)。這是因為在遠古時代,JavaScript 中沒有良好的「錯誤」概念。
相反地,Symbol.toPrimitive
較為嚴格,它必須回傳基本型別,否則會產生錯誤。
進一步轉換
正如我們所知,許多運算子與函式會執行型別轉換,例如乘法 *
會將運算元轉換為數字。
如果我們傳遞物件作為引數,則會有兩個計算階段
- 物件會轉換為基本型別(使用上述規則)。
- 如果進一步計算有需要,則結果的基本型別也會被轉換。
例如
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
- 乘法
obj * 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"
相同的方式實作它)
規格明確說明哪些運算子使用哪些提示。
轉換演算法為
- 如果方法存在,則呼叫
obj[Symbol.toPrimitive](hint)
, - 否則,如果提示是
「字串」
- 嘗試呼叫
obj.toString()
或obj.valueOf()
,無論存在哪一個。
- 嘗試呼叫
- 否則,如果提示是
「數字」
或「預設」
- 嘗試呼叫
obj.valueOf()
或obj.toString()
,無論存在哪一個。
- 嘗試呼叫
所有這些方法必須回傳基本型別才能運作(如果已定義)。
在實務上,通常只要實作 obj.toString()
就足夠了,它是一個「萬用」方法,用於字串轉換,它應該回傳物件的「人類可讀」表示,用於記錄或除錯目的。
留言
<code>
標籤,要插入多行,請將它們包在<pre>
標籤中,要插入超過 10 行,請使用沙盒(plnkr、jsbin、codepen…)