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