天天看點

從規範的角度解析對象 — 原始值轉換

對象到原始值的轉換,是由許多期望以原始值作為值的内建函數和運算符自動調用的。這裡有三種類型(hint):"string","number" 和 "default"。

對象 — 原始值轉換

當對象相加

obj1 + obj2

,相減

obj1 - obj2

,或者使用

alert(obj)

列印時會發生什麼?

在這種情況下,對象會被自動轉換為原始值,然後執行操作。

在 類型轉換 一章中,我們已經看到了數值,字元串和布爾轉換的規則。但是我們沒有講對象的轉換規則。現在我們已經掌握了方法(method)和 symbol 的相關知識,可以開始學習對象原始值轉換了。

  1. 所有的對象在布爾上下文(context)中均為

    true

    。是以對于對象,不存在 to-boolean 轉換,隻有字元串和數值轉換。
  2. 數值轉換發生在對象相減或應用數學函數時。例如,

    Date

    對象(将在 日期和時間 一章中介紹)可以相減,

    date1 - date2

    的結果是兩個日期之間的內插補點。
  3. 至于字元串轉換 —— 通常發生在我們像

    alert(obj)

    這樣輸出一個對象和類似的上下文中。

ToPrimitive

我們可以使用特殊的對象方法,對字元串和數值轉換進行微調。

下面是三個類型轉換的變體,被稱為 "hint",在 規範 中有詳細介紹(譯注:當一個對象被用在需要原始值的上下文中時,例如,在

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 嘗試查找并調用三個對象方法:

  1. 調用

    obj[Symbol.toPrimitive](hint)

    —— 帶有 symbol 鍵

    Symbol.toPrimitive

    (系統 symbol)的方法,如果這個方法存在的話,
  2. 否則,如果 hint 是

    "string"

    —— 嘗試

    obj.toString()

    obj.valueOf()

    ,無論哪個存在。
  3. 否則,如果 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) {
    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。

進一步的轉換

我們已經知道,許多運算符和函數執行類型轉換,例如乘法

*

将操作數轉換為數字。

如果我們将對象作為參數傳遞,則會出現兩個階段:

  1. 對象被轉換為原始值(通過前面我們描述的規則)。
  2. 如果生成的原始值的類型不正确,則繼續進行轉換。

例如:

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

alert(obj * 2); // 4,對象被轉換為原始值字元串 "2",之後它被乘法轉換為數字 2。
           
  1. 乘法

    obj * 2

    首先将對象轉換為原始值(字元串 "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 常常合并在一起。

轉換算法是:

  1. 調用

    obj[Symbol.toPrimitive](hint)

    如果這個方法存在,
  2. 否則,如果 hint 是

    "string"

    • 嘗試

      obj.toString()

      obj.valueOf()

      ,無論哪個存在。
  3. 否則,如果 hint 是

    "number"

    或者

    "default"

    • 嘗試

      obj.valueOf()

      obj.toString()

      ,無論哪個存在。

在實踐中,為了便于進行日志記錄或調試,對于所有能夠傳回一種“可讀性好”的對象的表達形式的轉換,隻實作以

obj.toString()

作為全能轉換的方法就夠了。

現代 JavaScript 教程:開源的現代 JavaScript 從入門到進階的優質教程。React 官方文檔推薦,與 MDN 并列的 JavaScript 學習教程。

線上免費閱讀:https://zh.javascript.info

微信掃描下方二維碼,關注公衆号「技術漫談」,訂閱更多精彩内容。