沿着平滑的曲線學會 JavaScript 中的隱式強制類型轉換(實戰應用篇)

這一部份內容是承接上一篇的, 建議先閱讀沿着平滑的曲線學會 JavaScript 中的隱式強制類型轉換(基礎篇)數組

前兩章討論了基本數據類型和基本包裝類型的關係, 以及兩個在類型轉換中十分重要的方法: valueOftoString 方法. 接下來的內容創建在前兩章之上, 給出判斷隱式類型轉換結果的方法, 文章最後部分給出了多個練習以及解析, 用以檢驗文中討論方法的正確性.bash

3 各類類型之間的強制類型轉換

此處談的強制類型轉換指的是除了符號類型(symbol)以外的基本數據類型以及對象之間的類型轉換, 對於符號類型(symbol)單獨討論.函數

3.1 ToPrimitive 將變量轉換爲 基本數據類型

把一個變量轉換爲 基本數據類型 的轉換過程能夠被抽象成一種稱爲 ToPrimitive 的操做, 主意它只是一個抽象出來的名稱, 而不是一個具體的方法, 各類數據類型對它的實現纔是具體的.post

ToPrimitive 把一個變量轉變成一個基本的類型, 會根據變量的類型不一樣而採起不一樣的操做:ui

  1. 若是這個變量已是基本類型了: 那就不進行轉換了, 直接返回這個變量, 就直接用這個變量的值了.spa

  2. 當這個變量是一個對象時: 就調用這個對象的內部的方法 [[DefaultValue]] 來來把對象轉換成基本類型。設計

簡單來講, 對於基本數據類型直接返回自己. 對於對象就執行對象自己的 [[DefaultValue]] 方法來得到結果. 那麼這個 [[DefaultValue]] 方法是怎麼工做的呢, 其實也並不難.code

3.2 [[DefaultValue]] 操做 返回對象的基本數據類型(原始類型)的值

[[DefaultValue]] 方法利用對象內部的 valueOf 方法或 toString 方法返回操做數的基本數據類型(能夠指定想獲得的類型偏好)。此操做的過程可如此簡單理解:對象

在默認的狀況下:ip

先調用 valueOf() 方法, 若是返回值是基本類型, 則使用這個值; 不然: 調用 toString() 方法, 獲得返回值. 若是這兩個方法都沒法獲得基本數據類型的返回值,則會拋出 TypeError 異常.

另: 對於 Date 對象, 會將這兩個方法的調用順序顛倒過來. 先調用 toString ,若得不到基本類型的值, 就再調用 valueOf. 若都不能獲得基本類型的值, 一樣拋出 TypeError 異常.

簡單總結一下 3.1 部分的內容: 在將一個值轉換爲基本數據類型的時候, 若是這個值自己就是一個基本數據類型, 則直接使用它本身; 若是這個值是個對象, 就調用對象的兩個方法: valueOftoString , 這兩個函數獲得的結果就是這個對象轉換成的基本類型的值.

3.2 基本數據類型之間的類型轉換

前一部分討論了對象如何強制轉換爲基本數據類型, 夲節主要討論基本數據類型之間的相互轉換. 主要包含三個小部分:

  1. 其餘類型數據轉換成 字符串
  2. 其餘類型轉換成 數值
  3. 其餘類型轉換成 布爾值

下面來一一具體討論.

3.2.1 其餘類型數據轉換成 字符串

其餘基本數據類型的值轉換成字符串類型其實很是簡單, 直接變成字符串的形式就能夠了. 例如:

null -> "null", undefined -> "undefined", true -> "true", false -> "false", 3.14159 -> "3.14159"

注: 對於很是大或者很是小的數字來講, 轉換成字符串會是科學記數法的形式, 例如:

3140000000000000000000 -> "3.14e+21" // 很大的數轉換成字符串

0.000000314 -> "3.14e-7"  // 很小的數轉換成字符串
複製代碼

3.2.2 其餘類型數據轉換成 數值

其餘基本數據類型轉換成數值類型也比較簡單, 只有字符串須要作很是簡單的判斷. 具體爲:

null -> 0, undefined -> NaN, true -> 1, false -> 0

對於字符串來講, 可細分爲一下的狀況:

  • 空字符串轉換爲 0
  • 若字符串中只含 數字, 加減號, 小數點符號, 則直接轉換, 並且忽略前導的 0, 例如:
"3.14" -> 3.14
"-0003.14" -> -3.14 // 前導有 0
複製代碼
  • 若是字符串中內容爲十六進制, 則轉換成的數值爲十進制的形式, 例如: "0xa -> 10"

  • 其餘狀況,則都轉換結果爲 NaN, 例如:

"A10" -> NaN
"1A0" -> NaN
"10A" -> NaN
複製代碼

3.2.3 其餘類型數據轉換成 布爾值

這就更簡單了, 只有幾個特殊的值轉換後爲 false , 除此以外的其餘值轉換後都爲 true, 這幾個特殊的值以下: NaN, undefined, null, 0, +0, -0, 空字符串""

上述 3.2 部分的內容的記憶是比較簡單的, 在處理具體類型轉換的問題時只須要靈活運用就能夠了, 可是有時候在同一個問題中的同一個變量涉及多個轉換過程, 好比從 對象 轉爲字符串, 而後再從字符串轉爲 數值.

下一節將會討論涉及到隱式類型轉換的實際應用, 會包含不少例子.

4 涉及到隱式類型轉換的狀況

JavaScript 中不少經常使用的操做都會引發隱式的強制類型轉換, 下面的部分舉幾個常見的例子.

4.1 加減乘除號引發的隱式類型轉換 + - * /

經常使用的四則運算操做符在有些時候會引發隱式強制類型的轉換

4.1.1 加號 +

在 JavaScript 中, 加號 + 能夠用來作加法運算, 也能夠用來拼接字符串, 那該怎麼判斷它執行的是哪一個操做呢?

有人說只要加號鏈接的兩個變量其中有一個是字符串時就執行的是拼接操做, 不然就執行加法操做. 這種說法是不完整的, 例以下面這幾個例子就沒法按照這種說法獲得結果, 由於加號兩邊的變量都不是字符串類型:

// 例 1
console.log(true + true); // ?
console.log(1 + null); // ?


// 例 2
let array = [2, 3];
let a = 1 + array;

let obj = { name: 'doug', age: 4 };
let b = 1 + obj;

console.log(a, b); // ?
複製代碼

那麼到底應該怎麼判斷呢? 我的認爲能夠這樣來作, 分紅兩種簡單的狀況 :

  1. 若是加號的左右兩邊都是除字符串以外的基本類型值, 或者是能夠經過 ToPrimitive(見 3.1 部分) 抽象操做轉換成這些類型的 對象, 那麼後臺會嘗試將這兩個變量都轉換成數字(具體機制見3.2.2節)進行加法操做.

看下面的實驗結果:

console.log( 1 + 1 );       // 2

// true -> 1; false -> 0
console.log( 1 + true );    // 2
console.log( 1 + false );   // 1
console.log( false + 1 );   // 1
console.log( false + true );    // 1

// null -> 0
console.log( 1 + null );    // 1
console.log( true + null ); // 1

// undefined -> NaN
console.log( 1 + undefined );   // NaN
console.log( true + undefined );    // NaN
console.log( null + undefined );    // NaN


// 經過 ToPrimitive 操做返回 number, boolean, null, undefined 基本類型值的對象
// 重寫了對象的 valueOf 和 toString 方法
let obj = {
    valueOf: function(){
        return true;
    },

    toString: function(){
        return 'call obj';
    }
}

console.log(1 + obj);   // 2
console.log(obj + obj); // 2, 這個例子更加典型
複製代碼

前面的內容提到過在默認狀況下 ToPrimitive 會首先調用 對象 objvalueOf 方法來獲取基本類型的值, 因此獲得了 true , 而後輸出語句就變成了 console.log(1 + true)console.log(true + true) , 以後 true 被轉換成數值類型 1, 式子變成console.log(1+1). 實驗結果證明了剛纔的設想, 適用於 例1 中的狀況.

那麼 什麼狀況下進行字符串的拼接操做? 設想以下 :

  1. 若是加號的左右兩邊存在 字符串 或者能夠經過 ToPrimitive (見 3.1 部分)抽象操做轉換成字符串的對象, 則執行的就是拼接操做.

在 例2 中:

let array = [2, 3];
let a = 1 + array;

let obj = { name: 'doug', age: 4 };
let b = 1 + obj;

console.log(a, b); // ?
複製代碼

變量 a 等於 1 加上一個 數組 array, 數組是能夠經過 ToPrimitive 轉化爲字符串的, 按照3.1 和 3.2 節的內容, 數組先調用了本身的 valueOf 方法, 發現返回的是 數組自己, 不是基本數據類型; 因而接着調用 toString 方法, 返回了一個各項用 "," 鏈接的字符串 "2,3" . 因而如今就有了 let a = 1 + "2,3", 是數值和字符串相加, 結果就是 "12,3".

對象 obj 調用 valueOf 返回它自己, 再調用 toString 方法返回字符串 "[object Object]". 而後就變成了 let b = 1 + "[object Object]" , 就變成了數值和字符串相加, 是拼接操做, 因此結果就出來了 "1[object Object]".

再看一個例子:

function fn(){ console.log('running fn'); }

console.log(fn + fn);
/* function fn(){ console.log('running fn'); }function fn(){ console.log('running fn'); } */
複製代碼

這個例子的結果用剛纔的設想是能夠比較容易的獲得結果的. 即: 函數 fn 能夠經過 ToPrimitive 操做返回一個字符串, 而後式子就變成了 字符串+字符串, 結果就是字符串的拼接.

至此. 上面的論斷可能存在不嚴謹的地方, 歡迎批評指正.

4.1.2 減乘除 - * / 運算符產生的強制類型轉換

這三個運算符會將左右兩邊不是數值類型的變量強制轉換成簡單數值類型, 而後執行數學運算, 例如:

console.log(true - false);	// 1
console.log(true - null);	// 1
console.log(true * true);	// 1
console.log(2 - undefined);	// NaN

console.log([2] - 1);	// 1
// [2] -valueOf-> [2] -toString-> "2" -> 2

console.log('3' * 2);	//6

console.log('4' / '2');	// 2

let obj = {
	toString: function(){ // 重寫了 toString 方法, 返回一個字符串
		return '4';
	}
};

console.log(obj * [2]);	// 8
複製代碼

上述幾個例子中變量最終都被轉換成了數值型的基本數據類型. 其中數組和對象經過 ToPrimitive (見 3.1 部分)先轉換成字符串, 接着強制轉換成數值類型再進行數學運算.

4.2 邏輯運算符 ||&&

咱們經常使用將邏輯運算符用在條件判斷中, 例如:

if(a || b){
    // codes
}
複製代碼

這是很天然的操做.

然而邏輯運算符返回的並非想象中的布爾類型的 true or false , 而是它左右兩個操做數中的一個。例如:

let a = 50;
let b = 100;

console.log(a || b, a && b); // 50 100, 並無輸出 true 或者 false
複製代碼

能夠看到輸出結果並非布爾值, 而是兩個操做數中的一個. 同時還發現, 對於兩個相同的操做數, ||&& 操做符的輸出狀況並不同. 下面來討論一下緣由.

這兩個操做符會根據左邊(只判斷左邊, 不判斷右邊)的操做數轉換成布爾類型以後的值決定返回哪一個操做數, 會先檢驗左邊的操做數的真值, 再作出決定. 具體機制以下:

  1. 對於 || , 當左邊的真值爲 true 時, 則返回左邊的; 不然返回右邊的操做數.
  2. 對於 && , 當左邊的真值爲 false 時, 直接返回左邊的; 不然返回右邊的操做數.

能夠這樣來簡單理解: || 意爲 或, 只要兩個鐘有一個爲真就能夠了, 因此若是左邊爲真總體就爲真, 直接返回左邊就能夠了. && 意爲 且, 要求兩邊都爲真, 若是左邊爲真, 那麼就取決於右邊的真假狀況了, 因此直接返回右邊.

回到開頭的例子:

if(a || b){
    // codes
}
複製代碼

根據上面的討論, 能夠知道 a || b 並不返回布爾值, 然而 if 倒是根據布爾值決定是否執行內部操做的, 那麼爲何能夠正常執行呢? 緣由是 if 語句還要對 a || b 的返回值進行一次隱式強制類型轉換, 轉換成布爾值, 而後再進行下一步的決定.

相似進行隱式強制類型轉換判斷的狀況還有:

  • for循環
  • while 和 do while 循環
  • 三元運算符 xx? a:b

4.3 非嚴格相等符號 ==

4.3.1 與嚴格相等符號 === 的異同

非嚴格相等符號(==)是和嚴格相等符號(===)相關的概念. 它們的區別是: == 容許進行強制類型轉換, 再比較轉換後的左右操做數; 而 === 不進行強制類型轉換, 直接比較左右兩個操做數.

當左右兩個操做數的類型相同的時候, 這兩種比較符號的效果相同, 運用的原理也相同.

在比較對象的時候, 這兩個比較符號的原理也相同: 比較左右兩個變量指向的是否是同一個對象.

4.3.2 對象(包括數組和函數)和基本數據類型之間的 == 比較

在對象與基本數據類型的比較的時候, 對象會經過 ToPrimitive (見 3.1 部分) 操做返回基本數據類型的值, 而後再進行比較.

4.3.3 布爾值和其餘類型的 == 比較

在布爾值與其餘類型比較時, 會先將布爾類型的值轉換成數值, 即: true->1, false->0.

4.3.4 字符串和數值的 == 比較

將字符串轉換成數值類型, 而後進行比較

4.3.4 nullundefined 的比較

nullundefined 在用 == 比較時返回的是 true, 並且除了它們自身以外, 只有這兩者相互比較時才返回 true.

換句話說, 除了其自身以外, null 只有和 undefined== 比較時才爲 true, 與其餘任何值比較時都是 false; 一樣的, 除了其自身以外, undefined 只有和 null== 比較時才爲 true, 與其餘任何值比較時都是 false.

即: 對於 nullundefined 來講, 只有這三種狀況爲真:

console.log( null == undefined ); 	// true
console.log( undefined == undefined ); 	// true
console.log( null == null ); 	// true
複製代碼

其餘特殊狀況

  1. NaN 不與任何值等, 即便和自身相比也不相等
console.log(NaN == NaN); 	// false
複製代碼
  1. +0 -0 0 三者相等
console.log(0 == +0);	// true
console.log(0 == -0);	// true
console.log(-0 == +0);	// true
複製代碼

總結一下: 對象(包括數組和函數)和其餘類型比較時, 要進行類型轉換的是對象; 布爾值和其餘數據比較時, 要進行類型轉換的是布爾值, 轉換成數值類型1或0; 在數值和字符串比較時, 要轉換類型的是字符串, 轉換成數值類型.

5 應用, 舉例分析

下面舉一些隱式強制類型轉換的例子, 用以前討論的內容判斷, 並給出解析:

console.log( "4" == 4 );		// true
// 字符串和數值比較, 字符串轉換爲數值, 即 "4" -> 4

console.log( "4a" == 4 );		// false
/* 原理同上, 字符串和數值比較, 字符串轉換爲數值, 可是字符串裏包含 "a", 因此轉換後是 NaN, 即 "4a" -> NaN; 式子變成 `NaN == 4", 由於 NaN 與任何值都不等, 故爲 false */

console.log( "5" == 4 );		// false
// 字符串和數值比較, 字符串轉換爲數值, 即 "5" -> 4, 式子變成 `5 == 4`, false

console.log( "100" == true );		// false
/* 存在布爾值, 首先布爾值轉換爲數值, 即: true -> 1, 式子變成: `"100" == 1`, 此時爲字符串和數值比較, 字符串轉換爲數值, 式子變成 `100 == 1`, false */

console.log( null == undefined );		// true
console.log( undefined == undefined );		// true
console.log( null == null );		// true
console.log( null == [] );		// false
console.log( null == "" );		// false
console.log( null == {} );		// false
console.log( null == 0 );		// false
console.log( null == false);		// false
console.log( undefined == [] );		// false
console.log( undefined == "" );		// false
console.log( undefined == {} );		// false
console.log( undefined == 0 );		// false
console.log( undefined == false );		// false
console.log(null == Object(null) );		// false
console.log(undefined == Object(undefined) );		// false
// 以上的答案比較容易得出, 由於 null 和 undefined 除了本身以外只認識彼此, 文章 4.3.4 部分


console.log( "0" == false );		// true
/* 包含布爾值, 首先布爾值轉換爲數值, 即: false -> 0,而後式子變成 ` "0" == 0 `; 此時變成了字符串和數值比較, 字符串轉換爲數值, 即: "0" -> 0, 而後式子變成 ` 0 == 0 `, true */

console.log( 0 == false);		// true
// false 轉換爲 0, true

console.log( false == "" );		// true
/* false -> 0, 式子變成 ` 0 == "" `, 數字和字符串比較; 字符串轉換爲數值, 即: "" -> 0, 式子變成 ` 0 == 0 `, true */

console.log( false == [] );		// true
// 包含對象和布爾值, 布爾值優先 轉換, 隨後對象經過 ToPrimitive 操做轉換爲基本數據類型後比較:
// false -> 0, [] -> "" -> 0; ` 0 == 0 `, true

console.log( false == {} );		// false
// 包含對象和布爾值, 布爾值優先 轉換, 隨後對象經過 ToPrimitive 操做轉換爲基本數據類型後比較:
// false -> 0, {} -> "Object Object" -> NaN; ` 0 == NaN ` false

console.log( 0 == "");		// true
// "" -> 0; ` 0 == 0 ` true

console.log( "" == [] );		// true
// 字符串和對象比較, 對象經過 ToPrimitive 操做轉換爲基本數據類型後比較:
// [] -toString- -> ""; 式子變成 ` "" == "" `, true 

console.log( 0 == []);		// true
// 數值和對象比較, 對象經過 ToPrimitive 操做轉換爲基本數據類型後比較:
// [] -toString- -> ""; 式子變成 ` 0 == "" `, 此時是數值類型和字符串比較, 字符串轉換爲數值
// "" -> 0 ; 式子變成了 ` 0 == 0 `,true

console.log( 0 == {});		// false
// 數值和對象比較, 對象經過 ToPrimitive 操做轉換爲基本數據類型後比較:
// {} -> "Object Object" -> NaN; ` 0 == NaN ` false
複製代碼

JavaScript 中設計的強制類型轉換的內容不止文中提到的這些, 仍存在沒有討論到的內容會在未來討論.同時文中可能存在錯誤, 請不吝指正, 謝謝.

參考資料:

  • 《JavaScript 高級程序設計》
  • 《你不知道的 JavaScript》
  • MDN
相關文章
相關標籤/搜索