當對象相加 obj1 + obj2
,相減 obj1 - obj2
,或者使用 alert(obj)
打印時會發生什麽?
JavaScript 不允許自定義運算符對對象的處理方式。與其他壹些編程語言(Ruby,C++)不同,我們無法實現特殊的對象處理方法來處理加法(或其他運算)。
在此類運算的情況下,對象會被自動轉換爲原始值,然後對這些原始值進行運算,並得到運算結果(也是壹個原始值)。
這是壹個重要的限制:因爲 obj1 + obj2
(或者其他數學運算)的結果不能是另壹個對象!
例如,我們無法使用對象來表示向量或矩陣,把它們相加並期望得到壹個“總和”向量作爲結果。這樣的想法是行不通的。
因此,由于我們從技術上無法實現此類運算,所以在實際項目中不存在對對象的數學運算。如果妳發現有,除了極少數例外,通常是寫錯了。
本文將介紹對象是如何轉換爲原始值的,以及如何對其進行自定義。
我們有兩個目的:
讓我們在遇到類似的對對象進行數學運算的編程錯誤時,能夠更加理解到底發生了什麽。
也有例外,這些操作也可以是可行的。例如日期相減或比較(Date
對象)。我們稍後會遇到它們。
在 類型轉換 壹章中,我們已經看到了數字、字符串和布爾轉換的規則。但是我們沒有講對象的轉換規則。現在我們已經掌握了方法(method)和 symbol 的相關知識,可以開始學習對象原始值轉換了。
沒有轉換爲布爾值。所有的對象在布爾上下文(context)中均爲 true
,就這麽簡單。只有字符串和數字轉換。
數字轉換發生在對象相減或應用數學函數時。例如,Date
對象(將在 日期和時間 壹章中介紹)可以相減,date1 - date2
的結果是兩個日期之間的差值。
至于字符串轉換 —— 通常發生在我們像 alert(obj)
這樣輸出壹個對象和類似的上下文中。
我們可以使用特殊的對象方法,自己實現字符串和數字的轉換。
現在讓我們壹起探究技術細節,因爲這是深入討論該主題的唯壹方式。
JavaScript 是如何決定應用哪種轉換的?
類型轉換在各種情況下有三種變體。它們被稱爲 “hint”,在 規範 所述:
"string"
對象到字符串的轉換,當我們對期望壹個字符串的對象執行操作時,如 “alert”:
// 輸出 alert(obj); // 將對象作爲屬性鍵 anotherObj[obj] = 123;
"number"
對象到數字的轉換,例如當我們進行數學運算時:
// 顯式轉換 let num = Number(obj); // 數學運算(除了二元加法) let n = +obj; // 壹元加法 let delta = date1 - date2; // 小于/大于的比較 let greater = user1 > user2;
大多數內建的數學函數也包括這種轉換。
"default"
在少數情況下發生,當運算符“不確定”期望值的類型時。
例如,二元加法 +
可用于字符串(連接),也可以用于數字(相加)。因此,當二元加法得到對象類型的參數時,它將依據 "default"
hint 來對其進行轉換。
此外,如果對象被用于與字符串、數字或 symbol 進行 ==
比較,這時到底應該進行哪種轉換也不是很明確,因此使用 "default"
hint。
// 二元加法使用默認 hint let total = obj1 + obj2; // obj == number 使用默認 hint if (user == 1) { ... };
像 <
和 >
這樣的小于/大于比較運算符,也可以同時用于字符串和數字。不過,它們使用 “number” hint,而不是 “default”。這是曆史原因。
上面這些規則看起來比較複雜,但在實踐中其實挺簡單的。
除了壹種情況(Date
對象,我們稍後會講到)之外,所有內建對象都以和 "number"
相同的方式實現 "default"
轉換。我們也可以這樣做。
盡管如此,了解上述的 3 個 hint 還是很重要的,很快妳就會明白爲什麽這樣說。
爲了進行轉換,JavaScript 嘗試查找並調用三個對象方法:
調用 obj[Symbol.toPrimitive](hint)
—— 帶有 symbol 鍵 Symbol.toPrimitive
(系統 symbol)的方法,如果這個方法存在的話,
否則,如果 hint 是 "string"
—— 嘗試調用 obj.toString()
或 obj.valueOf()
,無論哪個存在。
否則,如果 hint 是 "number"
或 "default"
—— 嘗試調用 obj.valueOf()
或 obj.toString()
,無論哪個存在。
我們從第壹個方法開始。有壹個名爲 Symbol.toPrimitive
的內建 symbol,它被用來給轉換方法命名,像這樣:
obj[Symbol.toPrimitive] = function(hint) { // 這裏是將此對象轉換爲原始值的代碼 // 它必須返回壹個原始值 // hint = "string"、"number" 或 "default" 中的壹個 }
如果 Symbol.toPrimitive
方法存在,則它會被用于所有 hint,無需更多其他方法。
例如,這裏 user
對象實現了它:
let user = { name: "John", money: 1000, [Symbol.toPrimitive](hint) { alert(`hint: ${hint}`); return hint == "string" ? `{name: "${this.name}"}` : this.money; } }; // 轉換演示: alert(user); // hint: string -> {name: "John"} alert(+user); // hint: number -> 1000 alert(user + 500); // hint: default -> 1500
從代碼中我們可以看到,根據轉換的不同,user
變成壹個自描述字符串或者壹個金額。user[Symbol.toPrimitive]
方法處理了所有的轉換情況。
如果沒有 Symbol.toPrimitive
,那麽 JavaScript 將嘗試尋找 toString
和 valueOf
方法:
對于 "string"
hint:調用 toString
方法,如果它不存在,則調用 valueOf
方法(因此,對于字符串轉換,優先調用 toString
)。
對于其他 hint:調用 valueOf
方法,如果它不存在,則調用 toString
方法(因此,對于數學運算,優先調用 valueOf
方法)。
toString
和 valueOf
方法很早己有了。它們不是 symbol(那時候還沒有 symbol 這個概念),而是“常規的”字符串命名的方法。它們提供了壹種可選的“老派”的實現轉換的方法。
這些方法必須返回壹個原始值。如果 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
執行和前面提到的那個 user
壹樣的操作,使用 toString
和 valueOf
的組合(而不是 Symbol.toPrimitive
):
let user = { name: "John", money: 1000, // 對于 hint="string" toString() { return `{name: "${this.name}"}`; }, // 對于 hint="number" 或 "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
將處理所有原始轉換。
關于所有原始轉換方法,有壹個重要的點需要知道,就是它們不壹定會返回 “hint” 的原始值。
沒有限制 toString()
是否返回字符串,或 Symbol.toPrimitive
方法是否爲 "number"
hint 返回數字。
唯壹強制性的事情是:這些方法必須返回壹個原始值,而不是對象。
曆史原因
由于曆史原因,如果 toString
或 valueOf
返回壹個對象,則不會出現 error,但是這種值會被忽略(就像這種方法根本不存在)。這是因爲在 JavaScript 語言發展初期,沒有很好的 “error” 的概念。
相反,Symbol.toPrimitive
更嚴格,它 必須 返回壹個原始值,否則就會出現 error。
我們已經知道,許多運算符和函數執行類型轉換,例如乘法 *
將操作數轉換爲數字。
如果我們將對象作爲參數傳遞,則會出現兩個運算階段:
對象被轉換爲原始值(通過前面我們描述的規則)。
如果還需要進壹步計算,則生成的原始值會被進壹步轉換。
例如:
let obj = { // toString 在沒有其他方法的情況下處理所有轉換 toString() { return "2"; } }; alert(obj * 2); // 4,對象被轉換爲原始值字符串 "2",之後它被乘法轉換爲數字 2。
乘法 obj * 2
首先將對象轉換爲原始值(字符串 “2”)。
之後 "2" * 2
變爲 2 * 2
(字符串被轉換爲數字)。
二元加法在同樣的情況下會將其連接成字符串,因爲它更願意接受字符串:
let obj = { toString() { return "2"; } }; alert(obj + 2); // 22("2" + 2)被轉換爲原始值字符串 => 級聯
對象到原始值的轉換,是由許多期望以原始值作爲值的內建函數和運算符自動調用的。
這裏有三種類型(hint):
"string"
(對于 alert
和其他需要字符串的操作)
"number"
(對于數學運算)
"default"
(少數運算符,通常對象以和 "number"
相同的方式實現 "default"
轉換)
規範明確描述了哪個運算符使用哪個 hint。
轉換算法是:
調用 obj[Symbol.toPrimitive](hint)
如果這個方法存在,
否則,如果 hint 是 "string"
嘗試調用 obj.toString()
或 obj.valueOf()
,無論哪個存在。
否則,如果 hint 是 "number"
或者 "default"
嘗試調用 obj.valueOf()
或 obj.toString()
,無論哪個存在。
所有這些方法都必須返回壹個原始值才能工作(如果已定義)。
在實際使用中,通常只實現 obj.toString()
作爲字符串轉換的“全能”方法就足夠了,該方法應該返回對象的“人類可讀”表示,用于日志記錄或調試。