[JS] 深入了解型別與轉型
JavaScript 不論遇到什麼奇形怪狀的資料運算,都會盡可能讓程式執行下去,這麼貼心的設計也是兩面刃,可能在不知不覺中程式正常運作,也可能突然就報錯了卻毫無頭緒,馬上來認識 JavaScript 的型別系統與轉型吧!
💎 型別(type)
JavaScript 的型別可以分為『原始型別』和『物件型別』兩類,差異在於:
- 原始型別
沒有
屬性和方法可以使用 - 物件型別
可以任意存取
屬性和方法
- 原始型別
原始型別(Primitive types)
- string
- number
- boolean
- null
- undefined
- symbol (ES 6 新定義)
物件型別(Object types)
- 原生物件(Native)
- Object, Array, Function, Date, Math, RegExp
- 原始型別包裹物件(Primitive Wrapper):Number, String, Boolean
- 寄宿物件(Host)
- window
- DOM
- 原生物件(Native)
型別檢查(typeof):
除了 null 有原生的 bug 會得到 object 的結果(此 bug 不會修正),其他型別都可以使用typeof
來檢查。1
2
3
4
5
6
7
8console.log(typeof 'a'); // string
console.log(typeof 1); // number
console.log(typeof true); // boolean
console.log(typeof null); // object <== 這是語言本身的bug
console.log(typeof undefined); // undefined
console.log(typeof Symbol()) // symbol
console.log(typeof {}); // object
console.log(typeof []); // object
💎 裝箱(boxing)和 拆箱(unboxing)
前面提到原始型別是沒有屬性和方法可以使用的,但我們卻可以利用字串方法來修改原始型別的字串(如下方的程式碼),這是因為 JavaScript 有 裝箱(boxing)和 拆箱(unboxing)機制來處理這些操作。
1
2console.log('123.45'.split('.'));
// ['123', '45']上個段落的物件型別中有一項
原始型別包裹物件(Primitive Wrapper)
,使用這個方式來建立的字串(數字或布林值)會是物件的形式,並且有許多原生方法可以使用,JavaScript 便是利用這個物件型別來實作裝箱與拆箱。1
2
3
4
5let str = '123';
let strObj = new String('123');
console.log(typeof str); // string
console.log(typeof strObj); // object裝箱(boxing):當 JS 執行方法發現資料是原始型別時,便會使用
new
把原始型別資料建立成物件型別的資料,就可以順利執行物件內的方法。拆箱(unboxing):當 JS 完成被裝箱的物件需要執行的方法後,就會把這個物件資料還原成原始型別的資料,這時會用到兩個重要的方法,這會在接下來介紹的『轉型』再次談到:
valueOf()
:回傳Number
原始型別。toString()
:回傳String
原始型別。
💎 強制轉型(coercion)
- 強制轉型分為兩種:
顯性轉型
(explicit coercion)、隱性轉型
(implicit coercion)
🔸 顯性轉型(explicit coercion):
顯性轉型指的是在程式碼中
直接撰寫
的型別轉換。轉數字
parseInt()
:- 轉整數。
- 小數點以後無條件捨去。
- 轉換方式:由左至右,遇到無法轉成數字的時候停止,回傳前面的數字。
1
2
3console.log(parseInt('1.23')); // 1
console.log(parseInt('1.23a')); // 1
console.log(parseInt('a1.23')); // NaN
parseFloat()
:- 轉浮點數(包含小數點以後)。
- 轉換方式與 parseInt() 相同。
1
console.log(parseFloat('1.23')); // 1.23
Number()
:- 可以轉整數和浮點數。
- 數字範圍在正負 2的53次方 -1 間。
- 轉換方式:整筆字串都可以轉成數字才轉換,否則得到 NaN。
1
2console.log(Number('-1.23')); // -1.23
console.log(Number('-1.23a')); // NaN
BigInt()
:- 轉整數
- 範圍可以超過2的53次方(表示方式會在數字後面加上
n
) - 轉換方式:非整數時直接報錯,不會得到 NaN。
- 需注意瀏覽器支援度。
1
2
3console.log(BigInt("9007199254740991")); // 9007199254740991n
console.log(typeof 1n); // bigint
console.log(BigInt("900a")); // Cannot convert 900a to a BigInt
算數運算子
:- 前方加上
+
:可能會和++
混淆。 - 前方加上
-
:內容若是負數,會被轉成正數。 - 後方加上
- 0
:較穩定寫法。1
2
3
4console.log(+'-1'); // -1
console.log(++'-1'); // Invalid left-hand side expression in prefix operation
console.log(-'-1'); // 1
console.log('-1' - 0); // -1
- 前方加上
轉字串
toString()
:不能轉換 null 和 undefined,可以設定進位制。1
2
3console.log((3).toString()); // 3
console.log(undefined.toString()); // Cannot read properties of undefined
console.log((3).toString(2)); // 11String()
:可以轉換 null 和 undefined,不能設定進位制。1
2console.log(String(3)); // 3
console.log(String(undefined)); // undefined算數運算子
:加上空字串+ ''
。1
console.log(typeof (3 + '')); // string
轉布林值
布林值只有兩種值(true 或 false),轉型後會得到 true 的值稱為 truthy,得到 false 的則是 falsy,除了下列項目是
falsy
,其他的都是truthy
:false
0
-0
0n
(BigInt)“”
(空字串,包含 ``, ‘’)null
undefined
NaN
document.all
(正常情況下不會用到)
轉換方式:
Boolean()
:1
console.log(Boolean([])); // true
邏輯運算子
:前方加上雙驚嘆號!!
,單驚嘆號!
會是相反結果。1
2console.log(!true); // false
console.log(!!{}); // true
🔸 隱性轉型(implicit coercion)
隱性轉型出現在使用運算子時,這在其他程式語言通常是不允許的(需要相同型別才可以運算),JavaScript 則會自動轉型讓程式繼續執行,也因為規則繁雜,容易疏忽出錯。
核心方法
ToPrimitive
JavaScript 在執行運算時會使用ToPrimitive
方法將運算元轉換成原始型別後就能繼續運算,ToPrimitive
帶有一個hint
(提示),用來決定要轉成什麼型別,如果要了解詳細的運作順序可以參考 深入理解Javascript中Object类型的转换 這篇文章,裡面引用了 ECMA 的規範詳細解說,非常值得一讀!原始型別的隱性轉型
+
算數運算子:加法運算中只要有一個值屬於字串型別,就會全部轉為 string,最高優先。1
2
3
4// 有字串
console.log('' + 1 + null + true); // 1nulltrue
// 無字串
console.log(1 + null + true); // 2其他算數運算子:
-
、*
、/
、%
運算都會轉成 number。1
2
3
4console.log('6' - undefined); // NaN
console.log(9 * [9]); // 81
console.log('3' / {}); // NaN
console.log(6 % true); // 0
物件型別的隱性轉型
toPrimitive
執行物件型別的轉換時會使用到物件內的valueOf
或toString
方法來取得原始型別的回傳值,因為這是物件內的方法,我們可以透過覆蓋方法來改變回傳值:1
2
3
4
5
6
7
8
9
10
11
12// 刻意改成 valueOf 回傳字串、toString 回傳數字
let a = {
valueOf: function() {
return '1';
},
toString: function() {
return 2;
}
};
console.log(a + 0); // "10"
console.log(a.toString()); // 2陣列
也是物件,ToPrimitive 會回傳陣列本身內容執行toString()
方法後的字串。1
2
3
4
5// 空陣列轉字串 "",空物件轉字串 "[object Object]"
console.log([] + {}); // "[object Object]"
// 陣列轉字串 "1,2,3",後方數字配合轉字串
console.log([1,2,3] + 2 + 1); // "1,2,321"{} + 任意值
:{}
在運算元前方時會被視為區塊語句
,而不是物件,所以實際執行的只有後面的+ x
+ 任意值
是前一段落提到的顯性轉型
轉數字的方法,所以後方的運算元轉為 number 型別。1
2
3
4
5
6
7
8
9
10
11
12
13// 強制轉型
console.log(+{}); // NaN
// 看起來是物件加陣列,實際上是後方的陣列強制轉型為數字
console.log({} + []); // 0
// 上方程式實際執行時等同下面兩行:
{}
+[];
// 宣告一個物件變數 x 來存放空物件,才能被當成物件來計算
let x = {};
console.log(x + []); // "[object Object]"
{} + {}
:這是一個特別狀況所以額外說明,不同的瀏覽器會有不同的執行結果:- NaN - 第一個 {} 被視為區塊,
- “[object Object][object Object]” - 第一個 {} 被視為物件
邏輯運算
邏輯運算中的運算元都會被轉為 boolean,差別在於其運算後回傳的結果。|| (or) 會回傳第一個結果為 true 的運算元,若無,則是最後一個。
1
2console.log(0 || false || null || undefined); // undefined
console.log(0 || false || null || {} || undefined); // {}&& (and) 若運算結果為 true,會回傳最後一個運算元,若運算結果為 false,回傳第一個結果為 false 的運算元。
1
2console.log(true && {} && -2 && ['a']); // ['a']
console.log(1 && 0 && true & false); // 0
💎 結語
關於轉型的細節實在太多,族繁不及備載,一些極端的範例在實際撰寫時並不會用到,不求成為行走的 MDN,但求踩坑的時候能順利 debug 就好!
參考文章
MDN-BigInt
sunnyhuang-何謂強制轉型(coercion)以及如何作到轉換型別
MDN-Number.prototype.toString()
淺談JS中String()與.toString()的區別 - 程式前沿
MDN-Symbol.toPrimitive。
Eddy 思考與學習-JS中的 {} + {} 與 {} + [] 的結果是什麼?