很久之前,看到過這樣一種判斷
[] == ![]; // true
當時覺得很神奇,翻了些博客,但也似懂非懂。今天翻看博客的時候,偶然又看見了它,感覺跟以前比更清晰了些,所以在此結合 js 類型轉換,記錄下自己的理解。
[TOC]
一、分解
乍一看,確實比較容易讓人迷惑,但是復雜的東西只要能夠被分解,我們就能容易地分析、理解它了。
由 JS 運算符優先級可知,“!” 的優先級高于 “==”,也就是說,一定程度上,我們可以將上面的語句改寫為:
[] == (![]); // true
或者,更進一步:
var a = [], b = ![];
a == b; // true
二、求值
其實到上一步的時候,你可能已經發現了一個問題,沒錯,那就是:
var b = ![]; // false
其實這里就涉及到對象類型的真假值的問題。JavaScript 規定,所有的 JavaScript 的引用類型數據都是真值,這里說“引用類型數據”而非對象類型,是因為
typeof null === 'object'; // true
!null === true; // true
筆者并不想糾結于 null 是不是對象類型這樣的問題進行討論。
正如上文所述,任何引用類型的數據都是真值,包括對象、函數等等:
!{} === true; // false
![] === false; // true
!function () {} === false; // true
!window === true; // false
!window.open === false; // true
注意這里說的是任何引用類型,類似 Boolean 此類的包裝函數所構造出來的對象當然也在此列:
!new Boolean(0) === false; // true
!new Boolean(true) === true; // false
有時我們想要將某種數據快速轉換為一個布爾值,而不想因為使用 Boolean 而導致數據變成對象的時候,可以使用如下形式:
!!0 // false
!!undefined // false
!!1 // true
!!{} // true
!![] // true
!!false // false
!!null // false
!!function () {} // true
順帶一提的是,出了 ! 會導致真假值的判斷,直接將一個值作為 if 語句的判斷條件也會如此。
if (condition) {
// do something
}
這里只要 condition
是一個真值,if 分支下的 “do something” 處的語句就會被執行。最典型的坑就出在 condition
是一個
- 空數組([])
- 空的 NodeList 實例( 如 document.querySelectorAll('not-exist') )
- 空的 HTMLCollection 實例( 如 document.getElementsByTagName('not-exist') )
- 空的 jQuery 實例( 如 $('not-exist') )...
因為 document.getElementById 沒有命中的時候返回 null,也即是一個假值,很多人認為 document.getElementsByTagName、jQuery 等也是如此,或者認為它們返回了一個空的數組,并認為這個空數組“應該”是一個假值。但實際上,無論是空數組、還是空的 NodeList / HTMLCollection / jQuery 的實例,它們本質上都還是引用類型數據,所以它們都是真值。一個比較簡單的驗證 NodeList / HTMLCollection / jQuery 的實例是否命中的方法是讀取它們的 length 屬性,如果不為 0 ,則可以認為它們命中了元素。
三、toString / valueOf
經過前兩步的分析,我們可以將前面的判斷改寫為:
[] == false; // true
按照常規思路,引用類型的變量之間的比較,是基于引用的比較,二者如果是相同的引用,則相等,否則不等。如果按照這樣的邏輯,引用類型的數據根本不可能和基礎類型的數據相等才對,但是這里就真的相等了。
說到這里,就必須提到原生 JS 中 toString / valueOf 這兩個處處遍布的方法。
(一) 分類
對于不同類型的對象,js定義了多個版本的 toString 和 valueOf 方法
(1) toString:
- 普通對象,返回 "[object Object]";
- 數組,返回數組元素之間添加逗號合并成的字符串;
- 函數,返回函數的定義式的字符串;
- 日期對象,返回一個可讀的日期和時間字符串;
- 正則,返回其字面量表達式構成的字符串;
(2) valueOf:
- 日期對象,返回自1970年1月1日到現在的毫秒數;
- 其它均返回對象本身;
toString / valueOf 兩個方法,主要可用于引用類型數據的類型轉換,通過調用它們,可以將引用類型數據使用在原本應該使用基本數據類型的地方。
(二)適用場景
原生的 toString / valueOf 分別位于對象的構造函數的 prototype 屬性上,如果需要修改,大可直接在實例對象上直接添加 toString / valueOf 方法,這樣也不會影響到原型鏈上的方法。
(1)類型轉換
1)對象=>字符串
a. 執行toString,如果返回了一個原始值,則將其轉化為字符串
b. 否則執行valueOf方法,如果返回了一個原始值,則將其轉化為字符串
c. 否則拋出類型錯誤
如:
var o = {};
o.toString = function () {
return 'my string';
};
String(o); // my string
2) 對象=>數字
a. 執行valueOf,如果返回了一個原始值,如果需要,則將其轉化為數字
b. 否則執行toString,如果返回了一個原始值,則將其轉化為數字并返回
c. 否則拋出類型錯誤
var o = {};
o.valueOf = function () {
return 233;
};
Number(o); // 233
(2)比較和運算
在執行 “>”、“<”、“+”、“-” 等操作的時候,如果涉及到引用類型數據,大部分引用類型數據在運算之前,會先嘗試執行其 valueOf 方法,如果該方法返回了一個基本數據類型,則拿該返回值替代對象本身參與運算否則則嘗試執行 toString 方法,如果該方法返回了一個基本類型數據,則使用該數據參與操作;如果該方法返回的不是基本類型數據,則嘗試執行 valueOf 方法,如果該方法返回了一個基本類型數據,則使用該數據參與操作;否則將提示 TypeError。
var o = {};
o.toString = function () {
return 2;
}
// 此時還沒有為 o 添加 valueOf 方法
// 它將先調用繼承自 Object.prototype.valueOf 方法
// 返回值是它自身
// 于是則調用這里我們為實例添加的 toString 方法
o == 2; // true
// 這里為實例添加了 valueOf 方法
// 一開始,它就將調用我們為實例添加的 valueOf 方法
// 返回值 1 是基本類型數據
// 則再調用 toString 方法
o.valueOf = function () {
return 1;
}
o == 1; // true
o + 1; // 2
o * 5; // 5
注意前面說的是“大部分引用類型數據”,唯一不遵循此規則的是 Date 類型對象。與其它引用類型數據不同的是,在比較或者計算的時候,它會先嘗試調用其 toString 方法,如果沒有返回基本數據類型才嘗試調用其 valueOf 方法。
var t = new Date();
// t 繼承自 Date.prototype 上的 toString / valueOf 都能返回基本類型數據
t.valueOf(); // 返回時間戳,如 1505438878370
t.toString(); // 時間信息字符串,如 "Fri Sep 15 2017 09:27:58 GMT+0800 (CST)"
t + 2344444; // 并不會得到一個時間戳,而是 "Fri Sep 15 2017 09:27:58 GMT+0800 (CST)2344444"
所以當你不清楚它會得到什么值的時候,請自己調用 toString / valueOf 方法,后來 Date.prototype 對象上增加了一個 getTime 方法替代 valueOf 獲取時間戳,但是這個方法在 IE 存在兼容性問題,僅 IE9+ 有效。
四、再轉換
到這里,其實就很清晰了。
[] == false; // true
其實就是:
([]).toString() == false; // true
也就是:
'' == false; // true
這里就涉及了基本類型數據的隱式轉換問題了。基本依照以下規則:
- 兩個都是數值,則比較數值
- 兩個都是字符串,則比較字符編碼值
- 其中一個是數值,則要把另個轉化成數值進行比較
- 如果其中一個是對象,則調用 valueOf / toString 方法
- 如果有一個是布爾值,則將其轉化成數值
顯然這里滿足最后一條規則,比較的時候,其實將會嘗試將二者轉化為數字類型。相當于:
Number('') == Number(false); // true
即:
0 == 0; // true
五、總結
JavaScript 是一門弱類型語言,但是弱類型并不代表沒有類型,相反的是,JavaScript 是一門類型豐富的語言,除了常見語言的數字、字符串、布爾、對象、函數、null 等,更是有一個神奇的 undefined 類型。一邊是弱類型,一邊又是多種類型,這看似矛盾,但由于隱式類型轉換的存在,這種矛盾看起來又如此的合理。
P.S. 雖然上面的代碼中,我使用了大量的 “==”,而非 “===”,但這僅是學習用的。實際開發的時候,我也推薦使用 “===”。
一方面,如果由于自己的疏忽,沒能正確處理好隱式類型轉換,往往會造成意料之外的問題,為項目帶來潛在的風險,比如我想驗證某個變量是否是 undefined,如果采用:
value == undefined;
但實際上,null 也會被匹配進來,可能造成潛在的風險,如果使用 “===” 就不會有這個問題;
另一方面,如果多人協作開發,隱式類型轉換往往會為其他人帶來困擾,尤其是在成員間開發能力參差不齊的情況下。
例如,我想驗證一個值是否是布爾值 true,但是我寫了這樣的代碼:
value == true;
你知道哪些數據會匹配成功么?