本文主要會談到關於陣列、字串、數字的錯誤操作方式與疑難雜症的解法。
...
...
寫程式粗心大意可是會爆炸的喔!
陣列是由數值做索引,可由任何型別值所構成的群集。在這裡要先提到兩個容易誤用的重點-(1) 稀疏陣列誤存 undefined 的元素 和 (2) 使用「很像數字」的字串當成鍵值來存資料時,鍵值被強制轉型為數字的狀況,最後會提到「類陣列」的操作。
稀疏陣列是指陣列中有插槽(slot)可能未定義其值或被略過而導致存放 undefined 元素的狀況,範例如下。
const list = [];
list[0] = 'Hello';
list[2] = 'World';
list[1] // undefined
list.length // 3
這會有什麼問題呢?
由於這可能是一些疏忽或錯誤操作所造成的,因此會對 lenghth 有錯誤的期待,例如,可能原本期待 list 的長度為 2,但因錯置了字串 'World'
的位置,導致 list 的長度為 3,在之後陣列的操作上可能會出現很難發現的 bug。
...
...
這種 bug 就是所謂的 地雷,你永遠不知道它什麼時候會爆炸,一旦爆炸就死傷慘重、很難挽救。
若使用「很像數字」的字串當成鍵值來存資料,鍵值會被強制轉型為數字,這也會造成後續處理上的難題,像是產生剛剛提到的稀疏矩陣的狀況(又是地雷一枚)。
const list = [];
list[0] = 'Hello';
list['20'] = 'World';
list['20'] // 'World'
list.length // 21
陣列其實也就是物件的子型別而已,所以若想用字串當成鍵值來存放資料也是可以的,只是鍵值會被強制轉型為數字。如上所示,鍵值 '20'
被強制轉為數字 20,導致 list 成為稀疏陣列,其長度就被誤判了。因此,若索引值是數字就用陣列,而非數字就用物件吧!
...
...
是不是讓你想到身邊的某些人呢?還是其實就是你自己?
老話一句,「加油,好嗎?」
類陣列是指以數值索引的值所成的群集,它可能是串列但並非真正的陣列,例如:DOM 物件操作後所得到的串列、函式引數所形成的串列(ES6 已棄用)。而為了能操作這些類陣列的元素,就必須將類陣列轉為真正的陣列,這樣就能進行 indexOf、concat、forEach 等的操作了。
DOM 物件操作後所得到的串列,範例如下。
const list = document.getElementsByTagName('div');
list // HTMLCollection(3) [div, div, div]
list.length // 3
函式引數所形成的串列,範例如下,取得不定個數的引數。
function foo() {
const arr = Array.prototype.slice.call(arguments);
console.log(arguments); // (1)
console.log(arr); // (2)
}
foo('hello', 'world', 'bar', 'baz');
得到
Arguments(4) ["hello", "world", "bar", "baz", callee: ƒ, Symbol(Symbol.iterator): ƒ]
(4) ["hello", "world", "bar", "baz"]
以上可知,函數引數所形成的類陣列,在經過 slice 轉換後可得到真正的陣列以供後續操作。注意,slice 會回傳一個指定開始到結束部份的新陣列,因此在不傳入任何參數的狀況下等同於複製陣列。
或使用 Array.from
也會有同樣的效果。
function foo() {
const arr = Array.from(arguments);
console.log(arguments); // (1)
console.log(arr); // (2)
}
foo('hello', 'world', 'bar', 'baz');
// Arguments(4) ["hello", "world", "bar", "baz", callee: ƒ, Symbol(Symbol.iterator): ƒ]
// (4) ["hello", "world", "bar", "baz"]
這部份還是繼續來看關於類陣列的處理。
JavaScript 在創建變數、賦值後是可變的(mutable);相較於 mutable,不可變(immutable) 就是指在創建變數、賦值後便不可改變,若對其有任何變更(例如:新增、修改、刪除),就會回傳一個新值。
當需要更新一個變數的時候,若值的型態為基本型態,則是不可變的,意即只要改變就會回傳一個新的值;若值的型態為物件型態,則由於物件是使用 call by reference 的方式共享資料來源,因此只是就地更新而已,或說是更新這個位置所儲存的值,而非回傳一個新的值。
字串可不可以當成陣列來處理呢?可以的,而且可以借用陣列的方法來做些事情,只是要注意,不能變更陣列的內容。
如下,借用陣列的 join 來實作在字串間插入字元。join 和 map 都不會變動到原始陣列的內容,因為回傳的結果是一個新的值。
const str = 'foo';
const str_another = Array.prototype.join.call(str, '--');
const str_the_other = Array.prototype.map.call(str, (char) => {
return `${char.toUpperCase()}.`
}).join('');
str_another // f--o--o
str_the_other // F.O.O.
但 reverse 是會改變原始陣列資料的,因此字串就不能借用。如下所示,arr 經反轉由 ['b', 'a', 'r']
改變為 ["r", "a", "b"]
。
const arr = ['b', 'a', 'r'];
arr.reverse(); // ["r", "a", "b"]
arr // ["r", "a", "b"]
所以若想借用陣列的 reverse 來反轉字串,就會被報錯了。
const str = 'foo';
const str_another = Array.prototype.reverse.call(str);
// Uncaught TypeError: Cannot assign to read only property '0' of object '[object String]' at String.reverse
面對無法借用陣列方法的狀況,可先將字串轉為陣列,在進行操作(像是反轉),最後再轉回字串即可。
const str = 'foo';
const str_the_other = str.split('').reverse().join('');
str_the_other // 'oof'
但以上是不是看起來很醜陋又麻煩?因此最好的方法是先把資料存成陣列,再使用陣列的方法操作,後續若需要使用字串表示,再用 join 打平串起來就可以了!
...
...
看到這裡是不是覺得人生很難?
JavaScript 的數字(number)型別包含兩種-整數和帶有小數的浮點數,其中數字的實作是以 IEEE 754 為標準,也就是浮點數(floating-point number)的雙精度(double precision)格式,意即 64 位元的二進位數字。
以下來探討一些疑難雜症。
非常大或非常小的數值以「指數」的方式呈現。
const a = 1E20;
const b = a * 100;
const c = a / 0.001;
a // 100000000000000000000
b // 1e+22
c // 1e+23
// 使用 toExponential 手動轉指數呈現
a.toExponential() // "1e+20"
使用 toFixed 指定要顯示的小數位數,會做四捨五入,不足會補零,注意結果會以「字串」格式呈現。
const a = 123.456789;
a.toFixed(1) // "123.5"
a.toFixed(2) // "123.46"
a.toFixed(3) // "123.457"
a.toFixed(10) // "123.4567890000"
使用 toPrecision 指定有效位數,會做四捨五入,不足會補零,注意結果會以「字串」格式呈現。
const a = 123.456789;
a.toPrecision(1) // "1e+2"
a.toPrecision(2) // "1.2e+2"
a.toPrecision(3) // "123"
a.toPrecision(4) // "123.5"
a.toPrecision(5) // "123.46"
a.toPrecision(10) // "123.4567890"
注意,數字後加上 .
會讓 JavaScript 引擎先判定為小數點,而非屬性存取。因此,若希望 100.toPrecision(1)
能正常顯示,應該為 100..toPrecision(1)
或 (100).toPrecision(1)
。
0xAB // 171
0o65 // 53
0b11 // 3
...
...
頭昏眼花了嗎?0x
、0o
、0b
可不是表情符號喔!
只要是使用 IEEE 754 來表示二進位浮點數的程式語言都有一個夢靨-無法精準地表示十進位的小數,範例如下。
0.1 + 0.2 === 0.3 // false
將 0.1、0.2 和 0.3 分別轉為二進位來看
因此 0.1 + 0.2 永遠不會剛好等於 0.3。
解法是取一個很小的誤差當作容許值,若運算結果小於這個誤差值就判斷為等於,在 ES6 中已定義好這個常數 Number.EPSILON
其值為 2^-52,或實作 polyfill 如下。
if (!Number.EPSILON) {
Number.EPSILON = Math.pow(2,-52);
}
那...要怎麼使用這個 Number.EPSILON
呢?先實作一個函式 equal,它會判斷誤差是否小於容許值-先將兩輸入值的差取絕對值,再與 Number.EPSILON
做比對,若小於這個誤差值就判斷為兩數相等。
function equal(n1, n2) {
return Math.abs(n1 - n2) < Number.EPSILON;
}
var a = 0.1 + 0.2;
var b = 0.3;
equal(a, b); // true
equal(0.0000001, 0.0000002); // false
ES6 定義所謂「安全」的數值範圍為
Number.MAX_SAFE_INTEGER
(其值為 2^53 - 1 等於 9007199254740991)、最小整數 Number.MIN_SAFE_INTEGER
(其值為 -9007199254740991)。Number.MAX_VALUE
(其值為 1.798e+308)、最小浮點數 Number.MIN_VALUE
(其值為 5e-324)。使用 Number.isInteger
來測試數值是否為整數。
Number.isInteger(42); // true
Number.isInteger(42.000); // true
Number.isInteger(42.3); // false
使用 Number.isSafeInteger
來測試數值是否位在安全範圍內。
Number.isSafeInteger(Number.MAX_SAFE_INTEGER); // true
Number.isSafeInteger(Math.pow( 2, 53 )); // false
Number.isSafeInteger(Math.pow( 2, 53 ) - 1); // true
polyfill。
if (!Number.isSafeInteger) {
Number.isSafeInteger = function(num) {
return Number.isInteger( num ) &&
Math.abs( num ) <= Number.MAX_SAFE_INTEGER;
};
}
部份運算(例如:位元運算子 bitwise operator)只允許使用 32 位元的有號整數,其範圍為 Math.pow(-2,31)
到 Math.pow(2,31)-1
。
在做這類運算前必須先把數值使用 | 0
轉為 32 位元的有號整數
const integer = 123456789;
const signed_integer = integer | 0;
看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到...
同步發表於部落格。
是不是讓你想到身邊的某些人呢?還是其實就是你自己?
老話一句,「加油,好嗎?」
這話有言外之音@@? 好特別存在的一句話@@
其實這是發生在我前同事(真的不是我)的類似故事,很驚人的哈哈哈
會特別發現...是因為...18年前,也收過『加油』這兩個字...
已經到了,隨便就是8年前,18年前...