物件 — 原始值轉換
當物件相加 obj1 + obj2,相減 obj1 - obj2,或者使用 alert(obj) 列印時會發生什么?
在這種情況下,物件會被自動轉換為原始值,然后執行操作,
在 型別轉換 一章中,我們已經看到了數值,字串和布爾轉換的規則,但是我們沒有講物件的轉換規則,現在我們已經掌握了方法(method)和 symbol 的相關知識,可以開始學習物件原始值轉換了,
所有的物件在布爾背景關系(context)中均為
true,所以對于物件,不存在 to-boolean 轉換,只有字串和數值轉換,數值轉換發生在物件相級訓應用數學函式時,例如,
Date物件(將在 日期和時間[1] 一章中介紹)可以相減,date1 - date2的結果是兩個日期之間的差值,至于字串轉換 —— 通常發生在我們像
alert(obj)這樣輸出一個物件和類似的背景關系中,
ToPrimitive
我們可以使用特殊的物件方法,對字串和數值轉換進行微調,
下面是三個型別轉換的變體,被稱為 "hint",在 規范[2] 中有詳細介紹(譯注:當一個物件被用在需要原始值的背景關系中時,例如,在 alert 或數學運算中,物件會被轉換為原始值):
"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" 轉換,我們也可以這樣做,
沒有 "boolean" hint請注意 —— 只有三種 hint,就這么簡單,
沒有 "boolean" hint(在布爾背景關系中所有物件都是
true)或其他任何東西,如果我們將"default"和"number"視為相同,就像大多數內建函式一樣,那么就只有兩種轉換了,
為了進行轉換,JavaScript 嘗試查找并呼叫三個物件方法:
呼叫
obj[Symbol.toPrimitive](hint "Symbol.toPrimitive")—— 帶有 symbol 鍵Symbol.toPrimitive(系統 symbol)的方法,如果這個方法存在的話,否則,如果 hint 是
"string"—— 嘗試obj.toString()和obj.valueOf(),無論哪個存在,否則,如果 hint 是
"number"或"default"—— 嘗試obj.valueOf()和obj.toString(),無論哪個存在,
Symbol.toPrimitive
我們從第一個方法開始,有一個名為 Symbol.toPrimitive 的內建 symbol,它被用來給轉換方法命名,像這樣:
obj[Symbol.toPrimitive] = function(hint) {
// 回傳一個原始值
// hint = "string"、"number" 和 "default" 中的一個
}
例如,這里 user 物件實作了它:
let user = {
name: "John",
money: 1000,
[Symbol.toPrimitive](hint "Symbol.toPrimitive") {
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] 處理了所有的轉換情況,
toString/valueOf
方法 toString 和 valueOf 來自上古時代,它們不是 symbol(那時候還沒有 symbol 這個概念),而是“常規的”字串命名的方法,它們提供了一種可選的“老派”的實作轉換的方法,
如果沒有 Symbol.toPrimitive,那么 JavaScript 將嘗試找到它們,并且按照下面的順序進行嘗試:
對于 "string" hint,
toString -> valueOf,其他情況,
valueOf -> toString,
這些方法必須回傳一個原始值,如果 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 方法是否為 hint "number" 回傳數字,
唯一強制性的事情是:這些方法必須回傳一個原始值,而不是物件,
歷史原因
由于歷史原因,如果
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"(少數運算子)
規范明確描述了哪個運算子使用哪個 hint,很少有運算子“不知道期望什么”并使用 "default" hint,通常對于內建物件,"default" hint 的處理方式與 "number" 相同,因此在實踐中,最后兩個 hint 常常合并在一起,
轉換演算法是:
呼叫
obj[Symbol.toPrimitive](hint "Symbol.toPrimitive")如果這個方法存在,否則,如果 hint 是
"string"
嘗試
obj.toString()和obj.valueOf(),無論哪個存在,
否則,如果 hint 是
"number"或者"default"嘗試
obj.valueOf()和obj.toString(),無論哪個存在,
在實踐中,為了便于進行日志記錄或除錯,對于所有能夠回傳一種“可讀性好”的物件的表達形式的轉換,只實作以
obj.toString()作為全能轉換的方法就夠了,
現代 JavaScript 教程:開源的現代 JavaScript 從入門到進階的優質教程,React 官方檔案推薦,與 MDN 并列的 JavaScript 學習教程[3],
在線免費閱讀:https://zh.javascript.info
參考資料
[1]
日期和時間: https://zh.javascript.info/date
[2]規范: https://tc39.github.io/ecma262/#sec-toprimitive
[3]React 官方檔案推薦,與 MDN 并列的 JavaScript 學習教程: https://zh-hans.reactjs.org/docs/getting-started.html#javascript-resources
看完三件事
如果你覺得本文對你有幫助,我想請你幫個忙:
轉發本文,點贊或者點個「在看」,是對我最大的認可和支持;
關注公眾號「技術漫談」,訂閱更多精彩內容,獲取更多學習資料;
公眾號后臺回復「加群」,加入演算法和技術交流群,與更多讀者交流,
長按上方二維碼關注公眾號「技術漫談」,訂閱更多精彩內容,
你“在看”我嗎?
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/217923.html
標籤:其他
