iT邦幫忙

2021 iThome 鐵人賽

DAY 18
1
Modern Web

Javascript 從寫對到寫好系列 第 18

Day 18 - 未知與空值 undefined、null、NaN

前言

今天來討論另一個容易被忽略的主題,如果要表達「有值」的情況,大家都很熟悉:

const score = 95;
const name = 'Joey';
const arr = ['Jack', 'Allen'];
const person = { name: 'Joey', age: 20 };
const isOpened = true;

但如果遇上「無值」,或者「未知」的情況,很容易會遇到以下這幾個:

undefined、null、NaN

讓我們今天一個一個來剖析它們吧!

undefined

直接翻譯叫做「尚未定義」的變數,也就是像這樣:

let name;
console.log(name); // undefined

聽起來好像只要經過賦值,就回不去 undefined 了嗎?其實還是可以的,來源於全域物件的 undefined

let name = 'Joey';
console.log(name); // Joey
name = undefined;
console.log(name); // undefined

所以,undefined 不是一種「狀態」,它就跟字串、數字、陣列一樣,undefined 就是一種「值」,只是它的值叫做 undefined

同時它的 type 也是很特別的,就叫做 undefined

console.log(typeof undefined); // "undefined"

而這個 undefined 會在變數初始化時,如果沒有一開始就賦值,那 Javascript 就是直接給它一個 undefined 的值。

因此,原本我們會說「某個變數沒有初始值」,但其實,只要你有用 letvar 去宣告它,它就一定有初始值

let name;
// 變數只要經過宣告,就會有個 undefined 的值
console.log(name); // undefined

// 變數沒有宣告就使用會產生 Reference Error
console.log(name2); // Uncaught ReferenceError: name2 is not defined

undefined 檢核

大部分的實際情況是,如果程式都是自己寫的,基本上變數是不是 undefined 自己都很清楚。

但現在經常去使用第三方的套件,或者是自己串接他人的後端,往往你也不知道對方到底傳了什麼過來,最基本的判斷就是先確保不是 undefined

尤其如果你寫的函式要給很多人呼叫,就更要在函式最開頭先做基本的 validation,像這個有瑕疵的版本:

const displayName = (firstName, lastName) => {
    return `${firstName} ${lastName}`;
};

displayName('Joey'); // "Joey undefined"

可以改成:

const displayName = (firstName, lastName) => {
    const nameAry = [];
    if (typeof firstName !== 'undefined') nameAry.push(firstName);
    if (typeof lastName !== 'undefined') nameAry.push(lastName);
    
    return nameAry.join(' ');
};

displayName('Joey'); // "Joey"

CSS style 也可以用到 undefined

css 要設定 style 的時候,會遇到「if 某種情況,要有這個 style,else 就維持原樣」,如果用 if/else 來寫會這樣:

const isRedColor = true;
let color;
if (isRedColor) {
    color = 'red';
}

// elem 是模擬一個 DOM 元素
elem.style.color = color;

但其實上面這段的第 2 行,還記得嗎?其實就等於:

let color = undefined;

所以改用三元運算子會輕鬆一點,而且 color 可以用 const 來宣告,其實比較符合這個變數的原意(初始化之後就沒有要改了)。

const isRedColor = true;
const color = isRedColor ? 'red' : undefined;

// elem 是模擬一個 DOM 元素
elem.style.color = color;

注意 Object 裡面的 undefined

上面提過 undefined 也是一種「值」,所以 Object 裡面也可以放 undefined

const person = {
    name: 'Joey',
    height: 173,
    weight: 63,
    son: undefined
};

但還記得我們在 Day 4 - Object 提到過,多餘的 property 很容易在執行 Object.keys 系列的函式時,出現意想不到的狀況:

const person = {
    name: 'Joey',
    height: 173,
    weight: 63,
    son: undefined
};
Object.entries(person)
      .forEach(([key, value]) => `${key}:${value}`);

執行結果

name:'Joey'
height:173
weight:63
son:undefined

null

null 是一種值,它的意思是「故意地沒有值」。好吧我知道大家看不懂我在寫什麼,所以如果看原文應該比較好理解:

The value null represents the intentional absence of any object value.

簡單說,就是這個變數有宣告而且有值,而它的值是「空值」的概念。

null 與 undefined 之間的差別

拿來跟前面提到的 undefined 比較一下:

let name;
console.log(name); // undefined

const nullName = null;
console.log(nullName); // null

console.log(typeof name); // "undefined"
console.log(typeof nullName); // "object"

我們可以用比較白話的方式來解釋這段 code:

我宣告了一個變數叫做 name,但這個變數我還沒想到要給它什麼值
我宣告了一個變數叫做 nullName,這個變數我決定讓它代表空值

所以,即便這兩個變數都可以說是「什麼都沒有」,但比較細微的差距在於,開發者有沒有「意圖」要定義這個變數

  • 沒意圖:undefined
  • 有意圖:null

另一個有趣的點是,透過 typeof 取得的值:

console.log(typeof name); // "undefined"
console.log(typeof nullName); // "object"

這是一個很神奇的設計,有 undefined 這個類別,但沒有 null 這個類別,事實上,就連 MDN 都說這是

bug in ECMAScript

所以現階段如果要判斷是不是 null,可以單純就用嚴格相等:

console.log(nullName === null); // true

在 DB 存 null 值

DB 要 update data 時,如果這個欄位「沒有變動」,通常會放 undefined 或直接就不放,但如果要強調這個欄位叫做「空值」,則應該要放 null

比如一個原本又瘦又有車的 Joey,變成又胖又沒車的 Joey:

// 假設 DB 目前有這筆資料:
// {
//     id: '61226502e1c26332bcb5f9ca',
//     name: 'Joey',
//     weight: 63,
//     car: 'TOYOTA'
// }

const updateObject = {
    id: undefined, // 這行可以移除
    name: undefined, // 這行可以移除
    weight: 73,
    car: null
};

updateById('61226502e1c26332bcb5f9ca', updateObject);

// 更新完後可能會是這樣
// {
//     id: '61226502e1c26332bcb5f9ca',
//     name: 'Joey',
//     weight: 73,
//     car: null
// }

NaN

今天介紹的東西真的一個比一個奇葩,這位選手叫做「不是個數字」。

NaN:Not-A-Number

通常是在 Math 函式計算失敗(如:Math.sqrt(-1))或函式解析數字失敗(如:parseInt("blabla"))後才會回傳:

console.log(Math.sqrt(-1)); // NaN
console.log(parseInt("blabla")); // NaN

這些 NaN 的特性只能用背的

奇特的是,它雖然「不是個數字」,但如果印出它的 type:

console.log(typeof NaN); // "number"

沒錯,「不是個數字」的類別是「數字」。

而且如果它不像 null 可以用嚴格相等來判斷出來:

console.log(null === null); // true
console.log(NaN === NaN); // false
console.log(parseInt("blabla") === NaN); // false

只能夠使用 Number.isNaN()isNaN() 來判斷,我個人比較習慣用前者,比較單純一點,想知道差異可以看 MDN

console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN(parseInt("blabla"))); // true

parse 成 number 時要特別小心

由於它跟 number 相關,所以我個人最常用到的時機點就是 string to number 的時候,比如網址上的參數:

const url = 'https://example.com?score=8';

// 拆 url 的 query 參數沒這麼簡單,這邊是偷吃步
const score = url.split('=')[1];
const scoreNum = parseInt(score, 10);
console.log(scoreNum * 3); // 24

我們沒有辦法預期網址上的 score 是不是真的都能夠被 parseInt 解析,比方說這個來亂的:

const url = 'https://example.com?score=八';

// 中間都跟上面一樣

console.log(scoreNum * 3); // NaN

這時就還是要透過特別的判斷式,以確保是一個可以被進行數學運算的 number

const url = 'https://example.com?score=八';

// 中間都跟上面一樣

if (Number.isNaN(scoreNum)) {
    console.log('score 請帶入數字');
} else {
    console.log(scoreNum * 3);
}

執行結果

score 請帶入數字

結語

今天介紹了 undefinednullNaN 三個「非主流」,除了比較細微的差異,也帶到了常常會碰到它們的地方。

很多時候看到有些人會想用 「!」 這個運算子,來一次排除掉這三種空值的概念:

const a = undefined;
const b = null;
const c = NaN;
if (!a && !b && !c) {
    console.log('用一個驚嘆號似乎就可以判斷了?');
}

但會衍生出什麼問題呢?我們留到明天來探討囉!

周旋在這個世界
你害怕的
是未知
還是一無所有?

參考資料

undefined MDN
null MDN
NaN MDN


上一篇
Day 17 - Error Handling 錯誤處理
下一篇
Day 19 - 相等判斷與型別轉換
系列文
Javascript 從寫對到寫好30

尚未有邦友留言

立即登入留言