本章旨在全面介紹強制類型轉換的優缺點。
1.值類型轉換
??????將值從一種類型轉換為另一種類型通常稱為類型轉換,這是顯式的情況;隱式的情況稱為強制類型轉換。
??????JavaScript中的強制類型轉換總是返回標量基本類型值,不會返回對象和函數。
??????可以這樣分別稱呼:顯式強制類型轉換、隱式強制類型轉換。(這里的顯/隱區分不是官方定的,而是根據一般的開發人員的感受而做的區分)
2.抽象值操作
??????本節介紹各種基本類型之間轉換的規則,主要介紹toString、toNumber、toBoolean,并捎帶講下toPrimitive。
2.1 toString
??????null -> 'null'
??????undefined -> 'undefined'
??????true -> 'true'
??????數字的轉換遵循通用規則,其中極大和極小的數字使用指數形式。
??????對普通對象,則調用對象的toString(Object.prototype.toString()),返回內部屬性[[Class]]的值,這個方法也可以自己定義。
??????數組的toString就是Array類里重新定義過的,跟Object.prototype.toString()不一樣。
下面講下JSON.stringify
??????對大多數基本類型值來說,JSON.stringify和toString的效果基本相同。
??????安全的JSON值指的是能夠呈現為有效JSON格式的值。
??????那么什么是不安全的JSON值? undefined、function、symbol和包含循環引用的對象 都不是安全的JSON值。
??????JSON.stringify()在對象中遇到undefined、function和symbol時會自動將其忽略,在數組中則會返回null(以保證單元位置不變)。而對包含循環引用的對象執行JSON.stringify()會出錯。
??????如果要對含有非法JSON值的對象做字符串化,就需要定義toJSON()方法來返回一個安全的JSON值。這個toJSON方法應該返回一個“能夠被字符串化的安全的JSON值”。
JSON.stringify的使用小妙招:
??????JSON.stringify(..)接收一個可選參數replacer,它可以是數組或者函數,用來指定對象序列化過程中哪些屬性應該被處理,哪些應該被排除。如果replacer是一個數組,那么它必須是一個字符串數組,其中包含序列化要處理的對象的屬性名稱,除此之外其他的屬性則被忽略;如果replacer是一個函數,它會對對象本身調用一次,然后對對象中的每個屬性各調用一次,每次傳遞兩個參數,鍵和值。如果要忽略某個鍵就返回undefined,否則返回指定的值。
??????JSON.stringify(..)的字符串化過程是遞歸的,遞歸的過程中會多次調用replacer函數(如果有的話),可以自己試著打印一下看看遞歸的順序。
??????JSON.stringify還有一個可選參數space,用來指定輸出的縮進格式。space為正整數時是指定每一級縮進的字符數,它還可以是字符串,此時最前面的十個字符被用于每一級的縮進。(后半句話沒懂是什么意思。)
??????mdn對space是這么解釋的:
??????A String or Number object that's used to insert white space into the output JSON string for readability purposes.
??????If this is a Number, it indicates the number of space characters to use as white space; this number is capped at 10 (if it is greater, the value is just 10). Values less than 1 indicate that no space should be used.
??????If this is a String, the string (or the first 10 characters of the string, if it's longer than that) is used as white space. If this parameter is not provided (or is null), no white space is used.
??????我覺得比書上的中文說得清楚。
2.2 toNumber
??????true -> 1
??????false -> 0
??????undefined -> NaN
??????null -> 0
??????ToNumber對字符串的處理基本遵循數字常量的相關規則。處理失敗時返回NaN。不同之處是ToNumber對以0開頭的十六進制數并不按十六進制處理(而是按十進制,參見第2章)。
??????(↑這句沒看懂,Number('0x011')也成功轉成了17,為什么作者要說“并不按十六進制處理呢?)
??????對象(包括數組)會首先被轉換為相應的基本類型值,如果返回的是非數字的基本類型值,則再遵循以上規則將其強制轉換為數字。
??????對象轉換為基本類型值的過程:首先檢查該值是否有valueOf()方法,如果有并且返回基本類型值,就使用該值進行強制類型轉換;如果沒有就使用toString()的返回值(如果存在)來進行強制類型轉換。
2.3 toBoolean
??????JavaScript規范具體定義了一小撮可以被強制類型轉換為false的值:undefined、null、false、+0、-0、NaN、空字符串。
??????我們可以理解為假值列表以外的值都是真值。
??????(ie瀏覽器有個神妙的假值對象document.all,它為什么會是個假值的原因,反正是歷史原因。)
3.顯式強制類型轉換
??????顯式強制類型轉換是那些顯而易見的類型轉換。我們在編碼時應盡可能地將類型轉換表達清楚,以免給別人留坑。類型轉換越清晰,代碼可讀性越高,更容易理解。
3.1 字符串和數字之間的顯式轉換
??????tips: JavaScript有一處奇特的語法,即構造函數沒有參數時可以不用帶()
??????涉及字位運算符的強制轉換
??????前面提過,字位運算符只適用于32位整數,所以運算符會強制操作數使用32位格式。這是通過抽象操作ToInt32來實現的,比如"123"會先被ToNumber轉換為123,然后再執行ToInt32。
??????雖然嚴格說來并非強制類型轉換(因為返回值類型并沒有發生變化),但字位運算符(如|和~)和某些特殊數字一起使用時會產生類似強制類型轉換的效果,返回另外一個數字。
??????例:
??????0 | -0 // 0
??????0 | NaN // 0
??????0 | Infinity // 0
??????0 | -Infinity // 0
??????以上這些特殊數字無法以32位格式呈現(因為它們來自64位IEEE 754標準,參見第2章),因此ToInt32返回0。
??????然后作者說到~的用法
??????很多語言中會有“哨位值”,用來表示特殊的含義,比如JavaScript中的indexOf()用-1表示沒有搜索到指定子串。對于這樣的方法如果我們直接用>=0或者===-1這樣的判斷,就是把方法的實現細節暴露出來了,而用~計算indexOf()的結果,剛好~-1
為0,是假值,其它結果是真值。
??????作者認為if (~a.indexOf(..)
這樣的判斷比>=0或者===-1更簡潔。
??????然后作者開始討論~~
??????~~
中的第一個~
執行ToInt32并反轉字位,然后第二個~
再進行一次字位反轉,即將所有字位反轉回原值,最后得到的仍然是ToInt32的結果(只適用于32位數字)。
??????~~x能將值截除為一個32位整數,但它對負數的處理與Math. floor(..)不同。
??????Math.floor(-49.6) // -50
??????~~-49.6 // 49
3.2 顯式解析數字字符串
??????強制轉換方法Number() 和 解析方法parseInt()、parseFloat()的區別:解析允許字符串中含有非數字字符,解析按從左到右的順序,如果遇到非數字字符就停止。而轉換不允許出現非數字字符,否則會失敗并返回NaN。
??????parseInt(..)針對的是字符串值,非字符串參數會首先被強制類型轉換為字符串。依賴這樣的隱式強制類型轉換并非上策,應該避免向parseInt(..)傳遞非字符串參數。
??????parseInt(..)第二個參數可以指定轉換的基數。如果沒有傳第二個參數,ES5前parseInt(..)會根據字符串的第一個字符來自行決定基數,從ES5開始parseInt(..)則默認轉換為十進制數(除非0x開頭)。
??????如果使用不當,parseInt會出現一些難以理解的結果,但其實并沒毛病。
??????比如:parseInt(1/0, 19)
,實際上是parseInt("Infinity", 19)
。第一個字符是"I",以19為基數時值為18。所以最后的結果是18,而非Infinity或者報錯。
??????還有一些例子:
??????parseInt(0.000008) // 0 (0來自于"0.000008")
??????parseInt(0.0000008) // 8 (8來自于"8e-7")
??????parseInt(false, 16) // 250 ("fa"來自于"false")
??????parseInt(parseInt, 16) // 15 ("f"來自于"function..")
??????parseInt("0x10") // 16
??????parseInt("103", 2) // 2
3.3 顯式轉換為布爾值
??????Boolean(..)是顯式的ToBoolean強制類型轉換,還有種寫法是!!
4.隱式強制類型轉換
4.1 隱式的簡化
??????隱式強制類型轉換 相當于 省略了一些轉換的代碼,讓轉換的環節看起來變少了,一些中間環節被隱藏掉了,代碼看起來更簡潔。
4.2 字符串和數字之間的隱式強制類型轉換
??????前面講過+運算符可以把字符串轉成數字,然后可以進行數字加法,+運算符也可以用于字符串拼接。那么JavaScript怎么決定最后執行什么操作?
??????舉幾個例子:
??????'42' + '0' // 420
??????42 + 0 // 42
??????[1, 2] + [3, 4] // 1,23,4
??????ES5規定,如果某個操作數是字符串的話,+將進行拼接操作,否則執行數字加法。
如果操作數是對象,它會先被調用valueOf,獲取值,如果valueOf得不到基本類型值,就會調用它的toString。
??????再說個神奇的例子:
??????[] + {} // [object Object]
??????{} + [] // 0
??????后面再分析
??????然后討論從字符串強制轉換為數字的情況
??????比如說用-運算符:
??????[3] - [2] // 1
??????[3, 4] - [2] // NaN
??????除了-運算符,*和/也會將操作數強制轉換為數字。
??????我自己在瀏覽器控制臺試了下
??????{} - 1 // -1
??????[] - 1 // -1
??????唉,也不知道作何解釋。
4.3 布爾值到數字的隱式強制類型轉換
??????可以對布爾值用數學運算符,它會被隱式轉換成數字0或1。有的時候這樣是有用的。
4.4 隱式強制類型轉換為布爾值
??????會發生布爾值隱式強制類型轉換的情況:
??????(1) if (..)語句中的條件判斷表達式
??????(2) for ( .. ; .. ; .. )語句中的條件判斷表達式
??????(3) while (..)和do..while(..)循環中的條件判斷表達式。
??????(4) ? :三目運算中的條件判斷表達式
??????(5) 邏輯運算符||(邏輯或)和&&(邏輯與)左邊的操作數(作為條件判斷表達式)
4.5 ||和&&
??????在JavaScript中||和&&返回的并不是布爾值,而是兩個操作數中的一個(且僅一個)。
??????||和&&首先會對第一個操作數(a和c)執行條件判斷,第一個操作數如果不是布爾值,會被強制類型轉換成布爾值。
??????對于||來說,如果條件判斷結果為true就返回第一個操作數的值,如果為false就返回第二個操作數的值。&&則相反。
??????a || b 大致相當于 a ? a : b
??????a && b 大致相當于 a ? b : a
??????之所以說大致相當,是因為如果a是一個復雜一些的表達式,用?:的寫法它可能被執行兩次,而在a || b和a && b中a只執行一次。
??????||常用來設默認值。
??????&&常被代碼壓縮工具用來壓縮代碼,如if (a) { foo(); }會被壓縮成a && foo()。
4.6 符號的強制類型轉換
??????symbol不能被強制類型轉換為數字(顯式隱式都不行)
??????symbol可以被強制類型轉換為布爾值(顯式和隱式結果都是true)
??????symbol可以被顯式強制類型轉換為字符串,但不能隱式轉換成字符串。
??????例:
var s1 = Symbol('cool')
String(s1) // 'Symbol(cool)'
s1 + '' // 會報錯 Uncaught TypeError: Cannot convert a Symbol value to a string
5.寬松相等==和嚴格相等===
??????==允許在相等比較中進行強制類型轉換,而===不允許。
5.1 兩者的性能
??????如果比較的兩個值類型不同,==會先進行強制類型轉換。如果兩個值類型相同,則==和===使用相同的算法。
??????性能上兩者幾乎沒有差別,使用時,只需要考慮有沒有強制類型轉換的必要,有就用==,沒有就用===,不用在乎性能。
5.2 抽象相等
??????es5規范規定了“抽象相等比較算法”定義了==運算符的行為。
??????它規定:
??????(1)如果兩個值的類型相同,就僅比較它們是否相等。(注意特例:NaN不等于NaN;+0等于-0)
??????(2)兩個對象之間比較,如果兩個對象指向同一個值時即視為相等。
??????(3)兩個值類型不相同,會發生隱式強制類型轉換,強制類型轉換可能會發生在其中一個值,也可能兩個都被轉換,之后再被進行比較。
??????下面進行更具體的分情況討論:
??????①字符串和數字之間的相等比較
??????前面說過,==的兩個比較值的類型不一致時,會先進行強制類型轉換。那么轉換哪個呢?
??????es5規范規定:如果Type(x)是數字,Type(y)是字符串,則返回x == ToNumber(y)的結果;如果Type(x)是字符串,Type(y)是數字,則返回ToNumber(x) == y的結果。
??????②其他類型和布爾類型之間的相等比較
?????? es5規范規定:如果Type(x)是布爾類型,則返回ToNumber(x) == y的結果;如果Type(y)是布爾類型,則返回x == ToNumber(y)的結果。
??????所以true、false這樣的比較值會被轉成1和0再進行比較。
??????③null和undefined之間的相等比較
??????es5規范規定:如果x為null, y為undefined,則結果為true;如果x為undefined, y為null,則結果為true。
??????在==中null和undefined相等(它們也與其自身相等),除此之外其他值都不存在這種情況。
?????? 比方說null == ''、null == false、null == 0的判斷結果都是false
?????? 根據上述規則,在開發中如果要判斷一個值是否為null或undefined,就沒必要寫成a === null || a === undefined,直接寫成a == null就行,還更簡潔。
??????④對象和非對象之間的相等比較
??????es5規范規定:如果Type(x)是字符串或數字,Type(y)是對象,則返回x == ToPrimitive(y)的結果;如果Type(x)是對象,Type(y)是字符串或數字,則返回ToPrimitive(x) == y的結果。
??????舉例:42 == [42],對象[42]會先被調用toPrimitive,得到"42",然后"42" == 42又被強制類型轉換了一次變成42 == 42,判斷結果為true。
??????Number對象、String對象(平時我們不怎么顯式地去用)存在“拆封”,這個拆封的過程也會調用對象的toPrimitive。
??????舉例:"abc" === new String("abc") // false
??????"abc" == new String("abc") // true
5.3 奇葩情況集錦
??????①給對象定義了奇葩的valueOf方法,然后就能看到各種壯觀的奇葩事情。
??????②各種假值的相等比較
??????由于強制類型轉換的原因,所以假值之間比較可能會有一些看起來難以理解的結果,比如[] == ''為真、[] == false為真,這都是因為強制類型轉換,想想Number('')為0、[].toString()為''就知道了。
??????③[] == ![]為真
??????原因是這樣的,![]首先把[]轉成布爾類型值,得true,然后前面那個!把它轉成false,然后[] == false為真,因為它們都被轉成數字了。
??????對上述奇葩情況做總結,首先不要拿布爾值做==判斷,布爾值會被轉成數字的,然后就會看起來很靈異,咱犯不上那樣,布爾值就不要拿來==了。其次,兩邊的值有[]、''或者0的,盡量不要用==,這樣就能避開幾乎所有奇奇怪怪的強制類型轉換行為了。
??????作者小tips:對于typeof,使用==是安全的,因為typeof總是返回七個字符串之一,其中沒有空字符串。所以在類型檢查過程中不會發生隱式強制類型轉換,typeof x == "function"是安全的。所以代碼中按需選擇==和===即可,沒必要處處用===。
6.抽象關系比較
??????這一小節討論a < b中涉及的隱式強制類型轉換。
??????ES5規范定義了“抽象關系比較”(abstract relational comparison),分為兩個部分:比較雙方都是字符串和其他情況。
??????①比較雙方首先調用ToPrimitive,如果結果出現非字符串,就根據ToNumber規則將雙方強制類型轉換為數字來進行比較。
??????②如果比較雙方都是字符串,則按字母順序來進行比較。
??????奇怪的例子:
??????{a: 1} <= {a: 2} // true
??????{a: 1} >= {a: 2} // true
??????這是為什么呢?因為根據規范,a <= b被處理為b < a,然后將結果反轉。而{a: 1}和{a: 2}的toPrimitive都是"[object Object]",a < b為false,a > b也為false。
??????我們可能以為<=應該是“小于或者等于”,但其實在JavaScript中<=是“不大于”的意思(即!(a > b),處理為!(b < a))。同理,a >= b處理為b <= a。
??????如果要避免a < b中發生隱式強制類型轉換,我們只能確保a和b為相同的類型,除此之外別無他法。