keywords:explicit coercion
,Strings <> Numbers
,Date <> Numbers
,bitwise operators
明確的強制轉型 ( explicit coercion )
目標是辨識出程式碼中的某些模式,更清楚地表示要將某個值從一個型別轉為另一個,留下讓未來開發人員一看就理解的程式碼
明確地:Strings <> Numbers
要在 string 與 number 之間進行強制轉型,我們會使用內建的 String(..) 、 Number(..),要注意的是我們不在前面加 new 因為使用 new 關鍵字等於是在建立物件包裹器 ( object wrappers ),a 會是一個物件型別
let a = new String('123');
let b = String('123');
let c = '123';
typeof a; // object
🚩 b === c; // true 這兩種寫法是完全相等的
typeof c; // string
String(..) 及 Number(..) 會把任何的值強制轉型為 string / number 基本型別值 ( primitive value )
使用的運算規則就如前兩章提到的方法:
- String(..) [20] 強制轉型 - 轉換值、ToString、JSON 提到的 ToString 方法
Number(..) [21] 強制轉型 - ToNumber、ToPrimitive、StringNumericLiteral、NonDecimalIntegerLiteral 提到的 ToNumber 方法
看看例子:let a = 99; String(a); // '99' String(9.9e5); // '990000' 只是好奇指數會不會展開 ㄏ let b = '1.5'; Number(b); // 1.5 Number('150000'); // 150000
除了 String() 與 Number() ,還有可以在 string 與 number 兩者之間明確地進行轉換
let a = 99;
a.toString(); // '99'
let b = '1.5';
+b; // 1.5
這裡的 a 看起來是明確的轉換,但實際上仍然有隱含性,toString() 不能在像 99 這樣的基本型別值上被呼叫,我們在前面 [17] 原生功能 Natives - 內部特性、封裝、解封裝 有做過筆記:
基本型別值沒有特性 ( properties ) 或方法 ( methods ),所以要存取 .length 或 .toString() 時會需要包裹基型值的物件包裹器,幸好 JavaScript 會自動封裝 ( box 也就是 包裹 wrap )
所以其實 JavaScript 有自動把 a 封裝在一個物件包裹器,如此 toString() 能夠在該物件上被呼叫,可以稱為『 明確地隱含 ( explicitly implicit ) 』🤔
- +b 中 + 運算子的單運算元 ( unary operator ) 形式 ( 即只有一個運算元的運算子,簡單說就是只有一個要計算的變數碰上的 + ),不會進行數學的加法運算或字串串接動作,單元的 + 會明確地把運算元 b 強制轉型為一個 number 值
- 普遍的觀點是 + 可被接受為明確強制轉型的一種形式,不過要取決於我們的經驗與觀點,如果我們知道單運算元 + 是明確的進行 number 的強制轉型,就是明確的,如果我們從未看過它.對我們來說就是隱含的、令人困惑的、有隱藏副作用的
罷特! 不能把兩個加號相連放在隔壁 ( ++ ),這樣會被解讀為遞增運算子,所以應該要在中間放一個空格 3+ +b
let b = '1.5'; 3+ +b; // 4.5 3++b; // SyntaxError: Invalid left-hand side expression in postfix operation
也不能將兩個減相連放在隔壁 ( -- ) 試圖要做一個負負得正的數學運算,因為這樣會被解讀為遞減運算子,如果想要負負得正一樣中間要放一個空白 - -b
- -b; // 1.5 --b; // 0.5
如果有與其他運算子相鄰的運算,建議避免使用單運算元 + / - ,因為會使人混淆,降低可閱讀性
日期轉為數字
單元 + 運算子除了用在 string 強制轉型為 number 之外,常見的另外一個用途是將 Date 物件強制轉型為一個 number,因為會是日期時間值的 Unix ( Unix timestamp 從 1970 年 1 月 1 日 00:00:00 UTC 開始計算的毫秒數) 時戳
let a = new Date('Sun,24 May 2020 10:40:43 GMT+8'); // Sun May 24 2020 10:40:43 GMT+0800 (台北標準時間) +a; // 1590288043000 這個時間點的時間戳 typeof a; // object typeof +a; // number
日期物件最常的使用情境是取得當下的時間戳
+new Date(); // 1590288043000
JavaScript 中一個奇特的語法,只有在沒有引數要傳入時,建構器呼叫 ( 帶有 new 函式呼叫 ) 的 () 會是選擇性的,+new Date ,但那個 () 會提升易讀性,並非所有開發人員都同意省略
+new Date; // 1590288043000 +new Array; // 0 +new String; // 0
- 不過作者建議不要使用與日期有關的強制轉型形式,要產生現在的時戳夠好的做法就用 ES5 新增的 Date.now() 靜態函式
如果要取得所指定的非現在日期時間之時戳,就使用 new Date().getTime();
Date.now(); // 1590288043000 new Date('Sun,24 May 2020 10:40:43 GMT+8').getTime(); // 1590288043000 這邊要注意不能只寫 GMT 因為那會是格林時間 +0 台灣是 +8 所以要記得寫 GMT+8
- 如果想要把時間戳轉成日期時間,可以使用 new Date(時間戳)
new Date(1590288043000); // Sun May 24 2020 10:40:43 GMT+0800 (台北標準時間)
~位元運算子
- 位元運算子將運算元視為一段帶號的 32 位元整數的 0 和 1 序列,而不是十進位、十六進位或八進位的 Number
看一下 規格書 2021 7.1.5 ToInt32:
接著看一下 MDN 文檔的位元運算子描述:
反轉的意思就是把二進制的 1 轉成 0 0 轉成 1
3 = 00 00000 00000 00000 00000 00000 00011 ~3 = 11 11111 11111 11111 11111 11111 11100 二補數系統確保了正值時最左邊的位元為 0,反之則為 1。因此,最左邊的位元被稱作符號位。 如果要把一個負的二進制轉成正數,就要把 1 轉成 0 0 轉成 1 然後再 +1 但前面要加一個負號 ~3 = 11 11111 11111 11111 11111 11111 11100 = 00 00000 00000 00000 00000 00000 00011 +1 = 00 00000 00000 00000 00000 00000 00100 = 100 前面 0 省略 = (1*2^2) + (0*2^1) + (0*2^0) ^ 符號這邊的意思是用數學次方的意思 = 4 + 0 + 0 不要忘記負號 = -4 ∴ ~3 等於 -4
簡單說 ~ 會先強制轉型為一個 32 位元的 number 值,然後進行反轉, ~x 大略等於 -(x+1)
~3; // -(3+1) equal -4
數字中唯一會使 Boolean ( x ) 為 false 的數字是 0 / -0,所以 ~-1 就會產生 0 ,-1 常被稱為哨符值 ( sentinel value ) 被賦予了一個特殊意義
C 語言中有許多函式都使用 -1 作為哨符值, -1 代表失敗
JavaScript 中 indexOf(..) 就採用了此先例,通常 indexOf() 都還會被檢查是否為 boolean ,判斷某個子字串是否出現在另外一個 string 中, 例如:let a = 'You dont know JS'; a.indexOf('ow') >= 0; // true 有找到 a.indexOf('wo') >= 0; // false 沒找到
這是一種容易洩漏資訊的抽象層,它把底層的實作行為,-1 表示失敗洩漏到程式碼中,這時候 ~ 就派上用場了
~a.indexOf('ow'); // -12 有找到 ~a.indexOf('wo'); // 0 沒找到
嚴格說 if(~a.indexOf(..)) 仍然仰賴隱含的強制轉型,它將結果 0 轉為false