iT邦幫忙

2021 iThome 鐵人賽

DAY 19
0
Modern Web

Javascript 從寫對到寫好系列 第 19

Day 19 - 相等判斷與型別轉換

前言

昨天我們介紹了 undefinednullNaN,也帶到了如何將這些特別的值判斷出來。

今天我們要來看更多,更多這些會讓人感到困惑的判斷式,以及它們在實戰中誤用會發生什麼慘況。

本篇文章會參考 JavaScript-Equality-Table,這是個視覺化又好懂的整理圖表,非常推薦在感到困惑的時候來看看。

主要有兩個主題:

  • 一般相等 ( == ) 與嚴格相等 ( === )
  • if 判斷式

一般相等 ( == ) 與嚴格相等 ( === )

這個會是我們通篇討論的基礎,一律使用嚴格相等( === )來判斷

  • 一般相等是「會先經過轉型」,左右型別一致後,再看是否相等
  • 嚴格相等則不會,如果型別不同就直接回傳 false

為什麼不要用一般相等?

一般相等的轉型滿複雜的,而且轉型的規則不是那麼直覺,所以這邊不特別講,可以參考網站中的這張表格,一目瞭然:

按照上圖,我們可能會有這種不太直覺的東西:

if (1 == "1") console.log(' 1 == "1" ');
if (true == "true") console.log(' true == "true" ');

看起來,第一個判斷式如果成立,那第二個應該也要成立吧?都只是數值的外面框上一個雙引號變成字串呀?

但結果只有第一個判斷式成立,而第二個卻不是。

執行結果

1 == "1"

諸如此類的神奇的轉型規則還有很多,可以自行參照上圖,但正是因為轉型規則太複雜,要使用還得不斷思考轉型後是否如預期,背一大堆轉型的例外。

因此我們才會說一律使用嚴格相等,果斷放棄轉型,確保等號兩邊一定是「基於型別相等的比較」,比較合乎邏輯。

嚴格相等

Javascript 真的是有很多奇特的點,雖然嚴格相等已經比一般相等容易理解了,但還是有原則與例外,先來看網站上的圖表:

是不是比一般相等乾淨多了?

基本上嚴格相等就是要符合這兩點,才會成立:

  • 同型別
  • 同值

primitive v.s. non-primitive

需特別注意這邊所謂的「同值」,會根據數值本身是 primitive 或 non-primitive 而又有不同,所以應該這樣寫:

  • 同型別
  • 同值
    • 如果是 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 判斷式

在搞定了一般相等與嚴格相等之後,我們接著來看另一種常見的寫法,就是更懶一點,連等號都不寫了,直接把變數或數值塞在判斷式裡面:

if (300) conosle.log('會執行');
if ('Hello') conosle.log('會執行');
if ([1,2,3,4]) conosle.log('會執行');

這種判斷式裡面也會轉型,不過比較單純一點,單純就是幫你轉成 true 或 false

Truthy & Falsy

會被轉成 true 的數值,就叫做 truthy value,反之叫做 falsy value。

  • truthy value:不是 falsy 的
  • 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)) {
    // ... 這邊都跟上個範例一樣
}

0 也是 falsy

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('用一個驚嘆號似乎就可以判斷了?');
}

如果看完前面的介紹,你應該就會發現,用這種方式除了會把 undefinednullNaN 這三個濾掉,其實也會把所有 falsy 的數值都濾掉,包括了 false0""

而且如果是想要濾掉 []{} 這種,其實是 truthy 的值,反而會達不到效果。

關於這點我其實也找不到一個統一的解,畢竟真的是要看 if 到底要篩選出什麼樣的資料,有時候 null 要可以過,有時候 [] 不可以過。

因此我想,認真學會每個類別的判斷方式,並且在適當的時機拿出來用,是比較有效的方式,自己也才真的知道在做什麼。

typeof
Number.isFinite()
Number.isNaN()
Array.isArray()

結語

這個章節寫完之後,才發覺 Javascript 身為一個弱型別語言,在類別轉型方面真的是充滿驚恐,對於新手來說或許是友善的,但對於半生不熟的老菜鳥們,想要深入去理解背後的判斷原理,真的是需要下一番苦功。

轉生之後
沒了面孔與相貌
只剩靈魂的連結

參考資料

JavaScript-Equality-Table
Equality_comparisons_and_sameness


上一篇
Day 18 - 未知與空值 undefined、null、NaN
下一篇
Day 20 - OR、AND 的活用方式與短路取值
系列文
Javascript 從寫對到寫好30

尚未有邦友留言

立即登入留言