1.什麼是型別轉換?
型別轉換的定義很容易理解,就是值從一個型別轉換為另一個型別,比如說從String
型別轉換為Number
型別,'42'→42
。但是,如果我們對JS中的型別轉換規則瞭解的並不足夠的話,我們就會遇到很多令人迷惑的問題,就好像如果沒有學習物理和化學,生活中處處都是魔法一樣。
JS中的型別轉換大概可以分為顯式型別轉換和隱式型別轉換兩種。兩者的區別十分明顯,比如顯式型別轉換'String(42)
',我們可以明顯的看出,這行程式碼就是為了將42轉為String型別。然而隱式型別轉換通常是某些操作的副作用,比如''+42
,就沒那麼明顯了,它是加法操作的副作用。
如果你沒有理解JS中的型別轉換,下面的幾個例子可能會讓你王德發。
//ex.1
console.log([] == ![]); //true
//ex.2
const a1 = {},
a2 = {};
console.log(a1 < a2); //false
console.log(a1 == a2); //false
console.log(a1 > a2); //false
console.log(a1 >= a2); //true
console.log(a1 <= a2); //true
//ex.3
const a3 = {
i: 1,
valueOf() {
return this.i++;
},
};
console.log(a3 == 1 && a3 == 2); //true
這些奇怪的現象都有背後的道理,當然,前提是你要理解它。
2.型別轉換的抽象操作
在詳細介紹顯式和隱式型別轉換之前,我們需要先知曉String
、Number
和Boolean
之間型別轉換的基本規則。ES5規範中定義了這些型別之間轉換的抽象操作(或者說轉換規則)。下面簡單介紹幾種抽象操作規則。
ToPrimitive(負責物件轉為基本資料型別)
為了將值轉換為相應的基本型別值,抽象操作 ToPrimitive 會首先檢查該值是否有 valueOf 方法。如果有並且該方法返回基本型別值,就使用該值進行強制型別轉換。
如果沒有就使用 toString的返回值(如果存在)來進行強制型別轉換。
如果 valueOf 和 toString 均不返回基本型別值,會產生 TypeError 錯誤。例如:
//ex.4 const testObj = {}; console.log(testObj.valueOf()); // {} testObj本身 console.log(testObj.toString()); //'[object Object]' string字串 /* 首先呼叫testObj的valueOf方法,返回值是testObj本身,不是原始(基本)值型別;所以呼叫testObj的toString方法,返回了字串'[object Object]',是基本型別,所以使用'[object Object]'進行操作。 */ console.log('' + testObj); // '[object Object]' testObj.valueOf = () => 'valueOf'; /* 此時呼叫testObj的valueOf方法,返回的是字串'valueOf',是基本型別,所以會用該返回值進行操作,而不會再繼續呼叫toString方法了。 */ console.log('' + testObj); // 'valueOf'
ToString(負責非字串轉為字串)
a.當要轉換的值是基本型別時,
要轉換的值 轉成String之後的結果 null 'null'
undefined 'undefined'
true , false 'true'
,'false'
Symbol('example') 僅支援顯式型別轉換得到 'Symbol('example')'
普通數字 0,1,2 符合通用規則 '0','1','2'
極大或極小的數字 1/10000000 使用指數形式 '1e-7' ,'1e+21'
b.當要轉換的值是物件時,如果是顯式型別轉換,則呼叫該值的toString方法,如果是隱式型別轉換,則先通過ToPrimitive操作轉為基本型別,再根據規則a轉為String型別。
當需要轉換的值的valueOf方法和toString方法沒有被修改時,常用的物件轉換如下:
要轉換的值 轉成String之後的結果 Object '[object Object]'
Array [1,2,3] 用逗號分隔的每個陣列元素, '1,2,3'
陣列元素是null或者undefined時, [null],[undefined] 空字串 ''
Date 美式英語日期格式的字串, 'Fri Oct 15 2021 14:04:28 GMT+0800 (中國標準時間)'
Function 表示函式原始碼的一個字串
ToNumber(負責非數字轉為數字)
要轉換的值 轉成Number之後的結果 true 1 false 0 undefined NaN null 0 string '42','42px' 基本遵循數字常量的相關規則,處理失敗時返回 NaN
42,NaN
symbol 不可轉換 Object等複雜型別 先通過ToPrimitive轉為基本型別,再轉成Number型別 例如:
console.log(Number([null])); //0 // [null] 通過ToPrimitive轉成基本型別是空字串 '',空字串轉為Number得到0
ToBoolean
JavaScript 中的值可以分為以下兩類:
(1) 可以被型別轉換為 false 的值:undefined
,null
,+0、-0和NaN
,false
,''
(2) 其他(被型別轉換為 true 的值),即除了上面的幾個值以外的。要注意的是有一個概念稱為假值物件,假值物件和普通物件的區別在於,當他被轉為Boolean時,會得到false。這類情況非常少見,但是確實存在,比如
document.all
物件,console.log(!!document.all); //false
。
好了,目前我們已經對這幾種抽象操作有一定了解了,下面就可以稍微深入一下型別轉換了。
3.顯式型別轉換
字串和數字之間的顯式型別轉換
字串和數字之間的型別轉換應該是我們最常見的一種了,它們之間的顯式型別轉換時通過JS中的原生建構函式String和Number實現的,但是不通過new機進行建構函式呼叫。這兩個函式在進行型別轉換時,都遵循ToString和ToNumber抽象操作。
例如:
console.log(Number('3')); // 3 console.log(String(3)); //'3'
顯式解析字串
解析字串中的數字(
parseInt
、parseFloat
)和將字串轉換為數字雖然得到的結果都是數字,但是二者的轉換規則是不一樣的。 a. 相對於Number而言,parseInt允許傳入的字串引數中含有非數字字元,解析按照從左到右的順序,遇到非數字字元就停止解析。例如
console.log(parseInt('10px')); //10
,console.log(Number('10px'));//NaN
。 b. parseInt是針對字串的函式,當傳入的第一個引數不是字串時,會先將其轉為字串再進行解析。
c. parseInt有第二個引數,代表解析時使用的進位制,比如
console.log(parseInt('11', 8)); //9
,用八進位制對'11'進行解析。該引數的範圍是2-36,傳入範圍之外的值(如果傳入0,會被設為10進位制),會返回NaN。在使用
parseInt(str,radix)
時,radix並沒有指定預設值(只是大部分情況radix會被預設設為10),也就是說如果你不傳入這個引數的話,得到的結果可能是你意料之外的。比如當str
以0x
或者0X
(大寫的X)開頭時,radix會被預設設為16,所以當console.log(parseInt('0x11')); //17
會得到17。有個很有趣的例子:
console.log([1, 2, 3].map(parseInt)); //[ 1, NaN, NaN ]
,就是利用了這個進位制引數,結果陣列的三項分別是parseInt(1,0)
,parseInt(2,1)
,parseInt(3,2)
,在第一項中,因為進位制傳入了0,被當為10進位制處理,所以返回了1;第二項中因為進位制傳入了1
,不在合法範圍(2-36)內,所以返回了NaN
;第三項中,因為進位制傳入2,而要解析的引數是3(二進位制只有0和1的表達才是合法的),所以返回了NaN
。
顯示轉換Boolean
與
String
和Number
一樣,Boolean
是顯式的 ToBoolean 型別轉換,遵循ToBoolean操作規則。不過一般!!
的方法使用的比較多。例如:console.log(Boolean(1), !!1);
4.隱式型別轉換
隱式型別轉換一般是其他操作的副作用。
加法操作:
如果運算元是物件,則先物件通過ToPrimitive操作轉為基本型別,然後再繼續操作。
如果其中一個運算元是字串(String)型別,則將另一個運算元也轉換為字串(String)型別,進行字串拼接。
如果運算元中沒有字串(String)型別,則將兩個運算元作為數字(Number)型別,進行加法運算。
例如:
// 因為 'hello' 是字串,所以把 1 轉成了 '1' ,然後進行字串拼接 console.log(1 + 'hello'); // '1hello' //因為沒有字串型別的運算元,所以進行加法運算,將 true 轉成數字型別為 1 ,所以結果為2 console.log(1 + true); // 2 //因為 [2] 是物件型別,將其通過 ToPrimitive 操作之後,轉成字串 '2' ,進而需要把 1 轉成 '1' ,進行字串拼接 console.log(1 + [2]); // '12'
減法、除法、乘法等操作:
如果運算元是物件,則先物件通過ToPrimitive操作轉為基本型別,然後再繼續操作。
將兩個運算元作為數字(Number)型別,進行運算。
例如:
// [3] - [2] → '3' - '2' → 3 - 2 =1 console.log([3] - [2]); // 1 //3 - 1 =2 console.log([3] - true); // 2
將其他型別隱式轉換為Boolean的操作:
當值作為判斷條件時。如 if語句中的條件判斷表示式; for 迴圈語句中的條件判斷表示式;while 迴圈和 do..while迴圈中的條件判斷表示式;三目運算子(?:) 中的條件判斷表示式; 邏輯運算子 || 和 && 中的值作為條件判斷表示式。該值會被隱式轉換為Boolean型別進行判斷,遵循ToBoolean操作規則。
寬鬆相等(==):
在JS中進行關係比較時,寬鬆相等(==)經常被解釋為:“只比較二者的值是否相等,而不比較型別”,其實這種解釋有些問題,這種解釋中的“值”,應該怎樣理解呢?
比如我在進行比較
console.log(0 == ''); //true
,0和空字串'',二者的原始值和型別都不相同,但是卻是寬鬆相等的。更為準確地解釋,應該是“當進行寬鬆相等的關係比較時,如果二者型別相同,則僅比較二者的值是否相同即可;如果二者型別不同,則需要先進行型別轉換為同一型別,再比較值是否相同”。
也就是說,當兩個型別不同的值進行寬鬆相等的比較時,會發生隱式的型別轉換。
寬鬆相等中,型別不同時的型別轉換規則如下(假設兩個運算元分別為x,y):
如果x和y的型別分別為字串(String)和數字(Number),則將字串型別轉為數字然後比較。
//先將字串 '1' 轉成了數字 1 ,再進行比較 console.log(1 == '1');//true
Boolean型別的運算元,都需要轉成數字(Number)型別。
//先將 false 轉成了 0,然後再根據規則1,將 '0' 轉成了 0,然後進行比較 console.log('0' == false);//true
如果一方是物件型別,則需要通過ToPrimitive抽象操作規則,轉成基本型別以後再繼續比較。
// 先將物件型別的陣列 [1] 轉成了字串 '1' ,再根據規則1,將字串 '1' 轉成了數字 1,然後進行比較 console.log([1] == 0);//false
- null和undefined是寬鬆相等的,可以說,在寬鬆相等的比較中,null和undefined是一回事。
另外要了解的是:
• NaN 不等於 NaN 。
• +0 等於 -0大於(>),小於(<),大於等於(>=),小於等於(<=):
在進行大於、小於這兩種比較時:
a. 如果有物件型別,首先通過ToPrimitive操作轉成基本型別,在進行比較。
b. 如果兩者都是字串,則按字元的ASCII碼值比較。
c. 若果有一方不是字串,則將二者都通過ToNumber抽象操作轉為Number型別之後,再進行比較。
在進行大於等於、小於等於這兩種比較時:
大於等於代表著不小於,即 a>=b的結果是 !(a<b)。小於等於同理。
5.回顧
瞭解了JS型別轉換規則之後,在回過頭看之前的幾個例子,想來我們都能夠理解這些奇怪的現象了。
ex.1:
//ex.1
console.log([] == ![]); //true
//[] 通過ToPrimitive轉成基本型別,得到 '', ![]得到false。寬鬆相等比較規則,轉成數字 0
// 即 ''==0 , 空字串''有轉成了數字 0 ,所以最終為true
ex.2:
//ex.2
const a1 = {},
a2 = {};
//a1 a2都是物件型別,通過ToPrimitive操作之後,結果都是 '[object Object]'
//顯然 '[object Object]'<'[object Object]' 和 '[object Object]'>'[object Object]'都是false
console.log(a1 < a2); //false
console.log(a1 > a2); //false
//在進行寬鬆相等比較時,二者型別相同,不會進行型別轉換,而是比較二者的值,a1,a2顯然指向的地址不同,所以a1==a2 =也為false
console.log(a1 == a2); //false
//小於等於 大於等於 分別為 大於和小於的結果取反,所以都為true
console.log(a1 >= a2); //true
console.log(a1 <= a2); //true
ex.3:
//ex.3
const a3 = {
i: 1,
valueOf() {
return this.i++;
},
};
console.log(a3 == 1 && a3 == 2); //true
//a3是物件型別,通過ToPrimitive操作,轉成基本型別得到 a3.i (即1),所以 a3==1 為true
//但是在上面執行valueOf的時候,i自增了一次,所以在 a3==2的比較時, a3轉為基本型別的值時,得到的就是2了,所以a3==2也為true。