<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                # 第四章:強制轉換 現在我們更全面地了解了 JavaScript 的類型和值,我們將注意力轉向一個極具爭議的話題:強制轉換。 正如我們在第一章中提到的,關于強制轉換到底是一個有用的特性,還是一個語言設計上的缺陷(或介于兩者之間!),早就開始就爭論不休了。如果你讀過關于 JS 的其他書籍,你就會知道流行在世面上那種淹沒一切的?*聲音*:強制轉換是魔法,是邪惡的,令人困惑的,而且就是徹頭徹尾的壞主意。 本著這個系列叢書的總體精神,我認為你應當直面你不理解的東西并設法更全面地?*搞懂它*。而不是因為大家都這樣做,或是你曾經被一些怪東西咬到就逃避強制轉換。 我們的目標是全面地探索強制轉換的優點和缺點(是的,它們?*有*?優點!),這樣你就能在程序中對它是否合適做出明智的決定。 ## 轉換值 將一個值從一個類型明確地轉換到另一個類型通常稱為“類型轉換(type casting)”,當這個操作隱含地完成時稱為“強制轉換(coercion)”(根據一個值如何被使用的規則來強制它變換類型)。 注意:?這可能不明顯,但是 JavaScript 強制轉換總是得到基本標量值的一種,比如?`string`、`number`、或?`boolean`。沒有強制轉換可以得到像?`object`?和?`function`?這樣的復雜值。第三章講解了“封箱”,它將一個基本類型標量值包裝在它們相應的?`object`?中,但在準確的意義上這不是真正的強制轉換。 另一種區別這些術語的常見方法是:“類型轉換(type casting/conversion)”發生在靜態類型語言的編譯時,而“類型強制轉換(type coercion)”是動態類型語言的運行時轉換。 然而,在 JavaScript 中,大多數人將所有這些類型的轉換都稱為?*強制轉換(coercion)*,所以我偏好的區別方式是使用“隱含強制轉換(implicit coercion)”與“明確強制轉換(explicit coercion)”。 其中的區別應當是很明顯的:在觀察代碼時如果一個類型轉換明顯是有意為之的,那么它就是“明確強制轉換”,而如果這個類型轉換是做為其他操作的不那么明顯的副作用發生的,那么它就是“隱含強制轉換”。 例如,考慮這兩種強制轉換的方式: ```source-js var a = 42; var b = a + ""; // 隱含強制轉換 var c = String( a ); // 明確強制轉換 ``` 對于?`b`?來說,強制轉換是隱含地發生的,因為如果與?`+`?操作符組合的操作數之一是一個?`string`?值(`""`),這將使?`+`?操作成為一個?`string`?連接(將兩個字符串加在一起),而?`string`?連接的?*一個(隱藏的)副作用*?將?`a`?中的值?`42`?強制轉換為它的?`string`?等價物:`"42"`。 相比之下,`String(..)`?函數使一切相當明顯,它明確地取得?`a`?中的值,并把它強制轉換為一個?`string`?表現形式。 兩種方式都能達到相同的效果:從?`42`?變成?`"42"`。但它們?*如何*?達到這種效果,才是關于 JavaScript 強制轉換的熱烈爭論的核心。 注意:?技術上講,這里有一些在語法形式區別之上的,行為上的微妙區別。我們將在本章稍后,“隱含:Strings Numbers”一節中仔細講解。 “明確地”、“隱含地”、或“明顯地”和“隱藏的副作用”這些術語,是?*相對的*。 如果你確切地知道?`a + ""`?是在做什么,并且你有意地這么做來強制轉換一個?`string`,你可能感覺這個操作已經足夠“明確”了。相反,如果你從沒見過?`String(..)`?函數被用于?`string`?強制轉換,那么對你來說它的行為可能看起來太過隱蔽而讓你感到“隱含”。 但我們是基于一個?*大眾的,充分了解,但不是專家或 JS 規范愛好者的*?開發者的觀點來討論“明確”與“隱含”的。無論你的程度如何,或是沒有在這個范疇內準確地找到自己,你都需要根據我們在這里的觀察方式,相應地調整你的角度。 記住:我們自己寫代碼而也只有我們自己會讀它,通常是很少見的。即便你是一個精通 JS 里里外外的專家,也要考慮一個經驗沒那么豐富的隊友在讀你的代碼時感受如何。對于他們和對于你來說,“明確”或“隱含”的意義相同嗎? ## 抽象值操作 在我們可以探究?*明確*?與?*隱含*?強制轉換之前,我們需要學習一些基本規則,是它們控制著值如何?*變成*?一個?`string`、`number`、或?`boolean`?的。ES5 語言規范的第九部分用值的變形規則定義了幾種“抽象操作”(“僅供內部使用的操作”的高大上說法)。我們將特別關注于:`ToString`、`ToNumber`、和?`ToBoolean`,并稍稍關注一下?`ToPrimitive`。 ### `ToString` 當任何一個非?`string`?值被強制轉換為一個?`string`?表現形式時,這個轉換的過程是由語言規范的 9.8 部分的?`ToString`?抽象操作處理的。 內建的基本類型值擁有自然的字符串化形式:`null`?變為?`"null"`,`undefined`?變為?`"undefined"`,`true`?變為?`"true"`。`number`?一般會以你期望的自然方式表達,但正如我們在第二章中討論的,非常小或非常大的?`number`?將會以指數形式表達: ```source-js // `1.07`乘以`1000`,7次 var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000; // 7次乘以3位 => 21位 a.toString(); // "1.07e21" ``` 對于普通的對象,除非你指定你自己的,默認的?`toString()`(可以在?`Object.prototype.toString()`?找到)將返回?*內部?`[[Class]]`*(見第三章),例如?`"[object Object]"`。 但正如早先所展示的,如果一個對象上擁有它自己的?`toString()`?方法,而你又以一種類似?`string`?的方式使用這個對象,那么它的?`toString()`?將會被自動調用,而且這個調用的?`string`?結果將被使用。 注意:?技術上講,一個對象被強制轉換為一個?`string`?要通過?`ToPrimitive`?抽象操作(ES5 語言規范,9.1 部分),但是那其中的微妙細節將會在本章稍后的?`ToNumber`?部分中講解,所以我們在這里先跳過它。 數組擁有一個覆蓋版本的默認?`toString()`,將數組字符串化為它所有的值(每個都字符串化)的(字符串)連接,并用?`","`分割每個值。 ```source-js var a = [1,2,3]; a.toString(); // "1,2,3" ``` 重申一次,`toString()`?可以明確地被調用,也可以通過在一個需要?`string`?的上下文環境中使用一個非?`string`?來自動地被調用。 #### JSON 字符串化 另一種看起來與?`ToString`?密切相關的操作是,使用?`JSON.stringify(..)`?工具將一個值序列化為一個 JSON 兼容的?`string`值。 重要的是要注意,這種字符串化與強制轉換并不完全是同一種東西。但是因為它與上面講的?`ToString`?規則有關聯,我們將在這里稍微轉移一下話題,來講解 JSON 字符串化行為。 對于最簡單的值,JSON 字符串化行為基本上和?`toString()`?轉換是相同的,除了序列化的結果?*總是一個?`string`*: ```source-js JSON.stringify( 42 ); // "42" JSON.stringify( "42" ); // ""42"" (一個包含雙引號的字符串) JSON.stringify( null ); // "null" JSON.stringify( true ); // "true" ``` 任何?*JSON 安全*?的值都可以被?`JSON.stringify(..)`?字符串化。但是什么是?*JSON 安全的*?任何可以用 JSON 表現形式合法表達的值。 考慮 JSON?不?安全的值可能更容易一些。一些例子是:`undefined`、`function`、(ES6+)`symbol`、和帶有循環引用的?`object`(一個對象結構中的屬性互相引用而造成了一個永不終結的循環)。對于標準的 JSON 結構來說這些都是非法的值,主要是因為它們不能移植到消費 JSON 值的其他語言中。 `JSON.stringify(..)`?工具在遇到?`undefined`、`function`、和?`symbol`?時將會自動地忽略它們。如果在一個?`array`?中遇到這樣的值,它會被替換為?`null`(這樣數組的位置信息就不會改變)。如果在一個?`object`?的屬性中遇到這樣的值,這個屬性會被簡單地剔除掉。 考慮下面的代碼: ```source-js JSON.stringify( undefined ); // undefined JSON.stringify( function(){} ); // undefined JSON.stringify( [1,undefined,function(){},4] ); // "[1,null,null,4]" JSON.stringify( { a:2, b:function(){} } ); // "{"a":2}" ``` 但如果你試著?`JSON.stringify(..)`?一個帶有循環引用的?`object`,就會拋出一個錯誤。 JSON 字符串化有一個特殊行為,如果一個?`object`?值定義了一個?`toJSON()`?方法,這個方法將會被首先調用,以取得用于序列化的值。 如果你打算 JSON 字符串化一個可能含有非法 JSON 值的對象,或者如果這個對象中正好有不適于序列化的值,那么你就應當為它定義一個?`toJSON()`?方法,返回這個?`object`?的一個?*JSON 安全*?版本。 例如: ```source-js var o = { }; var a = { b: 42, c: o, d: function(){} }; // 在 `a` 內部制造一個循環引用 o.e = a; // 這會因循環引用而拋出一個錯誤 // JSON.stringify( a ); // 自定義一個 JSON 值序列化 a.toJSON = function() { // 序列化僅包含屬性 `b` return { b: this.b }; }; JSON.stringify( a ); // "{"b":42}" ``` 一個很常見的誤解是,`toJSON()`?應當返回一個 JSON 字符串化的表現形式。這可能是不正確的,除非你事實上想要字符串化?`string`?本身(通常不會!)。`toJSON()`?應當返回合適的實際普通值(無論什么類型),而?`JSON.stringify(..)`?自己會處理字符串化。 換句話說,`toJSON()`?應當被翻譯為:“變為一個適用于字符串化的 JSON 安全的值”,而不是像許多開發者錯誤認為的那樣,“變為一個 JSON 字符串”。 考慮下面的代碼: ```source-js var a = { val: [1,2,3], // 可能正確! toJSON: function(){ return this.val.slice( 1 ); } }; var b = { val: [1,2,3], // 可能不正確! toJSON: function(){ return "[" + this.val.slice( 1 ).join() + "]"; } }; JSON.stringify( a ); // "[2,3]" JSON.stringify( b ); // ""[2,3]"" ``` 在第二個調用中,我們字符串化了返回的?`string`?而不是?`array`?本身,這可能不是我們想要做的。 既然我們說到了?`JSON.stringify(..)`,那么就讓我們來討論一些不那么廣為人知,但是仍然很有用的功能吧。 `JSON.stringify(..)`?的第二個參數值是可選的,它稱為?*替換器(replacer)*。這個參數值既可以是一個?`array`?也可以是一個?`function`。與?`toJSON()`?為序列化準備一個值的方式類似,它提供一種過濾機制,指出一個?`object`?的哪一個屬性應該或不應該被包含在序列化形式中,來自定義這個?`object`?的遞歸序列化行為。 如果?*替換器*?是一個?`array`,那么它應當是一個?`string`?的?`array`,它的每一個元素指定了允許被包含在這個?`object`?的序列化形式中的屬性名稱。如果一個屬性不存在于這個列表中,那么它就會被跳過。 如果?*替換器*?是一個?`function`,那么它會為?`object`?本身而被調用一次,并且為這個?`object`?中的每個屬性都被調用一次,而且每次都被傳入兩個參數值,*key*?和?*value*。要在序列化中跳過一個?*key*,可以返回?`undefined`。否則,就返回被提供的?*value*。 ```source-js var a = { b: 42, c: "42", d: [1,2,3] }; JSON.stringify( a, ["b","c"] ); // "{"b":42,"c":"42"}" JSON.stringify( a, function(k,v){ if (k !== "c") return v; } ); // "{"b":42,"d":[1,2,3]}" ``` 注意:?在?`function`?*替換器*?的情況下,第一次調用時 key 參數?`k`?是?`undefined`(而對象?`a`?本身會被傳入)。`if`?語句會?過濾掉?名稱為?`c`?的屬性。字符串化是遞歸的,所以數組?`[1,2,3]`?會將它的每一個值(`1`、`2`、和?`3`)都作為?`v`?傳遞給?*替換器*,并將索引值(`0`、`1`、和?`2`)作為?`k`。 `JSON.stringify(..)`?還可以接收第三個可選參數值,稱為?*填充符(space)*,在對人類友好的輸出中它被用做縮進。*填充符*?可以是一個正整數,用來指示每一級縮進中應當使用多少個空格字符。或者,*填充符*?可以是一個?`string`,這時每一級縮進將會使用它的前十個字符。 ```source-js var a = { b: 42, c: "42", d: [1,2,3] }; JSON.stringify( a, null, 3 ); // "{ // "b": 42, // "c": "42", // "d": [ // 1, // 2, // 3 // ] // }" JSON.stringify( a, null, "-----" ); // "{ // -----"b": 42, // -----"c": "42", // -----"d": [ // ----------1, // ----------2, // ----------3 // -----] // }" ``` 記住,`JSON.stringify(..)`?并不直接是一種強制轉換的形式。但是,我們在這里討論它,是由于兩個與?`ToString`?強制轉換有關聯的行為: 1. `string`、`number`、`boolean`、和?`null`?值在 JSON 字符串化時,與它們通過?`ToString`?抽象操作的規則強制轉換為?`string`?值的方式基本上是相同的。 2. 如果傳遞一個?`object`?值給?`JSON.stringify(..)`,而這個?`object`?上擁有一個?`toJSON()`?方法,那么在字符串化之前,`toJSON()`?就會被自動調用來將這個值(某種意義上)“強制轉換”為?*JSON 安全*?的。 ### `ToNumber` 如果任何非?`number`?值,以一種要求它是?`number`?的方式被使用,比如數學操作,就會發生 ES5 語言規范在 9.3 部分定義的?`ToNumber`?抽象操作。 例如,`true`?變為?`1`?而?`false`?變為?`0`。`undefined`?變為?`NaN`,而(奇怪的是)`null`?變為?`0`。 對于一個?`string`?值來說,`ToNumber`?工作起來很大程度上與數字字面量的規則/語法很相似(見第三章)。如果它失敗了,結果將是?`NaN`(而不是?`number`?字面量中會出現的語法錯誤)。一個不同之處的例子是,在這個操作中?`0`?前綴的八進制數不會被作為八進制數來處理(而僅作為普通的十進制小數),雖然這樣的八進制數作為?`number`?字面量是合法的。 注意:?`number`?字面量文法與用于?`string`?值的?`ToNumber`?間的區別極其微妙,在這里就不進一步講解了。更多的信息可以參考 ES 語言規范的 9.3.1 部分。 對象(以及數組)將會首先被轉換為它們的基本類型值的等價物,而后這個結果值(如果它還不是一個?`number`?基本類型)會根據剛才提到的?`ToNumber`?規則被強制轉換為一個?`number`。 為了轉換為基本類型值的等價物,`ToPrimitive`?抽象操作(ES5 語言規范,9.1 部分)將會查詢這個值(使用內部的?`DefaultValue`?操作 —— ES5 語言規范,8.12.8 部分),看它有沒有?`valueOf()`?方法。如果?`valueOf()`?可用并且它返回一個基本類型值,那么?*這個*?值就將用于強制轉換。如果不是這樣,但?`toString()`?可用,那么就由它來提供用于強制轉換的值。 如果這兩種操作都沒提供一個基本類型值,就會拋出一個?`TypeError`。 在 ES5 中,你可以創建這樣一個不可強制轉換的對象 —— 沒有?`valueOf()`?和?`toString()`?—— 如果它的?`[[Prototype]]`?的值為?`null`,這通常是通過?`Object.create(null)`?來創建的。關于?`[[Prototype]]`?的詳細信息參見本系列的?*this 與對象原型*。 注意:?我們會在本章稍后講解如何強制轉換至?`number`,但對于下面的代碼段,想象?`Number(..)`?函數就是那樣做的。 考慮如下代碼: ```source-js var a = { valueOf: function(){ return "42"; } }; var b = { toString: function(){ return "42"; } }; var c = [4,2]; c.toString = function(){ return this.join( "" ); // "42" }; Number( a ); // 42 Number( b ); // 42 Number( c ); // 42 Number( "" ); // 0 Number( [] ); // 0 Number( [ "abc" ] ); // NaN ``` ### `ToBoolean` 下面,讓我們聊一聊在 JS 中?`boolean`?如何動作。世面上關于這個話題有?許多的困惑和誤解,所以集中注意力! 首先而且最重要的是,JS 實際上擁有?`true`?和?`false`?關鍵字,而且它們的行為正如你所期望的?`boolean`?值一樣。一個常見的誤解是,值?`1`?和?`0`?與?`true`/`false`?是相同的。雖然這可能在其他語言中是成立的,但在 JS 中?`number`?就是?`number`,而?`boolean`?就是?`boolean`。你可以將?`1`?強制轉換為?`true`(或反之),或將?`0`?強制轉換為?`false`(或反之)。但它們不是相同的。 #### Falsy 值 但這還不是故事的結尾。我們需要討論一下,除了這兩個?`boolean`?值以外,當你把其他值強制轉換為它們的?`boolean`?等價物時如何動作。 所有的 JavaScript 值都可以被劃分進兩個類別: 1. 如果被強制轉換為?`boolean`,將成為?`false`?的值 2. 其它的一切值(很明顯將變為?`true`) 我不是在出洋相。JS 語言規范給那些在強制轉換為?`boolean`?值時將會變為?`false`?的值定義了一個明確的,小范圍的列表。 我們如何才能知道這個列表中的值是什么?在 ES5 語言規范中,9.2 部分定義了一個?`ToBoolean`?抽象操作,它講述了對所有可能的值而言,當你試著強制轉換它們為 boolean 時究竟會發生什么。 從這個表格中,我們得到了下面所謂的“falsy”值列表: * `undefined` * `null` * `false` * `+0`,?`-0`, and?`NaN` * `""` 就是這些。如果一個值在這個列表中,它就是一個“falsy”值,而且當你在它上面進行?`boolean`?強制轉換時它會轉換為?`false`。 通過邏輯上的推論,如果一個值?*不*?在這個列表中,那么它一定在?*另一個列表*?中,也就是我們稱為“truthy”值的列表。但是 JS 沒有真正定義一個“truthy”列表。它給出了一些例子,比如它說所有的對象都是 truthy,但是語言規范大致上暗示著:任何沒有明確地存在于 falsy 列表中的東西,都是 truthy。 #### Falsy 對象 等一下,這一節的標題聽起來簡直是矛盾的。我?*剛剛才說過*?語言規范將所有對象稱為 truthy,對吧?應該沒有“falsy 對象”這樣的東西。 這會是什么意思呢? 它可能誘使你認為它意味著一個包裝了 falsy 值(比如?`""`、`0`?或?`false`)的對象包裝器(見第三章)。但別掉到這個?*陷阱*中。 注意:?這個可能是一個語言規范的微妙笑話。 考慮下面的代碼: ```source-js var a = new Boolean( false ); var b = new Number( 0 ); var c = new String( "" ); ``` 我們知道這三個值都是包裝了明顯是 falsy 值的對象(見第三章)。但這些對象是作為?`true`?還是作為?`false`?動作呢?這很容易回答: ```source-js var d = Boolean( a && b && c ); d; // true ``` 所以,三個都作為?`true`?動作,這是唯一能使?`d`?得到?`true`?的方法。 提示:?注意包在?`a && b && c`?表達式外面的?`Boolean( .. )`?—— 你可能想知道為什么它在這兒。我們會在本章稍后回到這個話題,所以先做個心理準備。為了先睹為快,你可以自己試試如果沒有?`Boolean( .. )`?調用而只有?`d = a && b && c`?時?`d`?是什么。 那么,如果“falsy 對象”?不是包裝著 falsy 值的對象,它們是什么鬼東西? 刁鉆的地方在于,它們可以出現在你的 JS 程序中,但它們實際上不是 JavaScript 本身的一部分。 什么!? 有些特定的情況,在普通的 JS 語義之上,瀏覽器已經創建了它們自己的某種?*外來*?值的行為,也就是這種“falsy 對象”的想法。 一個“falsy 對象”看起來和動起來都像一個普通對象(屬性,等等)的值,但是當你強制轉換它為一個?`boolean`?時,它會變為一個?`false`?值。 為什么!? 最著名的例子是?`document.all`:一個?*由 DOM*(不是 JS 引擎本身) 給你的 JS 程序提供的類數組(對象),它向你的 JS 程序暴露你頁面上的元素。它?*曾經*?像一個普通對象那樣動作 —— 是一個 truthy。但不再是了。 `document.all`?本身從來就不是“標準的”,而且從很早以前就被廢棄/拋棄了。 “那他們就不能刪掉它嗎?” 對不起,想得不錯。但愿它們能。但是世面上有太多的遺產 JS 代碼庫依賴于它。 那么,為什么使它像 falsy 一樣動作?因為從?`document.all`?到?`boolean`?的強制轉換(比如在?`if`?語句中)幾乎總是用來檢測老的,非標準的 IE。 IE 從很早以前就開始順應規范了,而且在許多情況下它在推動 web 向前發展的作用和其他瀏覽器一樣多,甚至更多。但是所有那些老舊的?`if (document.all) { /* it's IE */ }`?代碼依然留在世面上,而且大多數可能永遠都不會消失。所有這些遺產代碼依然假設它們運行在那些給 IE 用戶帶來差勁兒的瀏覽體驗的,幾十年前的老 IE 上, 所以,我們不能完全移除?`document.all`,但是 IE 不再想讓?`if (document.all) { .. }`?代碼繼續工作了,這樣現代 IE 的用戶就能得到新的,符合標準的代碼邏輯。 “我們應當怎么做?” “我知道了!讓我們黑進 JS 的類型系統并假裝?`document.all`?是 falsy!” 呃。這很爛。這是一個大多數 JS 開發者們都不理解的瘋狂的坑。但是其它的替代方案(對上面兩敗俱傷的問題什么都不做)還要爛得?*多那么一點點*。 所以……這就是我們得到的:由瀏覽器給 JavaScript 添加的瘋狂、非標準的“falsy 對象”。耶! #### Truthy 值 回到 truthy 列表。到底什么是 truthy 值?記住:如果一個值不在 falsy 列表中,它就是 truthy。 考慮下面代碼: ```source-js var a = "false"; var b = "0"; var c = "''"; var d = Boolean( a && b && c ); d; ``` 你期望這里的?`d`?是什么值?它要么是?`true`?要么是?`false`。 它是?`true`。為什么?因為盡管這些`string`值的內容看起來是falsy值,但是`string`值本身都是truthy,而這是因為在falsy列表中`""`是唯一的`string`值。 那么這些呢? ```source-js var a = []; // 空數組 -- truthy 還是 falsy? var b = {}; // 空對象 -- truthy 還是 falsy? var c = function(){}; // 空函數 -- truthy 還是 falsy? var d = Boolean( a && b && c ); d; ``` 是的,你猜到了,這里的`d`依然是`true`。為什么?和前面的原因一樣。盡管它們看起來像,但是`[]`,`{}`,和`function(){}`*不在*?falsy列表中,因此它們是truthy值。 換句話說,truthy列表是無限長的。不可能制成一個這樣的列表。你只能制造一個falsy列表并查詢它。 花五分鐘,把falsy列表寫在便利貼上,然后粘在你的電腦顯示器上,或者如果你愿意就記住它。不管哪種方法,你都可以在自己需要的時候通過簡單地查詢一個值是否在falsy列表中,來構建一個虛擬的truthy列表。 truthy和falsy的重要性在于,理解如果一個值在被(明確地或隱含地)強制轉換為`boolean`值的話,它將如何動作。現在你的大腦中有了這兩個列表,我們可以深入強制轉換的例子本身了。 ## 明確的強制轉換 *明確的*?強制轉換指的是明顯且明確的類型轉換。對于大多數開發者來說,有很多類型轉換的用法可以清楚地歸類于這種?*明確的*?強制轉換。 我們在這里的目標是,在我們的代碼中指明一些模式,在這些模式中我們可以清楚明白地將一個值從一種類型轉換至另一種類型,以確保不給未來將讀到這段代碼的開發者留下任何坑。我們越明確,后來的人就越容易讀懂我們的代碼,也不必費太多的力氣去理解我們的意圖。 關于?*明確的*?強制轉換可能很難找到什么主要的不同意見,因為它與被廣泛接受的靜態類型語言中的類型轉換的工作方式非常接近。因此,我們理所當然地認為(暫且)?*明確的*?強制轉換可以被認同為不是邪惡的,或沒有爭議的。雖然我們稍后會回到這個話題。 ### 明確地:Strings Numbers 我們將從最簡單,也許是最常見強制轉換操作開始:將值在`string`和`number`表現形式之間進行強制轉換。 為了在`string`和`number`之間進行強制轉換,我們使用內建的`String(..)`和`Number(..)`函數(我們在第三章中所指的“原生構造器”),但?非常重要的是,我們不在它們前面使用`new`關鍵字。這樣,我們就不是在創建對象包裝器。 取而代之的是,我們實際上在兩種類型之間進行?*明確地強制轉換*: ```source-js var a = 42; var b = String( a ); var c = "3.14"; var d = Number( c ); b; // "42" d; // 3.14 ``` `String(..)`使用早先討論的`ToString`操作的規則,將任意其它的值強制轉換為一個基本類型的`string`值。`Number(..)`使用早先討論過的`ToNumber`操作的規則,將任意其他的值強制轉換為一個基本類型的`number`值。 我稱此為?*明確的*?強制轉換是因為,一般對于大多數開發者來說這是十分明顯的:這些操作的最終結果是適當的類型轉換。 實際上,這種用法看起來與其他的靜態類型語言中的用法非常相像。 舉個例子,在C/C++中,你既可以說`(int)x`也可以說`int(x)`,而且它們都將`x`中的值轉換為一個整數。兩種形式都是合法的,但是許多人偏向于后者,它看起來有點兒像一個函數調用。在JavaScript中,當你說`Number(x)`時,它看起來極其相似。在JS中它實際上是一個函數調用這個事實重要嗎?并非如此。 除了`String(..)`和`Number(..)`,還有其他的方法可以把這些值在`string`和`number`之間進行“明確地”轉換: ```source-js var a = 42; var b = a.toString(); var c = "3.14"; var d = +c; b; // "42" d; // 3.14 ``` 調用`a.toString()`在表面上是明確的(“toString”意味著“變成一個字符串”是很明白的),但是這里有一些藏起來的隱含性。`toString()`不能在像`42`這樣的?*基本類型*?值上調用。所以JS會自動地將`42`“封箱”在一個對象包裝器中(見第三章),這樣`toString()`就可以針對這個對象調用。換句話講,你可能會叫它“明確的隱含”。 這里的`+c`是`+`操作符的?*一元操作符*(操作符只有一個操作數)形式。取代進行數學加法(或字符串連接 —— 見下面的討論)的是,一元的`+`明確地將它的操作數(`c`)強制轉換為一個`number`值。 `+c`是?*明確的*?強制轉換嗎?這要看你的經驗和角度。如果你知道(現在你知道了!)一元`+`明確地意味著`number`強制轉換,那么它就是相當明確和明顯的。但是,如果你以前從沒見過它,那么它看起來就極其困惑,晦澀,帶有隱含的副作用,等等。 注意:?在開源的JS社區中一般被接受的觀點是,一元`+`是一個?*明確的*?強制轉換形式。 即使你真的喜歡`+c`這種形式,它絕對會在有的地方看起來非常令人困惑。考慮下面的代碼: ```source-js var c = "3.14"; var d = 5+ +c; d; // 8.14 ``` 一元`-`操作符也像`+`一樣進行強制轉換,但它還會翻轉數字的符號。但是你不能放兩個減號`--`來使符號翻轉回來,因為那將被解釋為遞減操作符。取代它的是,你需要這么做:`- -"3.14"`,在兩個減號之間加入空格,這將會使強制轉換的結果為`3.14`。 你可能會想到所有種類的可怕組合 —— 一個二元操作符挨著另一個操作符的一元形式。這里有另一個瘋狂的例子: ```source-js 1 + - + + + - + 1; // 2 ``` 當一個一元`+`(或`-`)緊鄰其他操作符時,你應當強烈地考慮避免使用它。雖然上面的代碼可以工作,但幾乎全世界都認為它是一個壞主意。即使是`d = +c`(或者`d =+ c`!)都太容易與`d += c`像混淆了,而后者完全是不同的東西! 注意:?一元`+`的另一個極端使人困惑的地方是,被用于緊挨著另一個將要作為`++`遞增操作符和`--`遞減操作符的操作數。例如:`a +++b`,`a + ++b`,和`a + + +b`。更多關于`++`的信息,參見第五章的“表達式副作用”。 記住,我們正努力變得明確并?減少?困惑,不是把事情弄得更糟! #### 從`Date`到`number` 另一個一元`+`操作符的常見用法是將一個`Date`對象強制轉換為一個`number`,其結果是這個日期/時間值的unix時間戳(從世界協調時間的1970年1月1日0點開始計算,經過的毫秒數)表現形式: ```source-js var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" ); +d; // 1408369986000 ``` 這種習慣性用法經常用于取得當前的?*現在*?時刻的時間戳,比如: ```source-js var timestamp = +new Date(); ``` 注意:?一些開發者知道一個JavaScript中的特別的語法“技巧”,就是在構造器調用(一個帶有`new`的函數調用)中如果沒有參數值要傳遞的話,`()`是?*可選的*。所以你可能遇到`var timestamp = +new Date;`形式。然而,不是所有的開發者都同意忽略`()`可以增強可讀性,因為它是一種不尋常的語法特例,只能適用于`new fn()`調用形式,而不能用于普通的`fn()`調用形式。 但強制轉換不是從`Date`對象中取得時間戳的唯一方法。一個不使用強制轉換的方式可能更好,因為它更加明確: ```source-js var timestamp = new Date().getTime(); // var timestamp = (new Date()).getTime(); // var timestamp = (new Date).getTime(); ``` 但是一個?*更更好的*?不使用強制轉換的選擇是使用ES5加入的`Date.now()`靜態函數: ```source-js var timestamp = Date.now(); ``` 而且如果你想要為老版本的瀏覽器填補`Date.now()`的話,也十分簡單: ```source-js if (!Date.now) { Date.now = function() { return +new Date(); }; } ``` 我推薦跳過與日期有關的強制轉換形式。使用`Date.now()`來取得當前?*現在*?的時間戳,而使用`new Date( .. ).getTime()`來取得一個需要你指定的?*非現在*?日期/時間的時間戳。 #### 奇異的`~` 一個經常被忽視并通常讓人糊涂的JS強制操作符是波浪線`~`操作符(也叫“按位取反”,“比特非”)。許多理解它在做什么的人也總是想要避開它。但是為了堅持我們在本書和本系列中的精神,讓我們深入并找出`~`是否有一些對我們有用的東西。 在第二章的“32位(有符號)整數”一節,我們講解了在JS中位操作符是如何僅為32位操作定義的,這意味著我們強制它們的操作數遵循32位值的表現形式。這個規則如何發生是由`ToInt32`抽象操作(ES5語言規范,9.5部分)控制的。 `ToInt32`首先進行`ToNumber`強制轉換,這就是說如果值是`"123"`,它在`ToInt32`規則實施之前會首先變成`123`。 雖然它本身沒有?*技術上進行*?強制轉換(因為類型沒有改變),但對一些特定的特殊`number`值使用位操作符(比如`|`或`~`)會產生一種強制轉換效果,這種效果的結果是一個不同的`number`值。 舉例來說,讓我們首先考慮慣用的空操作`0 | x`(在第二種章有展示)中使用的`|`“比特或”操作符,它實質上僅僅進行`ToInt32`轉換: ```source-js 0 | -0; // 0 0 | NaN; // 0 0 | Infinity; // 0 0 | -Infinity; // 0 ``` 這些特殊的數字是不可用32位表現的(因為它們源自64位的IEEE 754標準 —— 見第二章),所以`ToInt32`將這些值的結果指定為`0`。 有爭議的是,`0 | __`是否是一種`ToInt32`強制轉換操作的?*明確的*?形式,還是更傾向于?*隱含*。從語言規范的角度來說,毫無疑問是?*明確的*,但是如果你沒有在這樣的層次上理解位操作,它就可能看起來有點像?*隱含的*?魔法。不管怎樣,為了與本章中其他的斷言保持一致,我們稱它為?*明確的*。 那么,讓我們把注意力轉回`~`。`~`操作符首先將值“強制轉換”為一個32位`number`值,然后實施按位取反(翻轉每一個比特位)。 注意:?這與`!`不僅強制轉換它的值為`boolean`而且還翻轉它的每一位很相似(見后面關于“一元`!`”的討論)。 但是……什么!?為什么我們要關心被翻轉的比特位?這是一些相當特殊的,微妙的東西。JS開發者需要推理個別比特位是十分少見的。 另一種考慮`~`定義的方法是,`~`源自學校中的計算機科學/離散數學:`~`進行二進制取補操作。太好了,謝謝,我完全明白了! 我們再試一次:`~x`大致與`-(x+1)`相同。這很奇怪,但是稍微容易推理一些。所以: ```source-js ~42; // -(42+1) ==> -43 ``` 你可能還在想`~`這個鬼東西到底和什么有關,或者對于強制轉換的討論它究竟有什么要緊。讓我們快速進入要點。 考慮一下`-(x+1)`。通過進行這個操作,能夠產生結果`0`(或者從技術上說`-0`!)的唯一的值是什么?`-1`。換句話說,`~`用于一個范圍的`number`值時,將會為輸入值`-1`產生一個falsy(很容易強制轉換為`false`)的`0`,而為任意其他的輸入產生truthy的`number`。 為什么這要緊? `-1`通常稱為一個“哨兵值”,它基本上意味著一個在同類型值(`number`)的更大的集合中被賦予了任意的語義。在C語言中許多函數使用哨兵值`-1`,它們返回`>= 0`的值表示“成功”,返回`-1`表示“失敗”。 JavaScript在定義`string`操作`indexOf(..)`時采納了這種先例,它搜索一個子字符串,如果找到就返回它從0開始計算的索引位置,沒有找到的話就返回`-1`。 這樣的情況很常見:不僅僅將`indexOf(..)`作為取得位置的操作,而且作為檢查一個子字符串存在/不存在于另一個`string`中的`boolean`值。這就是開發者們通常如何進行這樣的檢查: ```source-js var a = "Hello World"; if (a.indexOf( "lo" ) >= 0) { // true // 找到了! } if (a.indexOf( "lo" ) != -1) { // true // 找到了 } if (a.indexOf( "ol" ) < 0) { // true // 沒找到! } if (a.indexOf( "ol" ) == -1) { // true // 沒找到! } ``` 我感覺看著`>= 0`或`== -1`有些惡心。它基本上是一種“抽象泄漏”,這里它將底層的實現行為 —— 使用哨兵值`-1`表示“失敗” —— 泄漏到我的代碼中。我倒是樂意隱藏這樣的細節。 現在,我們終于看到為什`~`可以幫到我們了!將`~`和`indexOf()`一起使用可以將值“強制轉換”(實際上只是變形)為?可以適當地強制轉換為`boolean`的值: ```source-js var a = "Hello World"; ~a.indexOf( "lo" ); // -4 <-- truthy! if (~a.indexOf( "lo" )) { // true // 找到了! } ~a.indexOf( "ol" ); // 0 <-- falsy! !~a.indexOf( "ol" ); // true if (!~a.indexOf( "ol" )) { // true // 沒找到! } ``` `~`拿到`indexOf(..)`的返回值并將它變形:對于“失敗”的`-1`我們得到falsy的`0`,而其他的值都是truthy。 注意:?`~`的假想算法`-(x+1)`暗示著`~-1`是`-0`,但是實際上它產生`0`,因為底層的操作其實是按位的,不是數學操作。 技術上講,`if (~a.indexOf(..))`仍然依靠?*隱含的*?強制轉換將它的結果`0`變為`false`或非零變為`true`。但總的來說,對我而言`~`更像一種?*明確的*?強制轉換機制,只要你知道在這種慣用法中它的意圖是什么。 我感覺這樣的代碼要比前面凌亂的`>= 0`?/?`== -1`更干凈。 ##### 截斷比特位 在你遇到的代碼中,還有一個地方可能出現`~`:一些開發者使用雙波浪線`~~`來截斷一個`number`的小數部分(也就是,將它“強制轉換”為一個“整數”)。這通常(雖然是錯誤的)被說成與調用`Math.floor(..)`的結果相同。 `~ ~`的工作方式是,第一個`~`實施`ToInt32`“強制轉換”并進行按位取反,然后第二個`~`進行另一次按位取反,將每一個比特位都翻轉回原來的狀態。于是最終的結果就是`ToInt32`“強制轉換”(也叫截斷)。 注意:?`~~`的按位雙翻轉,與雙否定`!!`的行為非常相似,它將在稍后的“明確地:* --> Boolean”一節中講解。 然而,`~~`需要一些注意/澄清。首先,它僅在32位值上可以可靠地工作。但更重要的是,它在負數上工作的方式與`Math.floor(..)`不同! ```source-js Math.floor( -49.6 ); // -50 ~~-49.6; // -49 ``` 把`Math.floor(..)`的不同放在一邊,`~~x`可以將值截斷為一個(32位)整數。但是`x | 0`也可以,而且看起來還(稍微)*省事兒*?一些。 那么,為什么你可能會選擇`~~x`而不是`x | 0`?操作符優先權(見第五章): ```source-js ~~1E20 / 10; // 166199296 1E20 | 0 / 10; // 1661992960 (1E20 | 0) / 10; // 166199296 ``` 正如這里給出的其他建議一樣,僅在讀/寫這樣的代碼的每一個人都知道這些操作符如何工作的情況下,才將`~`和`~~`作為“強制轉換”和將值變形的明確機制。 ### 明確地:解析數字字符串 將一個`string`強制轉換為一個`number`的類似結果,可以通過從`string`的字符內容中解析(parsing)出一個`number`得到。然而在這種解析和我們上面講解的類型轉換之間存在著區別。 考慮下面的代碼: ```source-js var a = "42"; var b = "42px"; Number( a ); // 42 parseInt( a ); // 42 Number( b ); // NaN parseInt( b ); // 42 ``` 從一個字符串中解析出一個數字是?*容忍*?非數字字符的 —— 從左到右,如果遇到非數字字符就停止解析 —— 而強制轉換是?*不容忍*?并且會失敗而得出值`NaN`。 解析不應當被視為強制轉換的替代品。這兩種任務雖然相似,但是有著不同的目的。當你不知道/不關心右手邊可能有什么其他的非數字字符時,你可以將一個`string`作為`number`解析。當只有數字才是可接受的值,而且像`"42px"`這樣的東西作為數字應當被排除時,就強制轉換一個`string`(變為一個`number`)。 提示:?`parseInt(..)`有一個孿生兄弟,`parseFloat(..)`,它(聽起來)從一個字符串中拉出一個浮點數。 不要忘了`parseInt(..)`工作在`string`值上。向`parseInt(..)`傳遞一個`number`絕對沒有任何意義。傳遞其他任何類型也都沒有意義,比如`true`,?`function(){..}`或`[1,2,3]`。 如果你傳入一個非`string`,你所傳入的值首先將自動地被強制轉換為一個`string`(見早先的“`ToString`”),這很明顯是一種隱藏的?*隱含*?強制轉換。在你的程序中依賴這樣的行為真的是一個壞主意,所以永遠也不要將`parseInt(..)`與非`string`值一起使用。 在ES5之前,`parseInt(..)`還存在另外一個坑,這曾是許多JS程序的bug的根源。如果你不傳遞第二個參數來指定使用哪種進制(也叫基數)來翻譯數字的`string`內容,`parseInt(..)`將會根據開頭的字符進行猜測。 如果開頭的兩個字符是`"0x"`或`"0X"`,那么猜測(根據慣例)將是你想要將這個`string`翻譯為一個16進制`number`。否則,如果第一個字符是`"0"`,那么猜測(也是根據慣例)將是你想要將這個`string`翻譯成8進制`number`。 16進制的`string`(以`0x`或`0X`開頭)沒那么容易搞混。但是事實證明8進制數字的猜測過于常見了。比如: ```source-js var hour = parseInt( selectedHour.value ); var minute = parseInt( selectedMinute.value ); console.log( "The time you selected was: " + hour + ":" + minute); ``` 看起來無害,對吧?試著在小時上選擇`08`在分鐘上選擇`09`。你會得到`0:0`。為什么?因為`8`和`9`都不是合法的8進制數。 ES5之前的修改很簡單,但是很容易忘:總是在第二個參數值上傳遞`10`。這完全是安全的: ```source-js var hour = parseInt( selectedHour.value, 10 ); var minute = parseInt( selectedMiniute.value, 10 ); ``` 在ES5中,`parseInt(..)`不再猜測八進制數了。除非你指定,否則它會假定為10進制(或者為`"0x"`前綴猜測16進制數)。這好多了。只是要小心,如果你的代碼不得不運行在前ES5環境中,你仍然需要為基數傳遞`10`。 #### 解析非字符串 幾年以前有一個挖苦JS的玩笑,使一個關于`parseInt(..)`行為的一個臭名昭著的例子備受關注,它取笑JS的這個行為: ```source-js parseInt( 1/0, 19 ); // 18 ``` 這里面設想(但完全不合法)的斷言是,“如果我傳入一個無限大,并從中解析出一個整數的話,我應該得到一個無限大,不是18”。沒錯,JS一定是瘋了才得出這個結果,對吧? 雖然這是個明顯故意造成的,不真實的例子,但是讓我們放縱這種瘋狂一小會兒,來檢視一下JS是否真的那么瘋狂。 首先,這其中最明顯的原罪是將一個非`string`傳入了`parseInt(..)`。這是不對的。這么做是自找麻煩。但就算你這么做了,JS也會禮貌地將你傳入的東西強制轉換為它可以解析的`string`。 有些人可能會爭論說這是一種不合理的行為,`parseInt(..)`應當拒絕在一個非`string`值上操作。它應該拋出一個錯誤嗎?坦白地說,像Java那樣。但是一想到JS應當開始在滿世界拋出錯誤,以至于幾乎每一行代碼都需要用`try..catch`圍起來,我就不寒而栗。 它應當返回`NaN`嗎?也許。但是……要是這樣呢: ```source-js parseInt( new String( "42") ); ``` 這也應當失敗嗎?它是一個非`string`值啊。如果你想讓`String`對象包裝器被開箱成`"42"`,那么`42`先變成`"42"`,以使`42`可以被解析回來就那么不尋常嗎? 我會爭論說,這種可能發生的半?*明確*?半?*隱含*?的強制轉換經常可以成為非常有用的東西。比如: ```source-js var a = { num: 21, toString: function() { return String( this.num * 2 ); } }; parseInt( a ); // 42 ``` 事實上`parseInt(..)`將它的值強制轉換為`string`來實施解析是十分合理的。如果你傳垃圾進去,那么你就會得到垃圾,不要責備垃圾桶 —— 它只是忠實地盡自己的責任。 那么,如果你傳入像`Infinity`(很明顯是`1 / 0`的結果)這樣的值,對于它的強制轉換來說哪種`string`表現形式最有道理呢?我腦中只有兩種合理的選擇:`"Infinity"`和`"∞"`。JS選擇了`"Infinity"`。我很高興它這么選。 我認為在JS中?所有的值?都有某種默認的`string`表現形式是一件好事,這樣它們就不是我們不能調試和推理的神秘黑箱了。 現在,關于19進制呢?很明顯,這完全是偽命題和造作。沒有真實的JS程序使用19進制。那太荒謬了。但是,讓我們再一次放任這種荒謬。在19進制中,合法的數字字符是`0`?-?`9`和`a`?-?`i`(大小寫無關)。 那么,回到我們的`parseInt( 1/0, 19 )`例子。它實質上是`parseInt( "Infinity", 19 )`。它如何解析?第一個字符是`"I"`,在愚蠢的19進制中是值`18`。第二個字符`"n"`不再合法的數字字符集內,所以這樣的解析就禮貌地停止了,就像它在`"42px"`中遇到`"p"`那樣。 結果呢?`18`。正如它應該的那樣。對JS來說,并非一個錯誤或者`Infinity`本身,而是將我們帶到這里的一系列的行為才是?非常重要?的,不應當那么簡單地被丟棄。 其他關于`parseInt(..)`行為的,令人吃驚但又十分合理的例子還包括: ```source-js parseInt( 0.000008 ); // 0 ("0" from "0.000008") parseInt( 0.0000008 ); // 8 ("8" from "8e-7") parseInt( false, 16 ); // 250 ("fa" from "false") parseInt( parseInt, 16 ); // 15 ("f" from "function..") parseInt( "0x10" ); // 16 parseInt( "103", 2 ); // 2 ``` 其實`parseInt(..)`在它的行為上是相當可預見和一致的。如果你正確地使用它,你就能得到合理的結果。如果你不正確地使用它,那么你得到的瘋狂結果并不是JavaScript的錯。 ### 明確地:* --> Boolean 現在,我們來檢視從任意的非`boolean`值到一個`boolean`值的強制轉換。 正如上面的`String(..)`和`Number(..)`,`Boolean(..)`(當然,不帶`new`!)是強制進行`ToBoolean`轉換的明確方法: ```source-js var a = "0"; var b = []; var c = {}; var d = ""; var e = 0; var f = null; var g; Boolean( a ); // true Boolean( b ); // true Boolean( c ); // true Boolean( d ); // false Boolean( e ); // false Boolean( f ); // false Boolean( g ); // false ``` 雖然`Boolean(..)`是非常明確的,但是它并不常見也不為人所慣用。 正如一元`+`操作符將一個值強制轉換為一個`number`(參見上面的討論),一元的`!`否定操作符可以將一個值明確地強制轉換為一個`boolean`。*問題*?是它還將值從truthy翻轉為falsy,或反之。所以,大多數JS開發者使用`!!`雙否定操作符進行`boolean`強制轉換,因為第二個`!`將會把它翻轉回原本的true或false: ```source-js var a = "0"; var b = []; var c = {}; var d = ""; var e = 0; var f = null; var g; !!a; // true !!b; // true !!c; // true !!d; // false !!e; // false !!f; // false !!g; // false ``` 沒有`Boolean(..)`或`!!`的話,任何這些`ToBoolean`強制轉換都將?*隱含地*?發生,比如在一個`if (..) ..`語句這樣使用`boolean`的上下文中。但這里的目標是,明確地強制一個值成為`boolean`來使`ToBoolean`強制轉換的意圖顯得明明白白。 另一個`ToBoolean`強制轉換的用例是,如果你想在數據結構的JSON序列化中強制轉換一個`true`/`false`: ```source-js var a = [ 1, function(){ /*..*/ }, 2, function(){ /*..*/ } ]; JSON.stringify( a ); // "[1,null,2,null]" JSON.stringify( a, function(key,val){ if (typeof val == "function") { // 強制函數進行 `ToBoolean` 轉換 return !!val; } else { return val; } } ); // "[1,true,2,true]" ``` 如果你是從Java來到JavaScript的話,你可能會認得這個慣用法: ```source-js var a = 42; var b = a ? true : false; ``` `? :`三元操作符將會測試`a`的真假,然后根據這個測試的結果相應地將`true`或`false`賦值給`b`。 表面上,這個慣用法看起來是一種?*明確的*?`ToBoolean`類型強制轉換形式,因為很明顯它操作的結果要么是`true`要么是`false`。 然而,這里有一個隱藏的?*隱含*?強制轉換,就是表達式`a`不得不首先被強制轉換為`boolean`來進行真假測試。我稱這種慣用法為“明確地隱含”。另外,我建議你在JavaScript中?完全避免這種慣用法。它不會提供真正的好處,而且會讓事情變得更糟。 對于?*明確的*?強制轉換`Boolean(a)`和`!!a`是好得多的選項。 ## 隱含的強制轉換 *隱含的*?強制轉換是指這樣的類型轉換:它們是隱藏的,由于其他的動作隱含地發生的不明顯的副作用。換句話說,任何(對你)不明顯的類型轉換都是?*隱含的強制轉換*。 雖然?*明確的*?強制轉換的目的很明白,但是這可能?*太過*?明顯 ——?*隱含的*?強制轉換擁有相反的目的:使代碼更難理解。 從表面上來看,我相信這就是許多關于強制轉換的憤怒的源頭。絕大多數關于“JavaScript強制轉換”的抱怨實際上都指向了(不管他們是否理解它)?*隱含的*?強制轉換。 注意:?Douglas Crockford,*"JavaScript: The Good Parts"*?的作者,在許多會議和他的作品中聲稱應當避免JavaScript強制轉換。但看起來他的意思是?*隱含的*?強制轉換是不好的(以他的意見)。然而,如果你讀他自己的代碼的話,你會發現相當多的強制轉換的例子,*明確*?和?*隱含*?都有!事實上,他的擔憂主要在于`==`操作,但正如你將在本章中看到的,那只是強制轉換機制的一部分。 那么,隱含強制轉換?是邪惡的嗎?它很危險嗎?它是JavaScript設計上的缺陷嗎?我們應該盡一切力量避免它嗎? 我打賭大多數讀者都傾向于踴躍地歡呼,“是的!” 別那么著急。聽我把話說完。 讓我們在?*隱含的*?強制轉換是什么,和可以是什么這個問題上采取一個不同的角度,而不是僅僅說它是“好的明確強制轉換的反面”。這太過狹隘,而且忽視了一個重要的微妙細節。 讓我們將?*隱含的*?強制轉換的目的定義為:減少搞亂我們代碼的繁冗,模板代碼,和/或不必要的實現細節,不使它們的噪音掩蓋更重要的意圖。 ### 用于簡化的隱含 在我們進入JavaScript以前,我建議使用某個理論上是強類型的語言的假想代碼來說明一下: ```source-js SomeType x = SomeType( AnotherType( y ) ) ``` 在這個例子中,我在`y`中有一些任意類型的值,想把它轉換為`SomeType`類型。問題是,這種語言不能從當前`y`的類型直接走到`SomeType`。它需要一個中間步驟,它首先轉換為`AnotherType`,然后從`AnotherType`轉換到`SomeType`。 現在,要是這種語言(或者你可用這種語言創建自己的定義)允許你這么說呢: ```source-js SomeType x = SomeType( y ) ``` 難道一般來說你不會同意我們簡化了這里的類型轉換,降低了中間轉換步驟的無謂的“噪音”嗎?我的意思是,在這段代碼的這一點上,能看到并處理`y`先變為`AnotherType`然后再變為`SomeType`的事實,*真的*?是很重要的一件事嗎? 有些人可能會爭辯,至少在某些環境下,是的。但我想我可以做出相同的爭辯說,在許多其他的環境下,不管是通過語言本身的還是我們自己的抽象,這樣的簡化通過抽象或隱藏這些細節?確實增強了代碼的可讀性。 毫無疑問,在幕后的某些地方,那個中間的步驟依然是發生的。但如果這樣的細節在視野中隱藏起來,我們就可以將使`y`變為類型`SomeType`作為一個泛化操作來推理,并隱藏混亂的細節。 雖然不是一個完美的類比,我要在本章剩余部分爭論的是,JS的?*隱含的*?強制轉換可以被認為是給你的代碼提供了一個類似的輔助。 但是,很重要的是,這不是一個無邊界的,絕對的論斷。絕對有許多?*邪惡的東西*?潛伏在?*隱含*?強制轉換周圍,它們對你的代碼造成的損害要比任何潛在的可讀性改善厲害的多。很清楚,我們不得不學習如何避免這樣的結構,使我們不會用各種bug來毒害我們的代碼。 許多開發者相信,如果一個機制可以做某些有用的事兒?A,但也可以被濫用或誤用來做某些可怕的事兒?Z,那么我們就應當將這種機制整個兒扔掉,僅僅是為了安全。 我對你的鼓勵是:不要安心于此。不要“把孩子跟洗澡水一起潑出去”。不要因為你只見到過它的“壞的一面”就假設?*隱含*?強制轉換都是壞的。我認為這里有“好的一面”,而我想要幫助和啟發你們更多的人找到并接納它們! ### 隱含地:Strings Numbers 在本章的早先,我們探索了`string`和`number`值之間的?*明確*?強制轉換。現在,讓我們使用?*隱含*?強制轉換的方式探索相同的任務。但在我們開始之前,我們不得不檢視一些將會?*隱含地*?發生強制轉換的操作的微妙之處。 為了服務于`number`的相加和`string`的連接兩個目的,`+`操作符被重載了。那么JS如何知道你想用的是哪一種操作呢?考慮下面的代碼: ```source-js var a = "42"; var b = "0"; var c = 42; var d = 0; a + b; // "420" c + d; // 42 ``` 是什么不同導致了`"420"`和`42`?一個常見的誤解是,這個不同之處在于操作數之一或兩者是否是一個`string`,這意味著`+`將假設`string`連接。雖然這有一部分是對的,但實際情況要更復雜。 考慮如下代碼: ```source-js var a = [1,2]; var b = [3,4]; a + b; // "1,23,4" ``` 兩個操作數都不是`string`,但很明顯它們都被強制轉換為`string`然后啟動了`string`連接。那么到底發生了什么? (警告:?語言規范式的深度細節就要來了,如果這會嚇到你就跳過下面兩段!) * * * 根據ES5語言規范的11.6.1部分,`+`的算法是(當一個操作數是`object`值時),如果兩個操作數之一已經是一個`string`,或者下列步驟產生一個`string`表達形式,`+`將會進行連接。所以,當`+`的兩個操作數之一收到一個`object`(包括`array`)時,它首先在這個值上調用`ToPrimitive`抽象操作(9.1部分),而它會帶著`number`的上下文環境提示來調用`[[DefaultValue]]`算法(8.12.8部分)。 如果你仔細觀察,你會發現這個操作現在和`ToNumber`抽象操作處理`object`的過程是一樣的(參見早先的“`ToNumber`”一節)。在`array`上的`valueOf()`操作將會在產生一個簡單基本類型時失敗,于是它退回到一個`toString()`表現形式。兩個`array`因此分別變成了`"1,2"`和`"3,4"`。現在,`+`就如你通常期望的那樣連接這兩個`string`:`"1,23,4"`。 * * * 讓我們把這些亂七八糟的細節放在一邊,回到一個早前的,簡化的解釋:如果`+`的兩個操作數之一是一個`string`(或在上面的步驟中成為一個`string`),那么操作就會是`string`連接。否則,它總是數字加法。 注意:?關于強制轉換,一個經常被引用的坑是`[] + {}`和`{} + []`,這兩個表達式的結果分別是`"[object Object]"`和`0`。雖然對此有更多的東西,但是我們將在第五章的“Block”中講解這其中的細節。 這對?*隱含*?強制轉換意味著什么? 你可以簡單地通過將`number`和空`string``""`“相加”來把一個`number`強制轉換為一個`string`: ```source-js var a = 42; var b = a + ""; b; // "42" ``` 提示:?使用`+`操作符的數字加法是可交換的,這意味著`2 + 3`與`3 + 2`是相同的。使用`+`的字符串連接很明顯通常不是可交換的,但是?對于`""`的特定情況,它實質上是可交換的,因為`a + ""`和`"" + a`會產生相同的結果。 使用一個`+ ""`操作將`number`(*隱含地*)強制轉換為`string`是極其常見/慣用的。事實上,有趣的是,一些在口頭上批評?*隱含*強制轉換得最嚴厲的人仍然在他們自己的代碼中使用這種方式,而不是使用它的?*明確的*?替代形式。 在?*隱含*?強制轉換的有用形式中,我認為這是一個很棒的例子,盡管這種機制那么頻繁地被人詬病! 將`a + ""`這種?*隱含的*?強制轉換與我們早先的`String(a)`*明確的*?強制轉換的例子相比較,有一個另外的需要小心的奇怪之處。由于`ToPrimitive`抽象操作的工作方式,`a + ""`在值`a`上調用`valueOf()`,它的返回值再最終通過內部的`ToString`抽象操作轉換為一個`string`。但是`String(a)`只直接調用`toString()`。 兩種方式的最終結果都是一個`string`,但如果你使用一個`object`而不是一個普通的基本類型`number`的值,你可能不一定得到?*相同的*?`string`值! 考慮這段代碼: ```source-js var a = { valueOf: function() { return 42; }, toString: function() { return 4; } }; a + ""; // "42" String( a ); // "4" ``` 一般來說這樣的坑不會咬到你,除非你真的試著創建令人困惑的數據結構和操作,但如果你為某些`object`同時定義了你自己的`valueOf()`和`toString()`方法,你就應當小心,因為你強制轉換這些值的方式將影響到結果。 那么另外一個方向呢?我們如何將一個`string`?*隱含強制轉換*?為一個`number`? ```source-js var a = "3.14"; var b = a - 0; b; // 3.14 ``` `-`操作符是僅為數字減法定義的,所以`a - 0`強制`a`的值被轉換為一個`number`。雖然少見得多,`a * 1`或`a / 1`也會得到相同的結果,因為這些操作符也是僅為數字操作定義的。 那么對`-`操作符使用`object`值會怎樣呢?和上面的`+`的故事相似: ```source-js var a = [3]; var b = [1]; a - b; // 2 ``` 兩個`array`值都不得不變為`number`,但它們首先會被強制轉換為`string`(使用意料之中的`toString()`序列化),然后再強制轉換為`number`,以便`-`減法操作可以實施。 那么,`string`和`number`值之間的?*隱含*?強制轉換還是你總是在恐怖故事當中聽到的丑陋怪物嗎?我個人不這么認為。 比較`b = String(a)`(*明確的*)和`b = a + ""`(*隱含的*)。我認為在你的代碼中會出現兩種方式都有用的情況。當然`b = a + ""`在JS程序中更常見一些,不管一般意義上?*隱含*?強制轉換的好處或害處的?*感覺*?如何,它都提供了自己的用途。 ### 隱含地:Booleans --> Numbers 我認為?*隱含*?強制轉換可以真正閃光的一個情況是,將特定類型的復雜`boolean`邏輯簡化為簡單的數字加法。當然,這不是一個通用的技術,而是一個特定情況的特定解決方法。 考慮如下代碼: ```source-js function onlyOne(a,b,c) { return !!((a && !b && !c) || (!a && b && !c) || (!a && !b && c)); } var a = true; var b = false; onlyOne( a, b, b ); // true onlyOne( b, a, b ); // true onlyOne( a, b, a ); // false ``` 這個`onlyOne(..)`工具應當僅在正好有一個參數是`true`/truthy時返回`true`。它在truthy的檢查上使用?*隱含的*?強制轉換,而在其他的地方使用?*明確的*?強制轉換,包括最后的返回值。 但如果我們需要這個工具能夠以相同的方式處理四個,五個,或者二十個標志值呢?很難想象處理所有那些比較的排列組合的代碼實現。 但這里是`boolean`值到`number`(很明顯,`0`或`1`)的強制轉換可以提供巨大幫助的地方: ```source-js function onlyOne() { var sum = 0; for (var i=0; i < arguments.length; i++) { // 跳過falsy值。與將它們視為0相同,但是避開NaN if (arguments[i]) { sum += arguments[i]; } } return sum == 1; } var a = true; var b = false; onlyOne( b, a ); // true onlyOne( b, a, b, b, b ); // true onlyOne( b, b ); // false onlyOne( b, a, b, b, b, a ); // false ``` 注意:?當然,除了在`onlyOne(..)`中的`for`循環,你可以更簡潔地使用ES5的`reduce(..)`工具,但我不想因此而模糊概念。 我們在這里做的事情有賴于`true`/truthy的強制轉換結果為`1`,并將它們作為數字加起來。`sum += arguments[i]`通過?*隱含的*強制轉換使這發生。如果在`arguments`列表中有且僅有一個值為`true`,那么這個數字的和將是`1`,否則和就不是`1`而不能使期望的條件成立。 我們當然本可以使用?*明確的*?強制轉換: ```source-js function onlyOne() { var sum = 0; for (var i=0; i < arguments.length; i++) { sum += Number( !!arguments[i] ); } return sum === 1; } ``` 我們首先使用`!!arguments[i]`來將這個值強制轉換為`true`或`false`。這樣你就可以像`onlyOne( "42", 0 )`這樣傳入非`boolean`值了,而且它依然可以如意料的那樣工作(要不然,你將會得到`string`連接,而且邏輯也不正確)。 一旦我們確認它是一個`boolean`,我們就使用`Number(..)`進行另一個?*明確的*?強制轉換來確保值是`0`或`1`。 這個工具的?*明確*?強制轉換形式“更好”嗎?它確實像代碼注釋中解釋的那樣避開了`NaN`的陷阱。但是,這最終要看你的需要。我個人認為前一個版本,依賴于?*隱含的*?強制轉換更優雅(如果你不傳入`undefined`或`NaN`),而?*明確的*?版本是一種不必要的繁冗。 但與我們在這里討論的幾乎所有東西一樣,這是一個主觀判斷。 注意:?不管是?*隱含的*?還是?*明確的*?方式,你可以通過將最后的比較從`1`改為`2`或`5`,來分別很容易地制造`onlyTwo(..)`或`onlyFive(..)`。這要比添加一大堆`&&`和`||`表達式要簡單太多了。所以,一般來說,在這種情況下強制轉換非常有用。 ### 隱含地:* --> Boolean 現在,讓我們將注意力轉向目標為`boolean`值的?*隱含*?強制轉換上,這是目前最常見,并且還是目前潛在的最麻煩的一種。 記住,*隱含的*?強制轉換是當你以強制一個值被轉換的方式使用這個值時才啟動的。對于數字和`string`操作,很容易就能看出這種強制轉換是如何發生的。 但是,哪個種類的表達式操作(*隱含地*)要求/強制一個`boolean`轉換呢? 1. 在一個`if (..)`語句中的測試表達式。 2. 在一個`for ( .. ; .. ; .. )`頭部的測試表達式(第二個子句)。 3. 在`while (..)`和`do..while(..)`循環中的測試表達式。 4. 在`? :`三元表達式中的測試表達式(第一個子句)。 5. `||`(“邏輯或”)和`&&`(“邏輯與”)操作符左手邊的操作數(它用作測試表達式 —— 見下面的討論!)。 在這些上下文環境中使用的,任何還不是`boolean`的值,將通過本章早先講解的`ToBoolean`抽象操作的規則,被?*隱含地*?強制轉換為一個`boolean`。 我們來看一些例子: ```source-js var a = 42; var b = "abc"; var c; var d = null; if (a) { console.log( "yep" ); // yep } while (c) { console.log( "nope, never runs" ); } c = d ? a : b; c; // "abc" if ((a && d) || c) { console.log( "yep" ); // yep } ``` 在所有這些上下文環境中,非`boolean`值被?*隱含地強制轉換*?為它們的`boolean`等價物,來決定測試的結果。 ### `||`和`&&`操作符 很可能你已經在你用過的大多數或所有其他語言中見到過`||`(“邏輯或”)和`&&`(“邏輯與”)操作符了。所以假設它們在JavaScript中的工作方式和其他類似的語言基本上相同是很自然的。 這里有一個鮮為人知的,但很重要的,微妙細節。 其實,我會爭辯這些操作符甚至不應當被稱為“邏輯__操作符”,因為這樣的名稱沒有完整地描述它們在做什么。如果讓我給它們一個更準確的(也更蹩腳的)名稱,我會叫它們“選擇器操作符”或更完整的,“操作數選擇器操作符”。 為什么?因為在JavaScript中它們實際上不會得出一個?*邏輯*?值(也就是`boolean`),這與它們在其他的語言中的表現不同。 那么它們到底得出什么?它們得出兩個操作數中的一個(而且僅有一個)。換句話說,它們在兩個操作數的值中選擇一個。 引用ES5語言規范的11.11部分: > 一個&&或||操作符產生的值不見得是Boolean類型。這個產生的值將總是兩個操作數表達式其中之一的值。 讓我們展示一下: ```source-js var a = 42; var b = "abc"; var c = null; a || b; // 42 a && b; // "abc" c || b; // "abc" c && b; // null ``` 等一下,什么!??想一想。在像C和PHP這樣的語言中,這些表達式結果為`true`或`false`,而在JS中(就此而言還有Python和Ruby!),結果來自于值本身。 `||`和`&&`操作符都在?第一個操作數(`a`或`c`) 上進行`boolean`測試。如果這個操作數還不是`boolean`(就像在這里一樣),就會發生一次普通的`ToBoolean`強制轉換,這樣測試就可以進行了。 對于`||`操作符,如果測試結果為`true`,`||`表達式就將?*第一個操作數*?的值(`a`或`c`)作為結果。如果測試結果為`false`,`||`表達式就將?*第二個操作數*?的值(`b`)作為結果。 相反地,對于`&&`操作符,如果測試結果為`true`,`&&`表達式將?*第二個操作數*?的值(`b`)作為結果。如果測試結果為`false`,那么`&&`表達式就將?*第一個操作數*?的值(`a`或`c`)作為結果。 `||`或`&&`表達式的結果總是兩個操作數之一的底層值,不是(可能是被強制轉換來的)測試的結果。在`c && b`中,`c`是`null`,因此是falsy。但是`&&`表達式本身的結果為`null`(`c`中的值),不是用于測試的強制轉換來的`false`。 現在你明白這些操作符如何像“操作數選擇器”一樣工作了嗎? 另一種考慮這些操作數的方式是: ```source-js a || b; // 大體上等價于: a ? a : b; a && b; // 大體上等價于: a ? b : a; ``` 注意:?我說`a || b`“大體上等價”于`a ? a : b`,是因為雖然結果相同,但是這里有一個微妙的不同。在`a ? a : b`中,如果`a`是一個更復雜的表達式(例如像調用`function`那樣可能帶有副作用),那么這個表達式`a`將有可能被求值兩次(如果第一次求值的結果為truthy)。相比之下,對于`a || b`,表達式`a`僅被求值一次,而且這個值將被同時用于強制轉換測試和結果值(如果合適的話)。同樣的區別也適用于`a && b`和`a ? b : a`表達式。 很有可能你在沒有完全理解之前你就已經使用了這個行為的一個極其常見,而且很有幫助的用法: ```source-js function foo(a,b) { a = a || "hello"; b = b || "world"; console.log( a + " " + b ); } foo(); // "hello world" foo( "yeah", "yeah!" ); // "yeah yeah!" ``` 這種`a = a || "hello"`慣用法(有時被說成C#“null合并操作符”的JavaScript版本)對`a`進行測試,如果它沒有值(或僅僅是一個不期望的falsy值),就提供一個后備的默認值(`"hello"`)。 但是?要小心! ```source-js foo( "That's it!", "" ); // "That's it! world" <-- Oops! ``` 看到問題了嗎?作為第二個參數的`""`是一個falsy值(參見本章早先的`ToBoolean`),所以`b = b || "world"`測試失敗,而默認值`"world"`被替換上來,即便本來的意圖可能是想讓明確傳入的`""`作為賦給`b`的值。 這種`||`慣用法極其常見,而且十分有用,但是你不得不只在?*所有的falsy值*?應當被跳過時使用它。不然,你就需要在你的測試中更加具體,而且可能應該使用一個`? :`三元操作符。 這種默認值賦值慣用法是如此常見(和有用!),以至于那些公開激烈誹謗JavaScript強制轉換的人都經常在它們的代碼中使用! 那么`&&`呢? 有另一種在手動編寫中不那么常見,而在JS壓縮器中頻繁使用的慣用法。`&&`操作符會“選擇”第二個操作數,當且僅當第一個操作數測試為truthy,這種用法有時被稱為“守護操作符”(參見第五章的“短接”) —— 第一個表達式的測試“守護”著第二個表達式: ```source-js function foo() { console.log( a ); } var a = 42; a && foo(); // 42 ``` `foo()`僅在`a`測試為truthy時會被調用。如果這個測試失敗,這個`a && foo()`表達式語句將會無聲地停止 —— 這被稱為“短接” —— 而且永遠不會調用`foo()`。 重申一次,幾乎很少有人手動編寫這樣的東西。通常,他們會寫`if (a) { foo(); }`。但是JS壓縮器選擇`a && foo()`是因為它短的多。所以,現在,如果你不得不解讀這樣的代碼,你就知道它是在做什么以及為什么了。 好了,那么`||`和`&&`在它們的功能上有些不錯的技巧,只要你樂意讓?*隱含的*?強制轉換摻和進來。 注意:?`a = b || "something"`和`a && b()`兩種慣用法都依賴于短接行為,我們將在第五章中講述它的細節。 現在,這些操作符實際上不會得出`true`和`false`的事實可能使你的頭腦有點兒混亂。你可能想知道,如果你的`if`語句和`for`循環包含`a && (b || c)`這樣的復合的邏輯表達式,它們到底都是怎么工作的。 別擔心!天沒塌下來。你的代碼(可能)沒有問題。你只是可能從來沒有理解在這個符合表達式被求值?之后,有一個向`boolean`?*隱含的*?強制轉換發生了。 考慮這段代碼: ```source-js var a = 42; var b = null; var c = "foo"; if (a && (b || c)) { console.log( "yep" ); } ``` 這段代碼將會像你總是認為的那樣工作,除了一個額外的微妙細節。`a && (b || c)`的結果?*實際上*?是`"foo"`,不是`true`。所以,這之后`if`語句強制值`"foo"`轉換為一個`boolean`,這理所當然地將是`true`。 看到了?沒有理由驚慌。你的代碼可能依然是安全的。但是現在關于它在做什么和如何做,你知道了更多。 而且現在你理解了這樣的代碼使用?*隱含的*?強制轉換。如果你依然屬于“避開(隱含)強制轉換陣營”,那么你就需要退回去并使所有這些測試?*明確*: ```source-js if (!!a && (!!b || !!c)) { console.log( "yep" ); } ``` 祝你好運!...對不起,只是逗個樂兒。 ### Symbol 強制轉換 在此為止,在?*明確的*?和?*隱含的*?強制轉換之間幾乎沒有可以觀察到的結果上的不同 —— 只有代碼的可讀性至關重要。 但是ES6的Symbol在強制轉換系統中引入了一個我們需要簡單討論的坑。由于一個明顯超出了我們將在本書中討論的范圍的原因,從一個`symbol`到一個`string`的?*明確*?強制轉換是允許的,但是相同的?*隱含*?強制轉換是不被允許的,而且會拋出一個錯誤。 考慮如下代碼: ```source-js var s1 = Symbol( "cool" ); String( s1 ); // "Symbol(cool)" var s2 = Symbol( "not cool" ); s2 + ""; // TypeError ``` `symbol`值根本不能強制轉換為`number`(不論哪種方式都拋出錯誤),但奇怪的是它們既可以?*明確地*?也可以?*隱含地*?強制轉換為`boolean`(總是`true`)。 一致性總是容易學習的,而對付例外從來就不有趣,但是我們只需要在ES6`symbol`值和我們如何強制轉換它們的問題上多加小心。 好消息:你需要強制轉換一個`symbol`值的情況可能極其少見。它們典型的被使用的方式(見第三章)可能不會用到強制轉換。 ## 寬松等價與嚴格等價 寬松等價是`==`操作符,而嚴格等價是`===`操作符。兩個操作符都被用于比較兩個值的“等價性”,但是“寬松”和“嚴格”暗示著它們行為之間的一個?非常重要?的不同,特別是在它們如何決定“等價性”上。 關于這兩個操作符的一個非常常見的誤解是:“`==`檢查值的等價性,而`===`檢查值和類型的等價性。”雖然這聽起來很好很合理,但是不準確。無數知名的JavaScript書籍和文章都是這么說的,但不幸的是它們都?*錯了*。 正確的描述是:“`==`允許在等價性比較中進行強制轉換,而`===`不允許強制轉換”。 ### 等價性的性能 停下來思考一下第一種(不正確的)解釋和這第二種(正確的)解釋的不同。 在第一種解釋中,看起來`===`明顯的要比`==`*做更多工作*,因為它還必須檢查類型。在第二種解釋中,`==`是要?*做更多工作*的,因為它不得不在類型不同時走過強制轉換的步驟。 不要像許多人那樣落入陷阱中,認為這會與性能有任何關系,雖然在這個問題上`==`好像要比`===`慢一些。強制轉換確實要花費?*一點點*?處理時間,但也就是僅僅幾微秒(是的,1微秒就是一秒的百萬分之一!)。 如果你比較同類型的兩個值,`==`和`===`使用的是相同的算法,所以除了在引擎實現上的一些微小的區別,它們做的應當是相同的工作。 如果你比較兩個不同類型的值,性能也不是重要因素。你應當問自己的是:當比較這兩個值時,我想要進行強制轉換嗎? 如果你想要進行強制轉換,使用`==`寬松等價,但如果你不想進行強制轉換,就使用`===`嚴格等價。 注意:?這里暗示`==`和`===`都會檢查它們的操作數的類型。不同之處在于它們在類型不同時如何反應。 ### 抽象等價性 在ES5語言規范的11.9.3部分中,`==`操作符的行為被定義為“抽象等價性比較算法”。那里列出了一個詳盡但簡單的算法,它明確地指出了類型的每一種可能的組合,與對于每一種組合強制轉化應當如何發生(如果有必要的話)。 警告:?當(*隱含的*)強制轉換被中傷為太過復雜和缺陷過多而不能成為?*有用的,好的部分*?時,遭到譴責的正是這些“抽象等價”規則。一般上,它們被認為對于開發者來說過于復雜和不直觀而不能實際學習和應用,而且在JS程序中,和改善代碼的可讀性比起來,它傾向于導致更多的bug。我相信這是一種有缺陷的預斷 —— 讀者都是整天都在寫(而且讀,理解)算法(也就是代碼)的能干的開發者。所以,接下來的是用簡單的詞語來直白地解讀“抽象等價性”。但我懇請你也去讀一下ES5規范的11.9.3部分。我想你將會對它是多么合理而感到震驚。 基本上,它的第一個條款(11.9.3.1)是在說,如果兩個被比較的值是同一類型,它們就像你期望的那樣通過等價性簡單自然地比較。比如,`42`只和`42`相等,而`"abc"`只和`"abc"`相等。 在一般期望的結果中,有一些例外需要小心: * `NaN`永遠不等于它自己(見第二章) * `+0`和`-0`是相等的(見第二章) 條款11.9.3.1的最后一個規定是關于`object`(包括`function`和`array`)的`==`寬松相等性比較。這樣的兩個值僅在它們引用?*完全相同的值*?時?*相等*。這里沒有強制轉換發生。 注意:?`===`嚴格等價比較與11.9.3.1的定義一模一樣,包括關于兩個`object`的值的規定。很少有人知道,在兩個`object`被比較的情況下,`==`和`===`的行為相同! 11.9.3算法中的剩余部分指出,如果你使用`==`寬松等價來比較兩個不同類型的值,它們兩者或其中之一將需要被?*隱含地*?強制轉換。由于這個強制轉換,兩個值最終歸于同一類型,可以使用簡單的值的等價性來直接比較它們相等與否。 注意:?`!=`寬松不等價操作是如你預料的那樣定義的,它差不多就是`==`比較操作完整實施,之后對結果取反。這對于`!==`嚴格不等價操作也是一樣的。 #### 比較:`string`與`number` 為了展示`==`強制轉換,首先讓我們建立本章中早先的`string`和`number`的例子: ```source-js var a = 42; var b = "42"; a === b; // false a == b; // true ``` 我們所預料的,`a === b`失敗了,因為不允許強制轉換,而且值`42`和`"42"`確實是不同的。 然而,第二個比較`a == b`使用了寬松等價,這意味著如果類型偶然不同,這個比較算法將會對兩個或其中一個值實施?*隱含的*強制轉換。 那么這里發生的究竟是那種強制轉換呢?是`a`的值變成了一個`string`,還是`b`的值`"42"`變成了一個`number`? 在ES5語言規范中,條款11.9.3.4-5說: > 1. 如果Type(x)是Number而Type(y)是String, 返回比較x == ToNumber(y)的結果。 > 2. 如果Type(x)是String而Type(y)是Number, 返回比較ToNumber(x) == y的結果。 警告:?語言規范中使用`Number`和`String`作為類型的正式名稱,雖然這本書中偏好使用`number`和`string`指代基本類型。別讓語言規范中首字母大寫的`Number`與`Number()`原生函數把你給搞糊涂了。對于我們的目的來說,類型名稱的首字母大寫是無關緊要的 —— 它們基本上是同一個意思。 顯然,語言規范說為了比較,將值`"42"`強制轉換為一個`number`。這個強制轉換如何進行已經在前面將結過了,明確地說就是通過`ToNumber`抽象操作。在這種情況下十分明顯,兩個值`42`是相等的。 #### 比較:任何東西與`boolean` 當你試著將一個值直接與`true`或`false`相比較時,你會遇到`==`寬松等價的?*隱含*?強制轉換中最大的一個坑。 考慮如下代碼: ```source-js var a = "42"; var b = true; a == b; // false ``` 等一下,這里發生了什么!?我們知道`"42"`是一個truthy值(見本章早先的部分)。那么它和`true`怎么不是`==`寬松等價的? 其中的原因既簡單又刁鉆得使人迷惑。它是如此的容易讓人誤解,許多JS開發者從來不會花費足夠多的精力來完全掌握它。 讓我們再次引用語言規范,條款11.9.3.6-7 > 1. 如果Type(x)是Boolean, 返回比較 ToNumber(x) == y 的結果。 > 2. 如果Type(y)是Boolean, 返回比較 x == ToNumber(y) 的結果。 我們來把它分解。首先: ```source-js var x = true; var y = "42"; x == y; // false ``` `Type(x)`確實是`Boolean`,所以它會實施`ToNumber(x)`,將`true`強制轉換為`1`。現在,`1 == "42"`會被求值。這里面的類型依然不同,所以(實質上是遞歸地)我們再次向早先講解過的算法求解,它將`"42"`強制轉換為`42`,而`1 == 42`明顯是`false`。 反過來,我們任然得到相同的結果: ```source-js var x = "42"; var y = false; x == y; // false ``` 這次`Type(y)`是`Boolean`,所以`ToNumber(y)`給出`0`。`"42" == 0`遞歸地變為`42 == 0`,這當然是`false`。 換句話說,值`"42"`既不`== true`也不`== false`。猛地一看,這看起來像句瘋話。一個值怎么可能既不是truthy也不是falsy呢? 但這就是問題所在!你在問一個完全錯誤的問題。但這確實不是你的錯,你的大腦在耍你。 `"42"`的確是truthy,但是`"42" == true`根本就?不是在進行一個boolean測試/強制轉換,不管你的大腦怎么說,`"42"`?*沒有*?被強制轉換為一個`boolean`(`true`),而是`true`被強制轉換為一個`1`,而后`"42"`被強制轉換為`42`。 不管我們喜不喜歡,`ToBoolean`甚至都沒參與到這里,所以`"42"`的真假是與`==`操作無關的! 而有關的是要理解`==`比較算法對所有不同類型組合如何動作。當`==`的任意一邊是一個`boolean`值時,`boolean`總是首先被強制轉換為一個`number`。 如果這對你來講很奇怪,那么你不是一個人。我個人建議永遠,永遠,不要在任何情況下,使用`== true`或`== false`。永遠。 但時要記住,我在此說的僅與`==`有關。`=== true`和`=== false`不允許強制轉換,所以它們沒有`ToNumber`強制轉換,因而是安全的。 考慮如下代碼: ```source-js var a = "42"; // 不好(會失敗的!): if (a == true) { // .. } // 也不該(會失敗的!): if (a === true) { // .. } // 足夠好(隱含地工作): if (a) { // .. } // 更好(明確地工作): if (!!a) { // .. } // 也很好(明確地工作): if (Boolean( a )) { // .. } ``` 如果你在你的代碼中一直避免使用`== true`或`== false`(也就是與`boolean`的寬松等價),你將永遠不必擔心這種真/假的思維陷阱。 #### 比較:`null`與`undefined` 另一個?*隱含*?強制轉換的例子可以在`null`和`undefined`值之間的`==`寬松等價中看到。又再一次引述ES5語言規范,條款11.9.3.2-3: > 1. 如果x是null而y是undefined,返回true。 > 2. 如果x是undefined而y是null,返回true。 當使用`==`寬松等價比較`null`和`undefined`,它們是互相等價(也就是互相強制轉換)的,而且在整個語言中不會等價于其他值了。 這意味著`null`和`undefined`對于比較的目的來說,如果你使用`==`寬松等價操作符來允許它們互相?*隱含地*?強制轉換的話,它們可以被認為是不可區分的。 ```source-js var a = null; var b; a == b; // true a == null; // true b == null; // true a == false; // false b == false; // false a == ""; // false b == ""; // false a == 0; // false b == 0; // false ``` `null`和`undefined`之間的強制轉換是安全且可預見的,而且在這樣的檢查中沒有其他的值會給出測試成立的誤判。我推薦使用這種強制轉換來允許`null`和`undefined`是不可區分的,如此將它們作為相同的值對待。 比如: ```source-js var a = doSomething(); if (a == null) { // .. } ``` `a == null`檢查僅在`doSomething()`返回`null`或者`undefined`時才會通過,而在任何其他值的情況下將會失敗,即便是`0`,`false`,和`""`這樣的falsy值。 這個檢查的?*明確*?形式 —— 不允許任何強制轉換 —— (我認為)沒有必要地難看太多了(而且性能可能有點兒不好!): ```source-js var a = doSomething(); if (a === undefined || a === null) { // .. } ``` 在我看來,`a == null`的形式是另一個用?*隱含*?強制轉換增進了代碼可讀性的例子,而且是以一種可靠安全的方式。 #### 比較:`object`與非`object` 如果一個`object`/`function`/`array`被與一個簡單基本標量(`string`,`number`,或`boolean`)進行比較,ES5語言規范在條款11.9.3.8-9中這樣說道: > 1. 如果Type(x)是一個String或者Number而Type(y)是一個Object, 返回比較 x == ToPrimitive(y) 的結果。 > 2. 如果Type(x)是一個Object而Type(y)是String或者Number, 返回比較 ToPrimitive(x) == y 的結果。 注意:?你可能注意到了,這些條款僅提到了`String`和`Number`,而沒有`Boolean`。這是因為,正如我們早先引述的,條款11.9.3.6-7首先將任何出現的`Boolean`操作數強制轉換為一個`Number`。 考慮如下代碼: ```source-js var a = 42; var b = [ 42 ]; a == b; // true ``` 值`[ 42 ]`的`ToPrimitive`抽象操作(見先前的“抽象值操作”部分)被調用,結果為值`"42"`。這里它就變為`42 == "42"`,我們已經講解過這將變為`42 == 42`,所以`a`和`b`被認為是強制轉換地等價。 提示:?我們在本章早先討論過的`ToPrimitive`抽象操作的所以奇怪之處(`toString()`,`valueOf()`),都在這里如你期望的那樣適用。如果你有一個復雜的數據結構,而且你想在它上面定義一個`valueOf()`方法來為等價比較提供一個簡單值的話,這將十分有用。 在第三章中,我們講解了“拆箱”,就是一個基本類型值的`object`包裝器(例如`new String("abc")`這樣的形式)被展開,其底層的基本類型值(`"abc"`)被返回。這種行為與`==`算法中的`ToPrimitive`強制轉換有關: ```source-js var a = "abc"; var b = Object( a ); // 與`new String( a )`相同 a === b; // false a == b; // true ``` `a == b`為`true`是因為`b`通過`ToPrimitive`強制轉換為它的底層簡單基本標量值`"abc"`,它與`a`中的值是相同的。 然而由于`==`算法中的其他覆蓋規則,有些值是例外。考慮如下代碼: ```source-js var a = null; var b = Object( a ); // 與`Object()`相同 a == b; // false var c = undefined; var d = Object( c ); // 與`Object()`相同 c == d; // false var e = NaN; var f = Object( e ); // 與`new Number( e )`相同 e == f; // false ``` 值`null`和`undefined`不能被裝箱 —— 它們沒有等價的對象包裝器 —— 所以`Object(null)`就像`Object()`一樣,它們都僅僅產生一個普通對象。 `NaN`可以被封箱到它等價的`Number`對象包裝器中,當`==`導致拆箱時,比較`NaN == NaN`會失敗,因為`NaN`永遠不會它自己相等(見第二章)。 ### 邊界情況 現在我們已經徹底檢視了`==`寬松等價的?*隱含*?強制轉換是如何工作的(從合理與驚訝兩個方式),讓我們召喚角落中最差勁兒的,最瘋狂的情況,這樣我們就能看到我們需要避免什么來防止被強制轉換的bug咬到。 首先,讓我們檢視修改內建的原生prototype是如何產生瘋狂的結果的: #### 一個擁有其他值的數字將會…… ```source-js Number.prototype.valueOf = function() { return 3; }; new Number( 2 ) == 3; // true ``` 警告:?`2 == 3`不會掉到這個陷阱中,這是由于`2`和`3`都不會調用內建的`Number.prototype.valueOf()`方法,因為它們已經是基本`number`值,可以直接比較。然而,`new Number(2)`必須通過`ToPrimitive`強制轉換,因此調用`valueOf()`。 邪惡吧?當然。任何人都不應當做這樣的事情。你?*可以*?這么做,這個事實有時被當成批評強制轉換和`==`的根據。但這種沮喪是被誤導的。JavaScript不會因為你能做這樣的事情而?*不好*,是?做這樣的事的開發者?*不好*。不要陷入“我的編程語言應當保護我不受我自己傷害”的謬論。 接下來,讓我們考慮另一個刁鉆的例子,它將前一個例子的邪惡帶到另一個水平: ```source-js if (a == 2 && a == 3) { // .. } ``` 你可能認為這是不可能的,因為`a`絕不會?*同時*?等于`2`和`3`。但是“同時”是不準確的,因為第一個表達式`a == 2`嚴格地發生在`a == 3`?*之前*。 那么,要是我們讓`a.valueOf()`在每次被調用時擁有一種副作用,使它第一次被調用時返回`2`而第二次被調用時返回`3`呢?很簡單: ```source-js var i = 2; Number.prototype.valueOf = function() { return i++; }; var a = new Number( 42 ); if (a == 2 && a == 3) { console.log( "Yep, this happened." ); } ``` 重申一次,這些都是邪惡的技巧。不要這么做。也不要用它們來抱怨強制轉換。潛在地濫用一種機制并不是譴責這種機制的充分證據。避開這些瘋狂的技巧,并堅持強制轉換的合法與合理的用法就好了。 #### False-y 比較 關于`==`比較中?*隱含*?強制轉換的最常見的抱怨,來自于falsy值互相比較時它們如何令人吃驚地動作。 為了展示,讓我們看一個關于falsy值比較的極端例子的列表,來瞧瞧哪一個是合理的,哪一個是麻煩的: ```source-js "0" == null; // false "0" == undefined; // false "0" == false; // true -- 噢! "0" == NaN; // false "0" == 0; // true "0" == ""; // false false == null; // false false == undefined; // false false == NaN; // false false == 0; // true -- 噢! false == ""; // true -- 噢! false == []; // true -- 噢! false == {}; // false "" == null; // false "" == undefined; // false "" == NaN; // false "" == 0; // true -- 噢! "" == []; // true -- 噢! "" == {}; // false 0 == null; // false 0 == undefined; // false 0 == NaN; // false 0 == []; // true -- 噢! 0 == {}; // false ``` 在這24個比較的類表中,17個是十分合理和可預見的。比如,我們知道`""`和`"NaN"`是根本不可能相等的值,并且它們確實不會強制轉換以成為寬松等價的,而`"0"`和`0`是合理等價的,而且確實強制轉換為寬松等價。 然而,這些比較中的7個被標上了“噢!”。作為誤判的成立,它們更像是會將你陷進去的坑。`""`和`0`絕對是有區別的不同的值,而且你很少會將它們作為等價的,所以它們的互相強制轉換是一種麻煩。注意這里沒有任何誤判的不成立。 #### 瘋狂的情況 但是我們不必停留在此。我們可以繼續尋找更能引起麻煩的強制轉換: ```source-js [] == ![]; // true ``` 噢,這看起來像是更高層次的瘋狂,對吧!?你的大腦可能會欺騙你說,你在將一個truthy和falsy值比較,所以結果`true`是令人吃驚的,因為我們知道一個值不可能同時為truthy和falsy! 但這不是實際發生的事情。讓我們把它分解一下。我們了解`!`一元操作符吧?它明確地使用`ToBoolean`規則將操作數強制轉換為一個`boolean`(而且它還會翻轉真假性)。所以在`[] == ![]`執行之前,它實際上已經被翻譯為了`[] == false`。我們已將在上面的列表中見過了這種形式(`false == []`),所以它的令人吃驚的結果對我們來說并不?*新鮮*。 其它的極端情況呢? ```source-js 2 == [2]; // true "" == [null]; // true ``` 在關于`ToNumber`的討論中我們說過,右手邊的`[2]`和`[null]`值將會通過一個`ToPrimitive`強制轉換,以使我們可以方便地與左手邊的簡單基本類型值進行比較。因為`array`值的`valueOf()`只是返回`array`本身,強制轉換會退到`array`的字符串化上。 對于第一個比較的右手邊的值來說,`[2]`將變為`"2"`,然后它會`ToNumber`強制轉換為`2`。`[null]`就直接變成`""`。 那么,`2 == 2`和`"" == ""`是完全可以理解的。 如果你的直覺依然不喜歡這個結果,那么你的沮喪實際上與你可能認為的強制轉換無關。這其實是在抱怨`array`值在強制轉換為`string`值時的默認`ToPrimitive`行為。很可能,你只是希望`[2].toString()`不返回`"2"`,或者`[null].toString()`不返回`""`。 但是這些`string`強制轉換到底?*應該*?得出什么結果?對于`[2]`的`string`強制轉換,除了`"2"`我確實想不出來其他合適的結果,也許是`"[2]"`?—— 但這可能會在其他的上下文中很奇怪! 你可以正確地制造另一個例子:因為`String(null)`變成了`"null"`,那么`String([null])`也應當變成`"null"`。這是個合理的斷言。所以,它才是真正的犯人。 *隱含*?強制轉換在這里并不邪惡。即使一個從`[null]`到`string`結果為`""`的?*明確*?強制轉換也不。真正奇怪的是,`array`值字符串化為它們內容的等價物是否有道理,和它是如何發生的。所以,應當將你沮喪的原因指向`String( [..] )`的規則,因為這里才是瘋狂起源的地方。也許根本就不應該有`array`的字符串化強制轉換?但這會在語言的其他部分造成許多的缺點。 另一個常被引用的著名的坑是: ```source-js 0 == "\n"; // true ``` 正如我們早先討論的空`""`,`"\n"`(或`" "`,或其他任何空格的組合)是通過`ToNumber`強制轉換的,而且結果為`0`。你還希望空格被轉換為其他的什么`number`值呢?*明確的*?`Number()`給出`0`會困擾你嗎? 空字符串和空格字符串可以轉換為的,另一個真正唯一合理的`number`值是`NaN`。但這?*真的*?會更好嗎?`" " == NaN`的比較當然會失敗,但是不清楚我們是否真的?*修正*?了任何底層的問題。 真實世界中的JS程序由于`0 == "\n"`而失敗的幾率非常之低,而且這樣的極端用例很容比避免。 在任何語言中,類型轉換?總是?有極端用例 —— 強制轉換也不例外。這里討論的是特定的一組極端用例的馬后炮,但不是針對強制轉換整體而言的爭論。 底線:你可能遇到的幾乎所有?*普通值*?間的瘋狂強制轉換(除了像早先那樣有意而為的`valueOf()`或`toString()`黑科技),都能歸結為我們在上面指出的7中情況的短列表。 對比這24個疑似強制轉換的坑,考慮另一個像這樣的列表: ```source-js 42 == "43"; // false "foo" == 42; // false "true" == true; // false 42 == "42"; // true "foo" == [ "foo" ]; // true ``` 在這些非falsy,非極端的用例中(而且我們簡直可以向這個列表中添加無限多個比較),強制轉換完全是安全,合理,和可解釋的。 #### 可行性檢查 好的,當我們深入觀察?*隱含的*?強制轉換時,我確實找到了一些瘋狂的東西。難怪大多數開發者聲稱強制轉換是邪惡而且應該避開的,對吧? 但是讓我們退一步并做一下可行性檢查。 通過大量比較,我們得到了一張7個麻煩的,坑人的強制轉換的列表,但我們還得到了另一張(至少17個,但實際上有無限多個)完全正常和可以解釋的強制轉換的列表。 如果你在尋找一本“把孩子和洗澡水一起潑出去”的教科書,這就是了:由于一個僅有7個坑的列表,而拋棄整個強制轉換(安全且有效的行為的無限大列表)。 一個更謹慎的反應是問,“我如何使用強制轉換的?*好的部分*,而避開這幾個?*壞的部分*?呢?” 讓我們再看一次這個?*壞*?列表: ```source-js "0" == false; // true -- 噢! false == 0; // true -- 噢! false == ""; // true -- 噢! false == []; // true -- 噢! "" == 0; // true -- 噢! "" == []; // true -- 噢! 0 == []; // true -- 噢! ``` 這個列表中7個項目的4個與`== false`比較有關,我們早先說過你應當?總是,總是?避免的。 現在這個列表縮小到了3個項目。 ```source-js "" == 0; // true -- 噢! "" == []; // true -- 噢! 0 == []; // true -- 噢! ``` 這些是你在一般的JavaScript程序中使用的合理的強制轉換嗎?在什么條件下它們會發生? 我不認為你在程序里有很大的可能要在一個`boolean`測試中使用`== []`,至少在你知道自己在做什么的情況下。你可能會使用`== ""`或`== 0`,比如: ```source-js function doSomething(a) { if (a == "") { // .. } } ``` 如果你偶然調用了`doSomething(0)`或`doSomething([])`,你就會嚇一跳。另一個例子: ```source-js function doSomething(a,b) { if (a == b) { // .. } } ``` 再一次,如果你調用`doSomething("",0)`或`doSomething([],"")`時,它們會失敗。 所以,雖然這些強制轉換會咬到你的情況?*可能*?存在,而且你會小心地處理它們,但是它們可能不會在你的代碼庫中超級常見。 #### 安全地使用隱含強制轉換 我能給你的最重要的建議是:檢查你的程序,并推理什么樣的值會出現在`==`比較兩邊。為了避免這樣的比較中的問題,這里有一些可以遵循的啟發性規則: 1. 如果比較的任意一邊可能出現`true`或者`false`值,那么就永遠,永遠不要使用`==`。 2. 如果比較的任意一邊可能出現`[]`,`""`,或`0`這些值,那么認真地考慮不使用`==`。 在這些場景中,為了避免不希望的強制轉換,幾乎可以確定使用`===`要比使用`==`好。遵循這兩個簡單的規則,可以有效地避免幾乎所有可能會傷害你的強制轉換的坑。 在這些情況下,使用更加明確/繁冗的方式會減少很多使你頭疼的東西。 `==`與`===`的問題其實可以更加恰當地表述為:你是否應當在比較中允許強制轉換? 在許多情況下這樣的強制轉換會很有用,允許你更簡練地表述一些比較邏輯(例如,`null`和`undefined`)。 對于整體來說,相對有幾個?*隱含*?強制轉換會真的很危險的情況。但是在這些地方,為了安全起見,絕對要使用`===`。 提示:?另一個強制轉換保證?*不會*?咬到你的地方是`typeof`操作符。`typeof`總是將返回給你7中字符串之一(見第一章),它們中沒有一個是空`""`字符串。這樣,檢查某個值的類型時不會有任何情況與?*隱含*?強制轉換相沖突。`typeof x == "function"`就像`typeof x === "function"`一樣100%安全可靠。從字面意義上將,語言規范說這種情況下它們的算法是相同的。所以,不要只是因為你的代碼工具告訴你這么做,或者(最差勁兒的)在某本書中有人告訴你?不要考慮它,而盲目地到處使用`===`。你掌管著你的代碼的質量。 *隱含*?強制轉換是邪惡和危險的嗎?在幾個情況下,是的,但總體說來,不是。 做一個負責任和成熟的開發者。學習如何有效并安全地使用強制轉換(*明確的*?和?*隱含的*?兩者)的力量。并教你周圍的人也這么做。 這里是由Alex Dorey (@dorey on GitHub)制作的一個方便的表格,將各種比較進行了可視化: [![](https://github.com/getify/You-Dont-Know-JS/raw/1ed-zh-CN/types%20%26%20grammar/fig1.png)](https://github.com/getify/You-Dont-Know-JS/blob/1ed-zh-CN/types%20%26%20grammar/fig1.png) 出處:[https://github.com/dorey/JavaScript-Equality-Table](https://github.com/dorey/JavaScript-Equality-Table) ## 抽象關系比較 雖然這部分的?*隱含*?強制轉換經常不為人所注意,但無論如何考慮比較`a < b`時發生了什么是很重要的(和我們如何深入檢視`a == b`類似)。 在ES5語言規范的11.8.5部分的“抽象關系型比較”算法,實質上把自己分成了兩個部分:如果比較涉及兩個`string`值要做什么(后半部分),和除此之外的其他值要做什么(前半部分)。 注意:?這個算法僅僅定義了`a < b`。所以,`a > b`作為`b < a`處理。 這個算法首先在兩個值上調用`ToPrimitive`強制轉換,如果兩個調用的返回值之一不是`string`,那么就使用`ToNumber`操作規則將這兩個值強制轉換為`number`值,并進行數字的比較。 舉例來說: ```source-js var a = [ 42 ]; var b = [ "43" ]; a < b; // true b < a; // false ``` 注意:?早先討論的關于`-0`和`NaN`在`==`算法中的類似注意事項也適用于這里。 然而,如果`<`比較的兩個值都是`string`的話,就會在字符上進行簡單的字典順序(自然的字母順序)比較: ```source-js var a = [ "42" ]; var b = [ "043" ]; a < b; // false ``` `a`和`b`?*不會*?被強制轉換為`number`,因為它們會在兩個`array`的`ToPrimitive`強制轉換后成為`string`。所以,`"42"`將會與`"043"`一個字符一個字符地進行比較,從第一個字符開始,分別是`"4"`和`"0"`。因為`"0"`在字典順序上?*小于*?`"4"`,所以這個比較返回`false`。 完全相同的行為和推理也適用于: ```source-js var a = [ 4, 2 ]; var b = [ 0, 4, 3 ]; a < b; // false ``` 這里,`a`變成了`"4,2"`而`b`變成了`"0,4,3"`,而字典順序比較和前一個代碼段一模一樣。 那么這個怎么樣: ```source-js var a = { b: 42 }; var b = { b: 43 }; a < b; // ?? ``` `a < b`也是`false`,因為`a`變成了`[object Object]`而`b`變成了`[object Object]`,所以明顯地`a`在字典順序上不小于`b`。 但奇怪的是: ```source-js var a = { b: 42 }; var b = { b: 43 }; a < b; // false a == b; // false a > b; // false a <= b; // true a >= b; // true ``` 為什么`a == b`不是`true`?它們是相同的`string`值(`"[object Object]"`),所以看起來它們應當相等,對吧?不。回憶一下前面關于`==`如何與`object`引用進行工作的討論。 那么為什么`a <= b`和`a >= b`的結果為`true`,如果`a < b`和`a == b`和`a > b`都是`false`? 因為語言規范說,對于`a <= b`,它實際上首先對`b < a`求值,然后反轉那個結果。因為`b < a`*也是*`false`,所以`a <= b`的結果為`true`。 到目前為止你解釋`<=`在做什么的方式可能是:“小于?*或*?等于”。而這可能完全相反,JS更準確地將`<=`考慮為“不大于”(`!(a > b)`,JS將它作為`(!b < a)`)。另外,`a >= b`被解釋為它首先被考慮為`b <= a`,然后實施相同的推理。 不幸的是,沒有像等價那樣的“嚴格的關系型比較”。換句話說,沒有辦法防止`a < b`這樣的關系型比較發生?*隱含的*?強制轉換,除非在進行比較之前就明確地確保`a`和`b`是同種類型。 使用與我們早先`==`與`===`合理性檢查的討論相同的推理方法。如果強制轉換有幫助并且合理安全,比如比較`42 < "43"`,就使用它。另一方面,如果你需要在關系型比較上獲得安全性,那么在使用`<`(或`>`)之前,就首先?*明確地強制轉換*?這些值。 ```source-js var a = [ 42 ]; var b = "043"; a < b; // false -- 字符串比較! Number( a ) < Number( b ); // true -- 數字比較! ``` ## 復習 在這一章中,我們將注意力轉向了JavaScript類型轉換如何發生,也叫?強制轉換,按性質來說它要么是?*明確的*?要么是?*隱含的*。 強制轉換的名聲很壞,但它實際上在許多情況下很有幫助。對于負責任的JS開發者來說,一個重要的任務就是花時間去學習強制轉換的里里外外,來決定哪一部分將幫助他們改進代碼,哪一部分他們真的應該回避。 *明確的*?強制轉換時這樣一種代碼,它很明顯地有意將一個值從一種類型轉換到另一種類型。它的益處是通過減少困惑來增強了代碼的可讀性和可維護性。 *隱含的*?強制轉換是作為一些其他操作的“隱藏的”副作用而存在的,將要發生的類型轉換并不明顯。雖然看起來?*隱含的*?強制轉換是?*明確的*?反面,而且因此是不好的(確實,很多人這么認為!),但是實際上?*隱含的*?強制轉換也是為了增強代碼的可讀性。 特別是對于?*隱含的*,強制轉換必須被負責地,有意識地使用。懂得為什么你在寫你正在寫的代碼,和它是如何工作的。同時也要努力編寫其他人容易學習和理解的代碼。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看