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


<<:  MySQL汇入JS

>>:  用React刻自己的投资Dashboard Day18 - 选单列active style功能

Day15 - 守护你的状态转移 : Guard

我们目前所学到的状态机已知,我们能透过 transition 限制状态转换的路径 对的 state ...

在 Windows 10 上安装 Visual Studio Code EP1

前言 写程序,设定好 IDE,可以增加自己的效率,今天来纪录一下安装 Visual Studio C...

IT铁人第28天 Elasticsearch 使用python查询资料 Aggregations:Sum/Value Count

今天文章的内容是Sum(总和)跟Value Count(数量计算) 今天的示范资料 Sum Sum大...

Day17-Session 管理(一)

前言 在了解 cookie 的安全性设定之後,接下来的两天要来讲讲跟 session 安全性有关的注...

资料结构和演算法

https://wolkesau.medium.com/资料结构和演算法-c3a453c9c64c ...