Javascript 對(duì)象 —— 原始值轉(zhuǎn)換

2023-02-17 10:44 更新

當(dāng)對(duì)象相加 ?obj1 + obj2?,相減 ?obj1 - obj2?,或者使用 ?alert(obj)? 打印時(shí)會(huì)發(fā)生什么?

JavaScript 不允許自定義運(yùn)算符對(duì)對(duì)象的處理方式。與其他一些編程語(yǔ)言(Ruby,C++)不同,我們無(wú)法實(shí)現(xiàn)特殊的對(duì)象處理方法來(lái)處理加法(或其他運(yùn)算)。 

在此類運(yùn)算的情況下,對(duì)象會(huì)被自動(dòng)轉(zhuǎn)換為原始值,然后對(duì)這些原始值進(jìn)行運(yùn)算,并得到運(yùn)算結(jié)果(也是一個(gè)原始值)。

這是一個(gè)重要的限制:因?yàn)?nbsp;obj1 + obj2(或者其他數(shù)學(xué)運(yùn)算)的結(jié)果不能是另一個(gè)對(duì)象!

例如,我們無(wú)法使用對(duì)象來(lái)表示向量或矩陣(或成就或其他),把它們相加并期望得到一個(gè)“總和”向量作為結(jié)果。這樣的想法是行不通的。

因此,由于我們從技術(shù)上無(wú)法實(shí)現(xiàn)此類運(yùn)算,所以在實(shí)際項(xiàng)目中不存在對(duì)對(duì)象的數(shù)學(xué)運(yùn)算。如果你發(fā)現(xiàn)有,除了極少數(shù)例外,通常是寫錯(cuò)了。

本文將介紹對(duì)象是如何轉(zhuǎn)換為原始值的,以及如何對(duì)其進(jìn)行自定義。

我們有兩個(gè)目的:

  1. 讓我們?cè)谟龅筋愃频膶?duì)對(duì)象進(jìn)行數(shù)學(xué)運(yùn)算的編程錯(cuò)誤時(shí),能夠更加理解到底發(fā)生了什么。
  2. 也有例外,這些操作也可以是可行的。例如日期相減或比較(?Date ?對(duì)象)。我們稍后會(huì)遇到它們。

轉(zhuǎn)換規(guī)則

在 類型轉(zhuǎn)換 一章中,我們已經(jīng)看到了數(shù)字、字符串和布爾轉(zhuǎn)換的規(guī)則。但是我們沒(méi)有講對(duì)象的轉(zhuǎn)換規(guī)則?,F(xiàn)在我們已經(jīng)掌握了方法(method)和 symbol 的相關(guān)知識(shí),可以開始學(xué)習(xí)對(duì)象原始值轉(zhuǎn)換了。

  1. 沒(méi)有轉(zhuǎn)換為布爾值。所有的對(duì)象在布爾上下文(context)中均為 ?true?,就這么簡(jiǎn)單。只有字符串和數(shù)字轉(zhuǎn)換。
  2. 數(shù)字轉(zhuǎn)換發(fā)生在對(duì)象相減或應(yīng)用數(shù)學(xué)函數(shù)時(shí)。例如,?Date ?對(duì)象(將在 日期和時(shí)間 一章中介紹)可以相減,?date1 - date2? 的結(jié)果是兩個(gè)日期之間的差值。
  3. 至于字符串轉(zhuǎn)換 —— 通常發(fā)生在我們像 ?alert(obj)? 這樣輸出一個(gè)對(duì)象和類似的上下文中。

我們可以使用特殊的對(duì)象方法,自己實(shí)現(xiàn)字符串和數(shù)字的轉(zhuǎn)換。

現(xiàn)在讓我們一起探究技術(shù)細(xì)節(jié),因?yàn)檫@是深入討論該主題的唯一方式。

hint

JavaScript 是如何決定應(yīng)用哪種轉(zhuǎn)換的?

類型轉(zhuǎn)換在各種情況下有三種變體。它們被稱為 “hint”,在 規(guī)范 所述:

?"string"
?

對(duì)象到字符串的轉(zhuǎn)換,當(dāng)我們對(duì)期望一個(gè)字符串的對(duì)象執(zhí)行操作時(shí),如 “alert”:

// 輸出
alert(obj);

// 將對(duì)象作為屬性鍵
anotherObj[obj] = 123;

?"number"
?

對(duì)象到數(shù)字的轉(zhuǎn)換,例如當(dāng)我們進(jìn)行數(shù)學(xué)運(yùn)算時(shí):

// 顯式轉(zhuǎn)換
let num = Number(obj);

// 數(shù)學(xué)運(yùn)算(除了二元加法)
let n = +obj; // 一元加法
let delta = date1 - date2;

// 小于/大于的比較
let greater = user1 > user2;

大多數(shù)內(nèi)建的數(shù)學(xué)函數(shù)也包括這種轉(zhuǎn)換。

?"default"
?

在少數(shù)情況下發(fā)生,當(dāng)運(yùn)算符“不確定”期望值的類型時(shí)。

例如,二元加法 + 可用于字符串(連接),也可以用于數(shù)字(相加)。因此,當(dāng)二元加法得到對(duì)象類型的參數(shù)時(shí),它將依據(jù) "default" hint 來(lái)對(duì)其進(jìn)行轉(zhuǎn)換。

此外,如果對(duì)象被用于與字符串、數(shù)字或 symbol 進(jìn)行 == 比較,這時(shí)到底應(yīng)該進(jìn)行哪種轉(zhuǎn)換也不是很明確,因此使用 "default" hint。

// 二元加法使用默認(rèn) hint
let total = obj1 + obj2;

// obj == number 使用默認(rèn) hint
if (user == 1) { ... };

像 < 和 > 這樣的小于/大于比較運(yùn)算符,也可以同時(shí)用于字符串和數(shù)字。不過(guò),它們使用 “number” hint,而不是 “default”。這是歷史原因。

上面這些規(guī)則看起來(lái)比較復(fù)雜,但在實(shí)踐中其實(shí)挺簡(jiǎn)單的。

除了一種情況(Date 對(duì)象,我們稍后會(huì)講到)之外,所有內(nèi)建對(duì)象都以和 "number" 相同的方式實(shí)現(xiàn) "default" 轉(zhuǎn)換。我們也可以這樣做。

盡管如此,了解上述的 3 個(gè) hint 還是很重要的,很快你就會(huì)明白為什么這樣說(shuō)。

為了進(jìn)行轉(zhuǎn)換,JavaScript 嘗試查找并調(diào)用三個(gè)對(duì)象方法:

  1. 調(diào)用 ?obj[Symbol.toPrimitive](hint)? —— 帶有 symbol 鍵 ?Symbol.toPrimitive?(系統(tǒng) symbol)的方法,如果這個(gè)方法存在的話,
  2. 否則,如果 hint 是 ?"string"? —— 嘗試調(diào)用 ?obj.toString()? 或 ?obj.valueOf()?,無(wú)論哪個(gè)存在。
  3. 否則,如果 hint 是 ?"number"? 或 ?"default"? —— 嘗試調(diào)用 ?obj.valueOf()? 或 ?obj.toString()?,無(wú)論哪個(gè)存在。

Symbol.toPrimitive

我們從第一個(gè)方法開始。有一個(gè)名為 Symbol.toPrimitive 的內(nèi)建 symbol,它被用來(lái)給轉(zhuǎn)換方法命名,像這樣:

obj[Symbol.toPrimitive] = function(hint) {
  // 這里是將此對(duì)象轉(zhuǎn)換為原始值的代碼
  // 它必須返回一個(gè)原始值
  // hint = "string"、"number" 或 "default" 中的一個(gè)
}

如果 Symbol.toPrimitive 方法存在,則它會(huì)被用于所有 hint,無(wú)需更多其他方法。

例如,這里 user 對(duì)象實(shí)現(xiàn)了它:

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

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

// 轉(zhuǎn)換演示:
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500

從代碼中我們可以看到,根據(jù)轉(zhuǎn)換的不同,user 變成一個(gè)自描述字符串或者一個(gè)金額。user[Symbol.toPrimitive] 方法處理了所有的轉(zhuǎn)換情況。

toString/valueOf

如果沒(méi)有 Symbol.toPrimitive,那么 JavaScript 將嘗試尋找 toString 和 valueOf 方法:

  • 對(duì)于 ?"string"? hint:調(diào)用 ?toString ?方法,如果它不存在,則調(diào)用 ?valueOf ?方法(因此,對(duì)于字符串轉(zhuǎn)換,優(yōu)先調(diào)用 ?toString?)。
  • 對(duì)于其他 hint:調(diào)用 ?valueOf ?方法,如果它不存在,則調(diào)用 ?toString ?方法(因此,對(duì)于數(shù)學(xué)運(yùn)算,優(yōu)先調(diào)用 ?valueOf ?方法)。

toString 和 valueOf 方法很早己有了。它們不是 symbol(那時(shí)候還沒(méi)有 symbol 這個(gè)概念),而是“常規(guī)的”字符串命名的方法。它們提供了一種可選的“老派”的實(shí)現(xiàn)轉(zhuǎn)換的方法。

這些方法必須返回一個(gè)原始值。如果 toString 或 valueOf 返回了一個(gè)對(duì)象,那么返回值會(huì)被忽略(和這里沒(méi)有方法的時(shí)候相同)。

默認(rèn)情況下,普通對(duì)象具有 toString 和 valueOf 方法:

  • ?toString ?方法返回一個(gè)字符串 ?"[object Object]"?。
  • ?valueOf ?方法返回對(duì)象自身。

下面是一個(gè)示例:

let user = {name: "John"};

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

所以,如果我們嘗試將一個(gè)對(duì)象當(dāng)做字符串來(lái)使用,例如在 alert 中,那么在默認(rèn)情況下我們會(huì)看到 [object Object]。

這里提到的默認(rèn)的 valueOf 只是為了完整起見,以避免混淆。正如你看到的,它返回對(duì)象本身,因此被忽略。別問(wèn)我為什么,這是歷史原因。所以我們可以假設(shè)它根本就不存在。

讓我們實(shí)現(xiàn)一下這些方法來(lái)自定義轉(zhuǎn)換。

例如,這里的 user 執(zhí)行和前面提到的那個(gè) user 一樣的操作,使用 toString 和 valueOf 的組合(而不是 Symbol.toPrimitive):

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

  // 對(duì)于 hint="string"
  toString() {
    return `{name: "${this.name}"}`;
  },

  // 對(duì)于 hint="number" 或 "default"
  valueOf() {
    return this.money;
  }

};

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

我們可以看到,執(zhí)行的動(dòng)作和前面使用 Symbol.toPrimitive 的那個(gè)例子相同。

通常我們希望有一個(gè)“全能”的地方來(lái)處理所有原始轉(zhuǎn)換。在這種情況下,我們可以只實(shí)現(xiàn) toString,就像這樣:

let user = {
  name: "John",

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

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

如果沒(méi)有 Symbol.toPrimitive 和 valueOf,toString 將處理所有原始轉(zhuǎn)換。

轉(zhuǎn)換可以返回任何原始類型

關(guān)于所有原始轉(zhuǎn)換方法,有一個(gè)重要的點(diǎn)需要知道,就是它們不一定會(huì)返回 “hint” 的原始值。

沒(méi)有限制 toString() 是否返回字符串,或 Symbol.toPrimitive 方法是否為 "number" hint 返回?cái)?shù)字。

唯一強(qiáng)制性的事情是:這些方法必須返回一個(gè)原始值,而不是對(duì)象。

歷史原因

由于歷史原因,如果 toString 或 valueOf 返回一個(gè)對(duì)象,則不會(huì)出現(xiàn) error,但是這種值會(huì)被忽略(就像這種方法根本不存在)。這是因?yàn)樵?JavaScript 語(yǔ)言發(fā)展初期,沒(méi)有很好的 “error” 的概念。

相反,Symbol.toPrimitive 更嚴(yán)格,它 必須 返回一個(gè)原始值,否則就會(huì)出現(xiàn) error。

進(jìn)一步的轉(zhuǎn)換

我們已經(jīng)知道,許多運(yùn)算符和函數(shù)執(zhí)行類型轉(zhuǎn)換,例如乘法 * 將操作數(shù)轉(zhuǎn)換為數(shù)字。

如果我們將對(duì)象作為參數(shù)傳遞,則會(huì)出現(xiàn)兩個(gè)運(yùn)算階段:

  1. 對(duì)象被轉(zhuǎn)換為原始值(通過(guò)前面我們描述的規(guī)則)。
  2. 如果還需要進(jìn)一步計(jì)算,則生成的原始值會(huì)被進(jìn)一步轉(zhuǎn)換。

例如:

let obj = {
  // toString 在沒(méi)有其他方法的情況下處理所有轉(zhuǎn)換
  toString() {
    return "2";
  }
};

alert(obj * 2); // 4,對(duì)象被轉(zhuǎn)換為原始值字符串 "2",之后它被乘法轉(zhuǎn)換為數(shù)字 2。
  1. 乘法 ?obj * 2? 首先將對(duì)象轉(zhuǎn)換為原始值(字符串 “2”)。
  2. 之后 ?"2" * 2? 變?yōu)?nbsp;?2 * 2?(字符串被轉(zhuǎn)換為數(shù)字)。

二元加法在同樣的情況下會(huì)將其連接成字符串,因?yàn)樗敢饨邮茏址?

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

alert(obj + 2); // 22("2" + 2)被轉(zhuǎn)換為原始值字符串 => 級(jí)聯(lián)

總結(jié)

對(duì)象到原始值的轉(zhuǎn)換,是由許多期望以原始值作為值的內(nèi)建函數(shù)和運(yùn)算符自動(dòng)調(diào)用的。

這里有三種類型(hint):

  • ?"string"?(對(duì)于 ?alert ?和其他需要字符串的操作)
  • ?"number"?(對(duì)于數(shù)學(xué)運(yùn)算)
  • ?"default"?(少數(shù)運(yùn)算符,通常對(duì)象以和 ?"number"? 相同的方式實(shí)現(xiàn) ?"default"? 轉(zhuǎn)換)

規(guī)范明確描述了哪個(gè)運(yùn)算符使用哪個(gè) hint。

轉(zhuǎn)換算法是:

  1. 調(diào)用 ?obj[Symbol.toPrimitive](hint)? 如果這個(gè)方法存在,
  2. 否則,如果 hint 是 ?"string"?
    • 嘗試調(diào)用 ?obj.toString()? 或 ?obj.valueOf()?,無(wú)論哪個(gè)存在。
  3. 否則,如果 hint 是 ?"number"? 或者 ?"default"?
    • 嘗試調(diào)用 ?obj.valueOf()? 或 ?obj.toString()?,無(wú)論哪個(gè)存在。

所有這些方法都必須返回一個(gè)原始值才能工作(如果已定義)。

在實(shí)際使用中,通常只實(shí)現(xiàn) obj.toString() 作為字符串轉(zhuǎn)換的“全能”方法就足夠了,該方法應(yīng)該返回對(duì)象的“人類可讀”表示,用于日志記錄或調(diào)試。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)