上一期中我們主要是瞭解了JavaScript中存在兩大資料型別:基本型別
和引用型別
以及其儲存的方式(堆和棧)。
本期我們將重點談談JavaScript資料型別轉換過程出現的各種“奇葩”的問題。
寫在前面
在JavaScript中當運算子在運算時,如果兩邊資料不統一,CPU就無法計算,這時我們編譯器會自動將運算子兩邊的資料做一個資料型別轉換,轉成一樣的資料型別再計算,這種無需程式設計師手動轉換,而由編譯器自動轉換的方式就稱為隱式轉換。
在JavaScript中“一切皆是物件”,在我們具體瞭解隱式轉換前先了解一下物件的兩個方法:toString()
和valueOf()
。
toString()
toString() 方法返回一個表示該物件的字串。
// 數字轉字串(123).toString() // '123'// 布林值轉字串(true).toString() // 'true'// 陣列轉字串['hello', 'world'].toString() // 'hello,world'// 物件轉字串({name: 'hello world'
}).toString() // '[object Object]'//日期物件轉字串Date().toString() // 'Wed Jan 23 2019 21:10:42 GMT+0800 (China Standard Time)'//JSON物件轉字串JSON.toString() // '[object JSON]'// Function轉字串Function.toString() // 'function Function() {
[native code]
}'// 函式轉字串(function(){
return 1;
}).toString() // 'function () {
return 1;
}'複製程式碼
valueOf()
valueOf() 方法返回指定物件的原始值。
JavaScript呼叫valueOf方法將物件轉換為原始值。你很少需要自己呼叫valueOf方法;當遇到要預期的原始值的物件時,JavaScript會自動呼叫它。
預設情況下,valueOf方法由Object後面的每個物件繼承。 每個內建的核心物件都會覆蓋此方法以返回適當的值。如果物件沒有原始值,則valueOf將返回物件本身。
JavaScript的許多內建物件都重寫了該函式,以實現更適合自身的功能需要。因此,不同型別物件的valueOf()方法的返回值和返回值型別均可能不同。
// 數字的原始值(123).valueOf() // 123// 布林值的原始值(true).valueOf() // true// 陣列的原始值['hello', 'world'].valueOf() // [ 'hello', 'world' ]// 物件的原始值({name: 'hello world'
}).valueOf() // {
name: 'hello world'
}//日期物件的原始值Date().valueOf() // 'Wed Jan 23 2019 21:10:42 GMT+0800 (China Standard Time)'//JSON的原始值JSON.valueOf() // 'Object [JSON] {
}'// Function的原始值Function.valueOf() // '[Function: Function]'// 函式的原始值(function func(){
return 1;
}).valueOf() // '[Function: func]'複製程式碼
隱式轉換規則
- 轉成string型別:+(字串連線符)
- 轉成number型別:++/–(自增或自減運算子)、+ – * / % (算術運算子)、>
<
>
= <
= == != === !== (關係運算子) - 轉成boolean型別:!(邏輯非運算子)
字串 VS 加號連線符
字串 + 基本型別 = 字串 + String(基本型別)
// 字串 + 數字console.log('hello' + 123) // 'hello' + '123'// 字串 + 布林console.log('hello' + true) // 'hello' + 'true'// 字串 + nullconsole.log('hello' + null) // 'hello' + 'null'// 字串 + undefinedconsole.log('hello' + undefined) // 'hello' + 'undefined'複製程式碼
數字 VS 加號連線符
數字 + 基本型別(非字串) = 數字型別 + Number(基本型別)
// 數字 + 布林console.log(1 + true) // 2// 等同於console.log(1 + Number(true)) // 1 + 1// 數字 + undefinedconsole.log(1 + undefined) // NaN// 等同於console.log(1 + Number(undefined)) // 1 + NaN// 數字 + nullconsole.log(1 + null) // 1// 等同於console.log(1 + Number(null)) // 1 + 0複製程式碼
數字 + 引用型別 = 數字 + 引用型別.toString()
// 數字 + 陣列console.log(1 + []) // '1'// 等同於console.log(1 + [].toString()) // 1 + '' = '1'// 數字 + 物件console.log(1 + {
}) // '1[object Object]'// 等同於console.log(1 + ({
}).toString()) // 1 + '[object Object]'複製程式碼
數字型別 + 函式 = 數字型別 + String(函式)
// 數字 + 函式var func = function() {
var a = 2;
return 2;
}console.log(1 + func);
// 1function () {var a = 2;
return 2;
}複製程式碼
關係運算子的隱式轉換
規則:將其他資料型別轉換成數字型別之後再比較關係
// 字串 vs 數字 = Number(字串) vs 數字console.log('2' >
10);
// flase// 等同於console.log(Number('2') >
10) // 2 >
10// 字串(數字) vs 字串(數字) = ASCII碼(對應值) vs ASCII碼(對應值)console.log('2' >
'10');
// true// 等同於console.log('2'.charCodeAt() >
'10'.charCodeAt) // 50 >
49// 字串(字母) vs 字串(字母) = ASCII碼(對應值) vs ASCII碼(對應值)console.log('abc' >
'b');
// false// 等同於console.log('abc'.charCodeAt() >
'b'.charCodeAt()) // 97 >
98// NaN 不等於 NaNconsole.log(NaN == NaN) // false// undefined vs nullconsole.log(undefined == null) // true//注意:undefined == null 不等同於console.log(Number(undefined) == Number(null)) // false NaN == 0// 這裡JavaScript的特殊約定的結果,undefined == null,詳情可以檢視更多資料// https://codeburst.io/javascript-null-vs-undefined-20f955215a2複製程式碼
邏輯非與關係運算子的隱式轉換
// 數字 vs 陣列 = 數字 vs Number(陣列)console.log([] == 0);
// true// 等同於console.log(Number([]) == 0) // 0 == 0// 數字 vs 布林 = 數字 vs Number(布林)console.log(![] == 0);
//true// 等同於console.log(Number(![]) == 0) // Number(false) == 0 // 引用型別 != 引用型別console.log([] == []);
// false// 邏輯非隱式轉換console.log([] == ![]);
// true// 等同於console.log(Number([]) == Number(!Boolean([]))) // 0 == 0// 邏輯非隱式轉換console.log({
} == !{
});
// false// 等同於console.log(Number({
}) == Number(!Boolean({
}))) // NaN == 0 複製程式碼
引用型別的隱式轉換
規則:
- 當引用型別的valueOf()呼叫時返回的值是一個基本型別時,則直接進行運算。
- 當引用型別的valueOf()呼叫時返回的值不是一個基本型別時,則引用型別的toString()將會被呼叫並返回轉換後的字串,然後再進行運算。
// 字串 + 陣列console.log('hello' + []) // 'hello' + [].toString()// 等同於console.log('hello' + [].toString()) // 'hello' + ''// 字串 + 物件console.log('hello' + {
}) // 'hello[object Object]'// 等同於console.log('hello' + ({
}).toString()) // 'hello' + '[object Object]'複製程式碼
案例一:
目的:驗證非自定義物件的隱式轉換過程
// 申明一個物件obj1var obj1 = {
age: 18
}console.log(10 + obj1) // '10[object Object]'複製程式碼
第一步:判斷物件的valueOf()返回值是否是基本型別
console.log(obj1.valueOf()) // {
age: 18
}console.log(typeof obj1.valueOf()) // object 返回的是一個物件複製程式碼
第二步:呼叫物件的toString()返回一個表示該物件的字串
console.log(obj1.toString()) // '[object Object]'複製程式碼
第三步:根據運算規則進行運算(非字元連線操作都轉換成Number進行運算)
// 因為obj1.toString() 返回的是字串,所以進行字串連線操作console.log(10 + obj1.toString()) // 10 + '[object Object]'複製程式碼
案例二:
目的:通過自定義物件的valueOf()和toString(),來驗證物件的隱式轉換過程
// 申明一個物件obj2var obj2 = {
age: 18 toString: function() {
return '' + this.age;
}, valueOf: function() {
return this.age;
}
}console.log(10 + obj2) // 10 + 18 = 28複製程式碼
第一步:判斷物件的valueOf()返回值是否是基本型別
console.log(obj2.toString()) // '18'console.log(typeof obj2.toString()) // stringconsole.log(obj2.valueOf()) // 18console.log(typeof obj2.valueOf()) // number複製程式碼
第二步:如果第一步能正確返回基本型別,則直接跳到第三步,否則將呼叫物件的toString()返回一個表示該物件的字串
// 如果物件的valueOf()返回的是基本型別console.log(10 + obj2) // 10 + obj2.valueOf()// 如果物件的valueOf()返回的是引用型別console.log(10 + obj2) // 10 + obj2.toString()複製程式碼
第三步:根據運算規則進行運算(非字元連線操作都轉換成Number進行運算)
// 如果obj2的返回值是字串,都進行字串 VS 加號規則console.log(10 + '18') // 10 + String(obj2)// 如果obj2的返回值是非字串,都進行數字 VS 加號規則console.log(10 + obj2) // 10 + Number(obj2)複製程式碼
特殊說明
JavaScript中存在幾個特殊的原始值:null、undefined、”、0、NaN。
console.log(typeof null) // objectconsole.log(null instanceof Object) // falseconsole.log(NaN == NaN) // falseconsole.log(null == undefined) // trueconsole.log(Number(null)) // 0console.log(Number(undefined)) // NaNconsole.log(0 == '') // trueconsole.log(0 == ' ') // trueconsole.log('' != ' ') //trueconsole.log(null != 0) // trueconsole.log(undefined != 0) // true複製程式碼
寫在最後
通過上面對JavaScript中的資料型別的隱式轉換可以總結出以下結論:
- JavaScript中運算子在運算時,最終都將轉換成相同型別進行運算(字串型別、數字型別)
- 字串與加號連線符運算時轉換成String型別
- 非字串加號連線符的運算都將轉換成Number型別
- 特別注意引用型別的隱式轉換是先判斷valueOf()返回的型別,如果返回是引用型別則呼叫toString()返回對應的字串值,最終都是按照1,2,3的規則進行運算。
以上內容雖然有進行驗證,但不知道描述上是否存在歧義,有些點表述的不是很清楚,望諒解。
如果有發現任何問題或者有更好的建議,歡迎直接給我留言。
交流
更多精彩內容請關注我的github部落格,如果你覺得還不錯請給個star,非常感謝。