掌握 Javascript 類型轉換:隱式轉換救救孩子

在上一篇中我們聊過了 JS 類型轉換的規則和我發現的一些常見書籍中關於類型轉換的一些小錯誤,當碰到顯示類型轉換的時候大家可以按照這些規則去拆解出答案。但 JS 中存在一些很隱晦的隱式類型轉換,這一篇就來談下我對隱式類型轉換的一些總結。

關於 JS 類型轉換規則請看上一篇的內容:掌握 JS 類型轉換:從規則開始

什麼是隱式類型轉換呢?顧名思義就是有時候你感覺不到這是類型轉換但是實際上類型轉換已經發生了。所以這個 "隱式" 取決於我們的理解和經驗,如果你看不出來那就是隱式的。

下面按照我自己對於隱式轉換的分類來逐個聊聊吧。

一元操作符 +、-

var a = '123';
var b = +a;
console.log(b); // 123

先來看看 +- 在一個類型值前面,這裏會執行 ToNumber 類型轉換。如果是 - 在前面的話,還會將結果的符號取反,如:-"123" 的結果是 -123。並且如果原類型是對象的話也是遵循 ToNumber 的轉換規則,大家可以自己試試,這裏就不再舉多餘的例子了。

二元操作符

接下來我們來看一下二元操作符相關的隱式轉換,比如:+-&&||==等等這些。

相減 a - b

var a = '123';
var b = true;
console.log(a - b); // 122

當執行減法操作時,兩個值都會先執行 ToNumber 轉換,所以這個是比較簡單的,當類型是對象時也是遵循同樣的規則。

相加 a + b

console.log('123' + 4); // "1234"
console.log(123 + true); // 124

相加的情況有點複雜,但隱式轉換的規則大家可以按照我總結的來記:

  1. 如果 + 的操作數中有對象,則執行 ToPrimitive 並且 hint 是 Number
  2. 如果 + 中有一個操作數是字符串(或通過第一步得到字符串),則執行字符串拼接(另一個操作數執行 ToString 轉換),否則執行 ToNumber 轉換後相加

這個相加操作的隱式轉換規則看似有點麻煩,其實解析後還是很明確的。

第一步,先看操作數裏面有沒有對象,如果有就是執行 hint 是 Number 的 ToPrimitive 操作。大家可以回憶下上篇說的 ToPrimitive 的內容,這裏要注意的是這裏的 ToPrimitive 並沒有將操作數強制轉化爲 Number 類型。因爲 hint 是 Number,所以先執行 valueOf() ,如果返回了字符串那轉換結果就是字符串了;如果返回的不是基本類型值纔會執行 toString(),如果都沒有返回基本類型值就直接拋異常了。

第二步,如果有一個操作數是字符串,那麼整個結果就是字符串拼接的,否則就是強轉數字加法;第二個操作數就會按這個規則進行對應的類型轉換。

開頭的代碼說明了字符串加數字、數字加布爾值的結果按這個規則走的,下面我們來看看對象情況下的代碼:

var a = Object.create(null);
a.valueOf = function() {
  return '123';
}
a.toString = function() {
  return '234';
}
console.log(a + 6); // "1236"

以上的執行結果說明了執行 ToPrimitive 並且 hint 是 Number 結論是正確的,因爲 "123"valueOf 返回的。兩個操作數相加的其他情況大家也可以自己試試,記住我上面的總結就完了。

a && b、a || b

在 JS 中我們都知道 &&|| 是一種"短路”寫法,一般我們會用在 ifwhile 等判斷語句中。這一節我們就來說說 &&|| 出現的隱式類型轉換。

我們通常把 &&|| 稱爲邏輯操作符,但我覺得 《你不知道的 Javascript(中卷)》中有個說法很好:稱它們爲"選擇器運算符"。看下面的代碼:

var a = 666;
var b = 'abc';
var c = null;

console.log(a || b); // 666
console.log(a && b); // "abc"
console.log(a || b && c); // 666

&&|| 會對操作數執行條件判斷,如果操作數不是布爾值,會先執行 ToBoolean 類型轉換後再執行條件判斷。最後 &&|| 會返回一個操作數的值還不是返回布爾值,所以稱之爲"選擇器運算符"很合理。

這裏有個可能很多人都不知道的情況是:在判斷語句的執行上下文中,&&|| 的返回值如果不是布爾值,那麼還會執行一次 ToBoolean 的隱式轉換:

var a = 666;
var b = 'abc';
var c = null;

if (a && (b || c)) {
  console.log('yes');
}

如果要避免最後的隱式轉換,我們應該這樣寫:

if (!!a && (!!b || !!c)) {
  console.log('yes');
}

a == b 和 a === b

從這裏開始是 JS 中隱式轉換最容易中坑的地方

首先我們先明確一個規則:"== 允許在相等比較中進行類型轉換,而 === 不允許。"

所以如果兩個值的類型不同,那麼 === 的結果肯定就是 false 了,但這裏要注意幾個特殊情況:

  • NaN !== NaN
  • +0 === -0

ES5 規範定義了 == 爲"抽象相等比較",即是說如果兩個值的類型相同,就只比較值是否相等;如果類型不同,就會執行類型轉換後再比較。下面我們就來看看各種情況下是如何轉換的。

null == undefined

這個大家記住就完了,null == undefined // true。也就是說在 == 中 null 與 undefined 是一回事。

所以我們判斷變量的值是 null 或者 undefined 就可以這樣寫了:if (a == null) {...}

數字和字符串的抽象相等比較

一個操作數是字符串一個是數字,則字符串會被轉換爲數字後再比較,即是:ToNumber(字符串) == 數字

var a = 666;
var b = '666';
console.log(a == b); // true

布爾值與其他類型的抽象相等比較

注意,這裏比較容易犯錯了:

var a = '66';
var b = true;
console.log(a == b); // false

雖然 '66' 是一個真值,但是這裏的比較結果卻不是 true,很容易掉坑裏。大家記住這個規則:布爾值如果與其他類型進行抽象比較,會先用 ToNumber 將布爾值轉換爲數字再比較。

顯然 '66' == 1 的結果當然是 false 咯。

對象與非對象的抽象相等比較

先說下規則:如果對象與非對象比較,則先執行 ToPrimitive(對象),並且 hint 參數爲空;然後得到的結果再與非對象比較。

這裏值得注意的是:在 ToPrimitive() 調用中如果 hint 參數爲空,那麼 [[DefaultValue]] 的調用行爲跟 hint 是Number 時一樣——先調用 valueOf() 不滿足條件再調用 toString()

注意這裏有個例外情況:如果對象是 Date 類型,則 [[DefaultValue]] 的調用行爲跟 hint 是 String 時一樣。

我們來測試一下是不是這樣的:

var a = Object.create(null);
a.valueOf = function() {
  console.log('a.valueOf is invoking.');
  return 666;
};
a.toString = function() {
  console.log('a.toString is invoking.');
  return '666';
};

console.log(a == 666);
// a.valueOf is invoking.
// true

console.log(a == '456');
// a.valueOf is invoking.
// false

a.valueOf = undefined;
console.log(a == '666');
// a.toString is invoking.
// true

根據輸出來看依據上面的規則來解釋是 OK 的。

有一個開源項目有張圖表可以方便大家去記憶 =====,點擊 這裏 查看。

a > b、a < b

按慣例先總結規則,情況略微複雜:

第一步:如果操作數是對象則執行 ToPrimitive(對象),並且 hint 參數爲空。

第二步:

  • 如果雙方出現非字符串,則對非字符串執行 ToNumber,然後再比較
  • 如果比較雙方都是字符串,則按字母順序進行比較

我們還是用代碼來測試下:

var a = Object.create(null);
a.valueOf = function() {
  console.log('a.valueOf is invoking.');
  return '666';
};
a.toString = function() {
  console.log('a.toString is invoking.');
  return true;
};

console.log(a > '700');
// a.valueOf is invoking.
// false

a.valueOf = undefined;
console.log(a < 2);
// a.toString is invoking.
// true

這裏注意下當測試 a < 2 時,toString() 返回了 true,然後會執行 ToNumber(true) 返回 1,最後 1 < 2 的結果就是 true。

a ≥ b,a ≤ b

最後這裏也是一個比較容易中坑的地方。

根據規範 a ≤ b 會被處理爲 a > b,然後將結果反轉,即處理爲 !(a > b);a ≥ b 同理按照 !(a < b) 處理。

我們來看個例子:

var a = { x: 666 };
var b = { x: 666 };

console.log(a >= b); // true
console.log(a <= b); // true

這裏 a 和 b 都是字面量對象,valueOf() 的結果還是對象,所以轉爲執行 toString(),結果都是'[object Object]',當然 a < ba > b 的結果都是 false,然後取反結果就是 true 了。≤ 和 ≥ 的結果都是 true,是不是有點出乎意料呢

總結

上一篇寫了 JS 類型轉換的規則,這一篇寫了隱式轉換中我總結的經驗和判斷法則。感覺已經差不多了,剩下的就是實踐中自己去理解了,後續可能還會找一些比較坑的類型轉換示例代碼寫一篇拆解分析。

感謝大家花時間聽我比比,歡迎 star 和關注我的 JS 博客:小聲比比 Javascript

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章