昨天我們介紹了 undefined
、null
、NaN
,也帶到了如何將這些特別的值判斷出來。
今天我們要來看更多,更多這些會讓人感到困惑的判斷式,以及它們在實戰中誤用會發生什麼慘況。
本篇文章會參考 JavaScript-Equality-Table,這是個視覺化又好懂的整理圖表,非常推薦在感到困惑的時候來看看。
主要有兩個主題:
這個會是我們通篇討論的基礎,一律使用嚴格相等( === )來判斷。
一般相等的轉型滿複雜的,而且轉型的規則不是那麼直覺,所以這邊不特別講,可以參考網站中的這張表格,一目瞭然:
按照上圖,我們可能會有這種不太直覺的東西:
if (1 == "1") console.log(' 1 == "1" ');
if (true == "true") console.log(' true == "true" ');
看起來,第一個判斷式如果成立,那第二個應該也要成立吧?都只是數值的外面框上一個雙引號變成字串呀?
但結果只有第一個判斷式成立,而第二個卻不是。
執行結果
1 == "1"
諸如此類的神奇的轉型規則還有很多,可以自行參照上圖,但正是因為轉型規則太複雜,要使用還得不斷思考轉型後是否如預期,背一大堆轉型的例外。
因此我們才會說一律使用嚴格相等,果斷放棄轉型,確保等號兩邊一定是「基於型別相等的比較」,比較合乎邏輯。
Javascript 真的是有很多奇特的點,雖然嚴格相等已經比一般相等容易理解了,但還是有原則與例外,先來看網站上的圖表:
是不是比一般相等乾淨多了?
基本上嚴格相等就是要符合這兩點,才會成立:
需特別注意這邊所謂的「同值」,會根據數值本身是 primitive 或 non-primitive 而又有不同,所以應該這樣寫:
所以才會看到上圖中,綠色的格子並沒有填滿 []
、{}
、[0]
這種 non-primitive 數值,代表以下都是不成立的:
console.log([] === []); // false
console.log({} === {}); // false
console.log([0] === [0]); // false
基本上每次產生一個 object,記憶體位址就是新的一個,除非把 object 放到變數存起來,否則 non-primitive 的比較通常都不會成立:
const person = { name: 'Joey' };
console.log(person === person); // true
console.log(person === { name: 'Joey' }); // false
經過拷貝後呢?因為拷貝其實是把原本 object 內的 key/value 複製到一個新的 object,所以仍然會有一個全新的記憶體位址產生:
const person = { name: 'Joey' };
const copiedPerson = { ...person };
console.log(person === copiedPerson); // false
就是 NaN
,請參考昨天的文章:
console.log(NaN === NaN); // false
判斷是否相等時,除了 NaN
要用 isNaN()
來判斷,其他全都用嚴格相等,並且特別注意 non-primitive 的比較。
在搞定了一般相等與嚴格相等之後,我們接著來看另一種常見的寫法,就是更懶一點,連等號都不寫了,直接把變數或數值塞在判斷式裡面:
if (300) conosle.log('會執行');
if ('Hello') conosle.log('會執行');
if ([1,2,3,4]) conosle.log('會執行');
這種判斷式裡面也會轉型,不過比較單純一點,單純就是幫你轉成 true 或 false。
會被轉成 true 的數值,就叫做 truthy value,反之叫做 falsy value。
false
, 0
, -0
, 0n
, ""
, null
, undefined
, NaN
可以注意到,有些我們感覺應該要是空值的東西,其實並不是 falsy:
if ([]) conosle.log('會執行'); // 會執行
if ({}) conosle.log('會執行'); // 會執行
if ("0") conosle.log('會執行'); // 會執行
基於這個原因,如果直接把變數放進 if 判斷式轉型,有時候會出意外:
const arr = [
{ name: 'Jack', score: 70 },
{ name: 'Allen', score: 65 },
{ name: 'Alice', score: 60 },
{ name: 'Susan', score: 90 }
];
const failedArr = arr.filter(item => item.score < 60);
if (failedArr) {
const displayFailedArr = failedArr.map(item => item.name).join('、');
console.log(`低於 60 分名單:${displayFailedArr}`);
}
執行結果
低於 60 分名單:
上面的範例,沒有人低於 60 分,所以其實第 7 行的 failedArr
是一個空陣列 []
。
而第 9 行的 if (failedArr)
就會變成 if ([])
,其實沒有打算執行 if 裡面的程式,但因為 []
是 truthy,所以還是會成立,最後印出來的名單就好像缺了什麼東西一樣。
如果要判斷陣列內是否有值,可以使用 Array.isArray(arr)
加上 arr.length > 0
兩個判斷:
const arr = [
{ name: 'Jack', score: 70 },
{ name: 'Allen', score: 65 },
{ name: 'Alice', score: 60 },
{ name: 'Susan', score: 90 }
];
const failedArr = arr.filter(item => item.score < 60);
if (Array.isArray(failedArr) && failedArr.length > 0) {
const displayFailedArr = failedArr.map(item => item.name).join('、');
console.log(`低於 60 分名單:${displayFailedArr}`);
}
執行結果
如果嫌 if 判斷式內太冗長,可以考慮使用 lodash/isEmpty
,或者自行把判斷式寫成共用函式,這樣就不會到處都要寫這麼長:
const isArrayEmpty = (inputArr) => Array.isArray(inputArr) && inputArr.length === 0;
// ... 這邊都跟上個範例一樣
if (!isArrayEmpty(failedArr)) {
// ... 這邊都跟上個範例一樣
}
const arr = [
{ name: 'Jack', score: 70 },
{ name: 'Allen' },
{ name: 'Alice', score: 60 },
{ name: 'Susan', score: 0 }
];
arr.forEach(item => {
if (item.score) {
console.log(`${item.name}:${item.score} 分`);
}
});
執行結果
Jack:70 分
Alice:60 分
上面的範例 arr
有一些缺陷,有一些人是沒有 score
的,面對這種通常會用到 filter
先篩選一遍,把有問題的資料篩掉,或者直接跑 forEach
用 if 來判斷。
所以我們試著把 score
放進 if 判斷式,期待能夠把「有 score
的人都印出來」。
但在眾多 number
裡面,唯獨 0 是 falsy (先不討論 NaN
),所以如果直接把一個 number
放進 if 判斷式,遇到 0 的 case 就會發生意外了(如上例 Susan
沒有被印出來)。
因此,如果是要判斷「是不是數字」,可以用 Number.isInteger()
判斷整數,或 Number.isFinite()
判斷有限數:
const arr = [
{ name: 'Jack', score: 70 },
{ name: 'Allen' },
{ name: 'Alice', score: 60 },
{ name: 'Susan', score: 0 }
];
arr.forEach(item => {
if (Number.isFinite(item.score)) {
console.log(`${item.name}:${item.score} 分`);
}
});
執行結果
Jack:70 分
Alice:60 分
Susan:0 分
同理,如果嫌 if 判斷式內太冗長,也可以拉出去當共用函式:
const isNormalNumber = (inputNum) => Number.isFinite(inputNum);
// ... 這邊都跟上個範例一樣
arr.forEach(item => {
if (isNormalNumber(item.score)) {
// ... 這邊都跟上個範例一樣
}
});
接著回到我們昨天丟下的問題,有些人會想用 「!
」 這個運算子,來一次排除掉這三種空值的概念:
const a = undefined;
const b = null;
const c = NaN;
if (!a && !b && !c) {
console.log('用一個驚嘆號似乎就可以判斷了?');
}
如果看完前面的介紹,你應該就會發現,用這種方式除了會把 undefined
、null
、NaN
這三個濾掉,其實也會把所有 falsy 的數值都濾掉,包括了 false
、0
、""
。
而且如果是想要濾掉 []
、{}
這種,其實是 truthy 的值,反而會達不到效果。
關於這點我其實也找不到一個統一的解,畢竟真的是要看 if 到底要篩選出什麼樣的資料,有時候 null
要可以過,有時候 []
不可以過。
因此我想,認真學會每個類別的判斷方式,並且在適當的時機拿出來用,是比較有效的方式,自己也才真的知道在做什麼。
typeof
Number.isFinite()
Number.isNaN()
Array.isArray()
這個章節寫完之後,才發覺 Javascript 身為一個弱型別語言,在類別轉型方面真的是充滿驚恐,對於新手來說或許是友善的,但對於半生不熟的老菜鳥們,想要深入去理解背後的判斷原理,真的是需要下一番苦功。
轉生之後
沒了面孔與相貌
只剩靈魂的連結
JavaScript-Equality-Table
Equality_comparisons_and_sameness