前端進階系列(第2期):深入理解JavaScript資料型別轉換
上一期中我們主要是瞭解了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' // 字串 + null console.log('hello' + null) // 'hello' + 'null' // 字串 + undefined console.log('hello' + undefined) // 'hello' + 'undefined' 複製程式碼
數字 VS 加號連線符
數字 + 基本型別(非字串) = 數字型別 + Number(基本型別)
// 數字 + 布林 console.log(1 + true) // 2 // 等同於 console.log(1 + Number(true)) // 1 + 1 // 數字 + undefined console.log(1 + undefined) // NaN // 等同於 console.log(1 + Number(undefined)) // 1 + NaN // 數字 + null console.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 不等於 NaN console.log(NaN == NaN) // false // undefined vs null console.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]' 複製程式碼
案例一:
目的:驗證非自定義物件的隱式轉換過程
// 申明一個物件obj1 var 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(),來驗證物件的隱式轉換過程
// 申明一個物件obj2 var 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()) // string console.log(obj2.valueOf()) // 18 console.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) // object console.log(null instanceof Object) // false console.log(NaN == NaN) // false console.log(null == undefined) // true console.log(Number(null)) // 0 console.log(Number(undefined)) // NaN console.log(0 == '') // true console.log(0 == '') // true console.log('' != ' ') //true console.log(null != 0) // true console.log(undefined != 0) // true 複製程式碼
寫在最後
通過上面對JavaScript中的資料型別的隱式轉換可以總結出以下結論:
- JavaScript中運算子在運算時,最終都將轉換成相同型別進行運算(字串型別、數字型別)
- 字串與加號連線符運算時轉換成String型別
- 非字串加號連線符的運算都將轉換成Number型別
- 特別注意引用型別的隱式轉換是先判斷valueOf()返回的型別,如果返回是引用型別則呼叫toString()返回對應的字串值,最終都是按照1,2,3的規則進行運算。
以上內容雖然有進行驗證,但不知道描述上是否存在歧義,有些點表述的不是很清楚,望諒解。
如果有發現任何問題或者有更好的建議,歡迎直接給我留言。
交流
更多精彩內容請關注我的github部落格,如果你覺得還不錯請給個star,非常感謝。