keywords:implicit coercion
,Addition Operator
,Strings <> Numbers
隱含的強制轉型 ( implicit coercion )
- 隱含的強制轉型就是 ( 對你來說 ) 不明顯且有副作用的任何型別轉換動作
- 明確的強制轉型就是讓程式碼更清晰明白,更容易理解;隱含的強制轉型就是完全相反的東西,讓程式碼更難以理解。
- JavaScript 隱含的強制轉型是抱怨強制轉型的主要來源,但隱含的強制轉型是邪惡的嗎? 作者認為也許可以從不同的角度來看看隱含的強制轉型是什麼,可以怎麼用,而非只是說『 它是好的明確強制轉型的反面 』
- 自己認為如果一個程式語言能夠為大家所用,那一定有其設計語言的便利性及原因,不然請非本科系的學 Java 或 C# 等物件導向的語言,我想應該沒幾個有興趣吧
- 隱含的強制轉型目標是為了減少冗長、反覆套用,以及非必要的實作細節,降低程式碼雜亂,以專注於更重要的目的
簡化隱含
先來看一個例子:
SomeType a = SomeType(AnotherType(b));
在這個例子中想要把擁有其他型別 b 轉為 SomeType,但這個語言無法直接從 b 目前的型別轉為 SomeType,它需要先轉為 AnotherType 再從 AnotherType 轉為 SomeType
現在有一種語言能夠這樣做:
SomeType a = SomeType(b);
大家不會同意在此簡化了型別轉換動作,減少了非必要的中間轉換型別的步驟嗎? 意思是看到並處理『 b 會先轉為 AnotherType,然後再變成 SomeType 』這個事實,真的有這麼重要嗎? 🤔
簡化的動作實際上增進了程式碼的可讀性
作者給大家的鼓勵是:不要滿足於此。別在到洗澡水時,連小嬰兒都一起倒掉了,別假設隱含的強制轉型全都是壞的
自己的想法是後端型別的轉換明確是很重要的,因為後端需要處理大量資料邏輯,且與資料庫有熱絡的溝通,如果型別不精確,會導致資料不精確造成公司損失,例如:原本需要精度高的型別 float 但卻用了 integer 尤其在銀行或是科學界,小數位數可能就會造成外幣兌換數目落差及火箭爆炸等災難;相對於前端 JavaScript 精確的型別並不是這麼重要,前端是注重使用者體驗的介面,使用者根本不會在意看到的數字是文字型別或數字型別
隱含地:Strings <> Numbers
[23] 強制轉型 - 明確的強制轉型、Strings <> Numbers、日期轉 Number、位元運算子有講到明確地 Strings <> Numbers 轉型,在開始講隱含地轉型前,先檢視一下隱含地強制進行型別轉換作業的一些細微之處
+運算子被重載了 ( overloaded ),用於 number 的相加以及 string 的串接這兩項工作,但 JavaScript 如何得知要哪種作業呢?
ex1: let a = '98'; let b = '7'; let c = 98; let d = 7; a + b; // '987' c + d; // 105 --------------------------------------- ex2: let x = [1,2]; let y = [3,4]; x + y; // '1,23,4'
常見的誤解是在於運算元是否有一個或兩個是 string,表示 + 會假設要進行的是 string 的串接,但在 ex2 又好像並非如此, 🤔 x y 皆非 string 但顯然它們都被強制轉型成了 string ,但為何不是強制轉型成 number?
- 根據 2021 語言規格的 12.8.3 The Addition Operator ( + ) 參考到的 12.15.5 Runtime Semantics: ApplyStringOrNumericBinaryOperator
步驟如下:- 運算子為 +
1-1. 將左右參數使用 ToPrimitive 抽象運算取得原生型別值
1-2. 如果左右參數其中一個型別為 string ,就將雙方都用 ToString 轉成 string 並做字串的連結並回傳 - 非 + 運算子或如果左右參數在使用 ToPrimitive 後皆非 string 型別,那一定是數字的運算
2-1. 原生型別值做 ToNumber,如果轉型後的左右參數型別不相同,擲出一個 TypeError
2-2. 如果 ToNumber 後左右參數相同就做數字的運算並回傳
- 運算子為 +
Note:所有的標準物件 ToPrimitive 的 hint 都是 Number 除了 Date 物件,換句話說
- 除了 Date 物件以外的物件 ToPrimitive 的順序:valueOf() 優先如果回傳非基本型別值再進行 toString()
Date 物件的 ToPrimitive 的順序:toString() 優先如果回傳非基本型別值再進行 valueOf()
let a = new Date(); a.valueOf(); // 1590822045060 a.toString(); // "Sat May 30 2020 15:00:45 GMT+0800 (台北標準時間)" a + 9; // "Sat May 30 2020 15:00:45 GMT+0800 (台北標準時間)9" a.toString = null; // 將 toString 方法設置為 null 😏 a + 9; // 1590822045069 ( 1590822045060 + 9 )
蠻有趣的啊 XD
所以講解上面 x y 的例子:
1.上面兩個 x y [] 串接的步驟會先走到 1-1.將左右參數使用 ToPrimitive,而 Array 非 Date 物件所以 hint 為 Number 也就是會先執行 valueOf() ,但沒辦法產生一個簡單的基型值而失敗
2.所以它改用 toString() 表示法
3.根據步驟 1-2 雙方其中一個型別為 string 故進行 ToString 進行字串的連結
step 1:
x.valueOf(); // [1,2]
y.valueOf(); // [3,4]
---------------------------------------
step 2:
x.toString(); // '1,2'
y.toString(); // '3,4'
---------------------------------------
step 3:
'1,2' + '3,4'; // '1,23,4'
Tips:如果 + 的任一邊運算元是 string ( 或是透過上面的步驟 1-1 能變成一個字串 ),那麼進行的動作就會是 string 的串接,否則的話一定會是數值的加法運算
程式裡我們經常使用這個隱含的強制轉型:
let a = 98;
let b = a + '';
b; // '98'
a + ''
a 會先使用 ToPrimitive 調用 valueOf() 後得到 number 基本型別值,便會停止不再調用 toString() ,但因 '' 為一個 string 型別值,所以雙方都會使用 ToString 被轉為一個 string。
a + ''
及 String(a)
都會產生一個 string ,但如果使用的是 object 而非正規的 number 基本型別值,不一定會得到相同的 string 值,因為 String(a) 使會直接調用 toString() 方法的
let a = {
valueOf: function(){ return 98 },
toString: function(){ return 7 }
};
a + ''; // '98'
String(a); // '7'
如果今天要把 string 隱含地強制轉型為 number 呢?
let a = '98.87';
let b = a - 0;
b; // 98.87
( - ) 運算子被定義用於數值減法運算,所以 a - 0 會使 a 的值被強制轉型為一個 number , a * 1 或 a / 1 也能達成相同的效果,因為這些運算子同樣也只定義給數值運算使用。
let x = [9];
let y = [7];
x - y; // 2
這兩個 array 值都必須變成 number,先進行 valueOf() 回傳還是陣列,故再經由 toString() 轉為基本型別值 string,然後再強制轉型為 ToNumber,以便進行 - 的減法運算
最後比較 明確的 b = String(a)
vs. 隱含的 b = a + ''
這兩種做法都能說是有用的,但 b = a +''
在 JS 程式中要常見得多,這證明了它的實用性。