容易被忽略的未知与空值 undefined、null、NaN

317 阅读5分钟

前言

今天来讨论另一个容易被忽略的主题,如果要表达「有值」的情况,大家都很熟悉:

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

但如果遇上「无值」,或者「未知」的情况,很容易会遇到以下这几个:

undefinednullNaN

让我们今天一个一个来剖析它们吧!

undefined是「值」

直接翻译叫做「尚未定义」的变数,也就是像这样:

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

听起来好像只要经过赋值,就回不去 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 的值。 因此,原本我们会说「某个变数没有初始值」,但其实,只要你有用 let 或 var 去宣告它,它就一定有初始值:

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.style.color = color;

但其实上面这段的第 2 行,还记得吗?其实就等于:

let color = undefined;

所以改用三元运算子会轻松一点,而且 color 可以用 const 来宣告,其实比较符合这个变数的原意 (初始化之后就没有要改了)。

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

elem.style.color = color;

注意 Object 里面的 undefined

上面提过 undefined 也是一种「值」,所以 Object 里面也可以放undefined: 还记得我们在彻底掌握 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 之间的差别

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 请带入数字