從 [] == ![] 看 js 類型轉換

很久之前,看到過這樣一種判斷

[] == ![];    // 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;

你知道哪些數據會匹配成功么?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 第5章 引用類型(返回首頁) 本章內容 使用對象 創建并操作數組 理解基本的JavaScript類型 使用基本類型...
    大學一百閱讀 3,270評論 0 4
  • 本章內容 使用對象 創建并操作數組 理解基本的 JavaScript 類型 使用基本類型和基本包裝類型 引用類型的...
    悶油瓶小張閱讀 697評論 0 0
  • 六月時候,這十分炎熱的季節,在其上旬的某天,心中突生想法,要去東輝峽谷走一遭。我選擇了單車出行,做此決定,...
    楊旭東_97e5閱讀 428評論 0 2
  • 正月十六月兒明, 男女老少烤雜病。 烤烤腳,百病消, 烤烤腚,一年四季不得病。 …… 正月十六家鄉有“烤雜病”的習...
    女派閱讀 1,601評論 0 0
  • 今天開始學習英語單詞
    encome閱讀 331評論 0 0