做爲 JavaScript 程序員,你必定獲取過當前系統的時間戳。在 ES5 引入 Date.now()
靜態方法以前,下面這段代碼你必定不會陌生:javascript
var timestamp = +new Date(); // timestamp 就是當前的系統時間戳,單位是 ms
你確定據說過 JavaScript 的強制類型轉換,你能指出這段代碼裏哪裏用到了強制類型轉換嗎?java
幾乎全部 JavaScript 程序員都接觸過強制類型轉換 —— 不管是有意的仍是無心的。強制類型轉換致使了不少隱蔽的 BUG,可是強制類型轉換同時也是一種很是有用的技術,咱們不該該因噎廢食。程序員
在本文中咱們來詳細探討一下 JavaScript 的強制類型轉換,以便咱們能夠在避免踩坑的狀況下最大化利用強制類型轉換的便捷。數組
類型轉換髮生在靜態類型語言的編譯階段,而強制類型轉換髮生在動態類型語言的運行時(runtime),所以在 JavaScript 中只有強制類型轉換。瀏覽器
強制類型轉換通常還可分爲 隱式強制類型轉換(implicit coercion)和 _顯式強制類型轉換(explicit coercion)_。函數
從代碼中能夠看出轉換操做是隱式的仍是顯式的,顯式強制類型轉換很容易就能看出來,而隱式強制類型轉換可能就沒有這麼明顯了。工具
好比:this
var a = 21; var b = a + ''; var c = String(a);
對於變量 b
而言,這次強制類型轉換是隱式的。+
操做符在其中一個操做數是字符串時進行的是字符串拼接操做,所以數字 21
會被轉換爲相應的字符串 "21"
。prototype
然而 String(21)
則是很是典型的顯式強制類型轉換。日誌
這兩種強制轉換類型的操做都是將數字轉換爲字符串。
不過「顯式」仍是「隱式」都是相對而言的。好比若是你知道 a + ""
是怎麼回事,那麼對你來講這可能就是「顯式」的。反之,若是你不知道 String(a)
能夠用來字符串強制類型轉換,那麼它對你來講可能就是「隱式」的。
在介紹強制類型轉換以前,咱們須要先了解一下字符串、數字和布爾值之間類型轉換的基本規則。在 ES5 規範中定義了一些「抽象操做」和轉換規則,在這咱們介紹一下 ToPrimitive
、ToString
、ToNumber
和 ToBoolean
。注意,這些操做僅供引擎內部使用,和平時 JavaScript 代碼中的 .toString()
等操做不同。
你能夠將 ToPrimitive
操做看做是一個函數,它接受一個 input
參數和一個可選的 PreferredType
參數。ToPrimitive
抽象操做會將 input
參數轉換成一個原始值。若是一個對象能夠轉換成不止一種原始值,可使用 PreferredType
指定抽象操做的返回類型。
根據不一樣的輸入類型,ToPrimitive
的轉換操做以下:
輸入類型 | 操做 / 返回值 |
---|---|
Undefined | 自身(無轉換操做) |
Null | 自身(無轉換操做) |
Boolean | 自身(無轉換操做) |
Number | 自身(無轉換操做) |
String | 自身(無轉換操做) |
Object | 返回 Object 的 default value 。Object 的 default value 經過在該對象上傳遞 PreferredType 參數給內部操做 [[DefaultValue]](hint) 得到。[[DefaultValue]](hint) 的實現請往下看。 |
[[DefaultValue]](hint)
內部操做在對象 O
上調用內部操做 [[DefaultValue]]
時,根據 hint
的不一樣,其執行的操做也不一樣,簡化版(具體可參考 ES5 規範 8.12.8 節)以下:
若是
hint
是String
;
若是
O
的toString
屬性是函數;
- 將
O
設置爲this
值並調用toString
方法,將返回值賦值給val
;- 若是
val
是原始值類型則返回;若是
O
的valueOf
屬性是函數;
- 將
O
設置爲this
值並調用valueOf
方法,將返回值賦值給val
;- 若是
val
是原始值類型則返回;- 拋出
TypeError
錯誤。若是
hint
是Number
;
若是
O
的valueOf
屬性是函數;
- 將
O
設置爲this
值並調用valueOf
方法,將返回值賦值給val
;- 若是
val
是原始值類型則返回;若是
O
的toString
屬性是函數;
- 將
O
設置爲this
值並調用toString
方法,將返回值賦值給val
;- 若是
val
是原始值類型則返回;- 拋出
TypeError
錯誤。若是
hint
參數爲空;
- 若是
O
是Date
對象,則和hint
爲String
時一致;- 不然和
hint
爲Number
時一致。
原始值的字符串化的規則以下:
null
轉化爲 "null"
;undefined
轉化爲 "undefined"
;true
轉化爲 "true"
;false
轉化爲 "false"
;數字的字符串化遵循通用規則,如 21
轉化爲 "21"
,極大或者極小的數字使用指數形式,如:
var num = 3.912 * Math.pow(10, 50); num.toString(); // "3.912e50"
對於普通對象,若是對象有自定義的 toString()
方法,字符串化時就會調用該自定義方法並使用其返回值,不然返回的是內部屬性 [[Class]]
的值,好比 "object [Object]"
。須要注意的是,數組默認的 toString()
方法通過了從新定義,其會將全部元素字符串化以後再用 ","
鏈接起來,如:
var arr = [1, 2, 3]; arr.toString(); // "1,2,3"
在 ES5 規範中定義的 ToNumber
操做能夠將非數字值轉換爲數字。其規則以下:
true
轉換爲 1
;false
轉換爲 0
;undefined
轉換爲 NaN
;null
轉換爲 0
;NaN
。在將某個值轉換爲原始值的時候,會首先執行抽象操做 ToPrimitive
,若是結果是數字則直接返回,若是是字符串再根據相應規則轉換爲數字。
參照上述規則,如今咱們能夠一步一步來解釋本文開頭的那行代碼了。
var timestamp = +new Date(); // timestamp 就是當前的系統時間戳,單位是 ms
其執行步驟以下:
new
操做符比 +
操做符優先級更高,所以先執行 new Date()
操做,生成一個新的 Date
實例;一元操做符 +
在其操做數爲非數字時,會對其進行隱式強制類型轉換爲數字:
hint
是 Number
;
Date
實例的 valueOf
屬性指向的是 Date.prototype.valueOf
,是一個函數;this
指向 Date
實例並調用 valueOf
函數,得到返回值;timestamp
變量。有了以上知識,咱們就能夠實現一些比較好玩的東西了,好比將數字和對象相加:
var a = { valueOf: function() { return 18; } }; var b = 20; +a; // 18 Number(a); // 18 a + b; // 38 a - b; // -2
順帶提一下,從 ES5 開始,使用 Object.create(null)
建立的對象,其 [[Prototype]]
屬性爲 null
所以沒有 valueOf()
和 toString()
方法,所以沒法進行強制類型轉換。請看以下示例:
var a = {}; var b = Object.create(null); +a; // NaN +b; // Uncaught TypeError: Cannot convert object to primitive value a + ''; // "[object Object]" b + ''; // Uncaught TypeError: Cannot convert object to primitive value
JavaScript 中有兩個關鍵字 true
和 false
,分別表示布爾類型的真和假。咱們常常會在 if
語句中將 0
做爲假值條件,1
做爲真值條件,這也利用了強制類型轉換。咱們能夠將 true
強制類型轉換爲 1
,false
強制類型轉換爲 0
,反之亦然。然而 true
和 1
並非一回事,false
和 0
也同樣。
在 JavaScript 中值能夠分爲兩類:
false
的值true
的值)在 ES5 規範中下列值被定義爲假值:
undefined
null
false
+0
、-0
和 NaN
""
假值的布爾強制類型轉換結果爲 false
。
在假值列表之外的值都是真值。
規則不免有例外。剛說了除了假值列表之外的全部其餘值都是真值,然而你能夠在現代瀏覽器的控制檯中執行下面幾行代碼試試:
Boolean(document.all); typeof document.all;
獲得的結果應該是 false
和 "undefined"
。然而若是你直接執行 document.all
獲得的是一個類數組對象,包含了頁面中全部的元素。document.all
實際上不能算是 JavaScript 語言的範疇,這是瀏覽器在特定條件下建立一些外來(exotic)值,這些就是「假值對象」。
假值對象看起來和普通對象並沒有二致(都有屬性,document.all
甚至能夠展爲數組),可是其強制類型轉換的結果倒是 false
。
在 ES5 規範中,document.all
是惟一一個例外,其緣由主要是爲了兼容性。由於老代碼可能會這麼判斷是不是 IE:
if (document.all) { // Internet Explorer }
在老版本的 IE 中,document.all
是一個對象,其強制類型轉換結果爲 true
,而在現代瀏覽器中,其強制轉換結果爲 false
。
除了假值之外都是真值。
好比:
var a = 'false'; var b = '0'; var c = "''"; var d = Boolean(a && b && c); d; // ?
d
是 true
仍是 false
呢?
答案是 true
。這些值都是真值,相信不須要過多分析。
一樣,如下幾個值同樣都是真值:
var a = []; var b = {}; var c = function() {};
顯式強制類型轉換很是常見,也不會有什麼坑,JavaScript 中的顯式類型轉換和靜態語言中的很類似。
字符串和數字之間的相互轉換靠 String()
和 Number()
這兩個內建函數實現。注意在調用時沒有 new
關鍵字,只是普通函數調用,不會建立一個新的封建對象。
var a = 21; var b = '2.71828'; var c = String(a); var d = Number(b); c; // "21" d; // 2.71828
除了直接調用 String()
或者 Number()
方法以外,還能夠經過別的方式顯式地進行數字和字符串之間的相互轉換:
var a = 21; var b = '2.71828'; var c = a.toString(); var d = +b; c; // "21" d; // 2.71828
雖然 a.toString()
看起來很像顯式的,然而其中涉及了隱式轉換,由於 21
這樣的原始值是沒有方法的,JavaScript 自動建立了一個封裝對象,並調用了其 toString()
方法。
+b
中的 +
是一元運算符,+
運算符會將其操做數轉換爲數字。而 +b
是顯式仍是隱式就取決於開發者自身了,本文以前也提到過,顯式仍是隱式都是相對的。
和字符串與數字之間的相互轉換同樣,Boolean()
能夠將參數顯示強制轉換爲布爾值:
var a = ''; var b = 0; var c = null; var d = undefined; var e = '0'; var f = []; var g = {}; Boolean(a); // false Boolean(b); // false Boolean(c); // false Boolean(d); // false Boolean(e); // true Boolean(f); // true Boolean(g); // true
不過咱們不多會在代碼中直接用 Boolean()
函數,更常見的是用 !!
來強制轉換爲布爾值,由於第一個 !
會將操做數強制轉換爲布爾值,並反轉(真值反轉爲假值,假值反轉爲真值),而第二個 !
會將結果反轉回原值:
var a = ''; var b = 0; var c = null; var d = undefined; var e = '0'; var f = []; var g = {}; !!a; // false !!b; // false !!c; // false !!d; // false !!e; // true !!f; // true !!g; // true
不過更常見的狀況是相似 if(...) {}
這樣的代碼,在這個上下文中,若是咱們沒有使用 Boolean()
或者 !!
轉換,就會自動隱式地進行 ToBoolean
轉換。
三元運算符也是一個很常見的布爾隱式強制類型轉換的例子:
var a = 21; var b = 'hello'; var c = false; var d = a ? b : c; d; // "hello"
在執行三元運算的時候,先對 a
進行布爾強制類型轉換,而後根據結果返回 :
先後的值。
大部分被詬病的強制類型轉換都是隱式強制類型轉換。可是隱式強制類型轉換真的一無可取嗎?並不必定,引擎在必定程度上簡化了強制類型轉換的步驟,這對於有些狀況來講並非好事,而對於另外一些狀況來講可能並不必定是壞事。
在上一節咱們已經介紹了字符串和數字之間的顯式強制類型轉換,在這一節咱們來講說他們二者之間的隱式強制類型轉換。
+
運算符既能夠用做數字之間的相加也能夠經過重載用於字符串拼接。咱們可能以爲若是 +
運算符兩邊的操做數有一個或以上是字符串就會進行字符串拼接。這種想法並不徹底錯誤,但也不是徹底正確的。好比如下代碼能夠驗證這句話是正確的:
var a = 21; var b = 4; var c = '21'; var d = '4'; a + b; // 25 c + d; // "214"
可是若是 +
運算符兩邊的操做數不是字符串呢?
var arr0 = [1, 2]; var arr1 = [3, 4]; arr0 + arr1; // ???
上面這條命令的執行結果是 "1,23,4"
。a
和 b
都不是字符串,爲何 JavaScript 會把 a
和 b
都轉換爲字符串再進行拼接?
根據 ES5 規範 11.6.1 節,若是 +
兩邊的操做數中,有一個操做數是字符串或者能夠經過如下步驟轉換爲字符串,+
運算符將進行字符串拼接操做:
- 若是一個操做數爲對象,則對其調用
ToPrimitive
抽象操做;ToPrimitive
抽象操做會調用[[DefaultValue]](hint)
,其中hint
爲Number
。
這個操做和上面所述的 ToNumber
操做一致,再也不重複。
在這個操做中,JavaScript 引擎對其進行 ToPrimitive
抽象操做的時候,先執行 valueOf()
方法,可是因爲其 valueOf()
方法返回的是數組,沒法獲得原始值,轉而調用 toString()
方法,toString()
方法返回了以 ,
拼接的全部元素的字符串,即 1,2
和 3,4
,+
運算符再進行字符串拼接,獲得結果 1,23,4
。
簡單來講,只要 +
的操做數中有一個是字符串,或者能夠經過上述步驟獲得字符串,就進行字符串拼接操做;其他狀況執行數字加法。
因此如下這段代碼可謂隨處可見:
var a = 21; a + ''; // "21"
利用隱式強制類型轉換將非字符串轉換爲字符串,這樣轉換很是方便。不過經過 a + ""
和直接調用 String(a)
之間並非徹底同樣,有些細微的差異須要注意一下。a + ""
會對 a
調用 valueOf()
方法,而後再經過上述的 ToString
抽象操做轉換爲字符串。而 String(a)
則會直接調用 toString()
。
雖然返回值都是字符串,然而若是 a
是對象的話,結果可能出乎意料!
好比:
var a = { valueOf: function() { return '21'; }, toString: function() { return '6'; } }; a + ''; // "42" String(a); // "6"
不過大部分狀況下也不會寫這麼奇怪的代碼,若是你真的要擴展 valueOf()
或者 toString()
方法的話,請留意一下,由於你可能無心間影響了強制類型轉換的結果。
那麼從字符串轉換爲數字呢?請看下面的例子:
var a = '2.718'; var b = a - 0; b; // 2.718
因爲 -
操做符不像 +
操做符有重載,-
只能進行數字減法操做,所以若是操做數不是數字的話會被強制轉換爲數字。固然,a * 1
和 a / 1
也能夠,由於這兩個運算符也只能用於數字。
把 -
用於對象會怎麼樣呢?好比:
var a = [3]; var b = [1]; a - b; // 2
-
只能執行數字減法,所以會對操做數進行強制類型轉換爲數字,根據前面所述的步驟,數組會調用其 toString()
方法得到字符串,而後再轉換爲數字。
假設如今你要實現這麼一個函數,在它的三個參數中,若是有且只有一個參數爲真值則返回 true
,不然返回 false
,你該怎麼寫?
簡單一點的寫法:
function onlyOne(x, y, z) { return !!((x && !y && !z) || (!x && y && !z) || (!x && !y && z)); } onlyOne(true, false, false); // true onlyOne(true, true, false); // false onlyOne(false, false, true); // true
三個參數的時候代碼好像也不是很複雜,那若是是 20 個呢?這麼寫確定過於繁瑣了。咱們能夠用強制類型轉換來簡化代碼:
function onlyOne(...args) { return ( args.reduce( (accumulator, currentValue) => accumulator + !!currentValue, 0 ) === 1 ); } onlyOne(true, false, false, false); // true onlyOne(true, true, false, false); // false onlyOne(false, false, false, true); // true
在上面這個改良版的函數中,咱們使用了數組的 reduce()
方法來計算全部參數中真值的數量,先使用隱式強制類型轉換把參數轉換成 true
或者 false
,再經過 +
運算符將 true
或者 false
隱式強制類型轉換成 1
或者 0
,最後的結果就是參數中真值的個數。
經過這種改良版的代碼,咱們能夠很簡單的寫出 onlyTwo()
、onlyThree()
的函數,只須要改一個數字就行了。這無疑是一個很大的提高。
在如下狀況中會發生隱式強制類型轉換:
if (...)
語句中的條件判斷表達式;for (..; ..; ..)
語句中的條件判斷表達式,也就是第二個;while (..)
和 do..while(..)
循環中的條件判斷表達式;.. ? .. : ..
三元表達式中的條件判斷表達式,也就是第一個;||
和邏輯與 &&
左邊的操做數,做爲條件判斷表達式。在這些狀況下,非布爾值會經過上述的 ToBoolean
抽象操做被隱式強制類型轉換爲布爾值。
||
和 &&
JavaScript 中的邏輯或和邏輯與運算符和其餘語言中的不太同樣。在別的語言中,其返回值類型是布爾值,然而在 JavaScript 中返回值是兩個操做數之一。所以在 JavaScript 中,||
和 &&
被稱做選擇器運算符可能更合適。
根據 ES5 規範 11.11 節:
||
和&&
運算符的返回值不必定是布爾值,而是兩個操做數中的其中一個。
好比:
var a = 21; var b = 'xyz'; var c = null; a || b; // 21 a && b; // "xyz" c || b; // "xyz" c && b; // null
若是 ||
或者 &&
左邊的操做數不是布爾值類型的話,則會對左邊的操做數進行 ToBoolean
操做,根據結果返回運算符左邊或者右邊的操做數。
對於 ||
來講,左邊操做數的強制類型轉換結果若是爲 true
則返回運算符左邊的操做數,若是是 false
則返回運算符右邊的操做數。
對於 &&
來講則恰好相反,左邊的操做數強制類型轉換結果若是爲 true
則返回運算符右邊的操做數,若是是 false
則返回運算符左邊的操做數。
||
和 &&
返回的是兩個操做數之一,而非布爾值。
在 ES6 的函數默認參數出現以前,咱們常常會看到這樣的代碼:
function foo(x, y) { x = x || 'x'; y = y || 'y'; console.log(x + ' ' + y); } foo(); // "x y" foo('hello'); // "hello y"
看起來和咱們預想的一致。可是,若是是這樣調用呢?
foo('hello world', ''); // ???
上面的執行結果是 hello world y
,爲何?
在執行到 y = y || "y"
的時候,JavaScript 對運算符左邊的操做數進行了布爾隱式強制類型轉換,其結果爲 false
,所以運算結果爲運算符右邊的操做數,即 "y"
,所以最後打印出來到日誌是 "hello world y"
而非咱們預想的 hello world
。
因此這種方式須要確保傳入的參數不能有假值,不然就可能和咱們預想的不一致。若是參數中可能存在假值,則應該有更加明確的判斷。
若是你看過壓縮工具處理後的代碼的話,你可能常常會看到這樣的代碼:
function foo() { // 一些代碼 } var a = 21; a && foo(); // a 爲假值時不會執行 foo()
這時候 &&
就被稱爲守護運算符(guard operator),即 &&
左邊的條件判斷表達式結果若是不是 true
則會自動終止,不會判斷操做符右邊的表達式。
因此在 if
或者 for
語句中咱們使用 ||
和 &&
的時候,if
或者 for
語句會先對 ||
和 &&
操做符返回的值進行布爾隱式強制類型轉換,再根據轉換結果來判斷。
好比:
var a = 21; var b = null; var c = 'hello'; if (a && (b || c)) { console.log('hi'); }
在這段代碼中,a && (b || c)
的結果實際是 'hello'
而非 true
,而後 if
再經過隱式類型轉換爲 true
才執行 console.log('hi')
。
Symbol
的強制類型轉換ES6 中引入了新的基本數據類型 —— Symbol
。然而它的強制類型轉換有些不同,它支持顯式強制類型轉換,可是不支持隱式強制類型轉換。
好比:
var s = Symbol('hi'); String(s); // 'Symbol(hi)' s + ''; // Uncaught TypeError: Cannot convert a Symbol value to a string
並且 Symbol
不能強制轉換爲數字,好比:
var s = Symbol('hi'); s - 0; // Uncaught TypeError: Cannot convert a Symbol value to a number
Symbol
的布爾強制類型轉換都是 true
。