前端JavaScript:NaN、undefined、null详解

0 阅读10分钟

在 JavaScript 的世界里,有三个特殊的值常常让初学者甚至有经验的开发者感到困惑:undefinednullNaN。它们都在某种程度上表示 “空” 或 “无值”,但在语义、类型系统以及运行时行为上却有着天壤之别。

很多线上 bug 的根源,往往就在于混淆了这三者的区别。比如,你是否曾疑惑过为什么 typeof null 会返回 'object'?为什么 NaN === NaN 会返回 false?为什么 null == undefinedtruenull === undefined 却是 false

本文将带你深入这三个特殊值的底层逻辑,拆解它们的产生场景、类型特征以及相等性判断的陷阱,帮助你彻底理清这三者的关系,写出更健壮的代码。

一、undefined:系统的 “未定义”

1.1 语义:自然的缺失

undefined 的核心语义是 “未定义” 。它代表的是一种 “自然的缺失”—— 也就是说,当 JavaScript 引擎在找不到值的时候,它会默认用 undefined 来填充。

这不是开发者主动设置的,而是语言本身的默认行为。当一个变量声明了但还没赋值,或者访问了一个对象上不存在的属性,JavaScript 并不会抛出错误,而是返回 undefined 告诉你:“这里应该有个值,但我还没找到。”

1.2 常见的产生场景

undefined 通常出现在以下几种情况:

  1. 变量声明但未赋值:这是最常见的场景。

    1.  let name;
       console.log(name); // undefined
      
  2. 访问不存在的对象属性

    1.  const user = { name: 'John' };
       console.log(user.age); // undefined
      
  3. 函数没有返回值:如果一个函数没有 return 语句,它默认返回 undefined

    1.  function doSomething() {
           // 没有 return
       }
       console.log(doSomething()); // undefined
      
  4. 函数参数未传参

    1.  function greet(name) {
           console.log(name); // undefined
       }
       greet(); // 没传参数
      

1.3 类型与注意事项

undefined 本身是一个独立的原始数据类型(Undefined Type),它只有一个值,就是 undefined

使用 typeof 运算符检测 undefined 时,会准确地返回 'undefined'

let a;
console.log(typeof a); // 'undefined'

最佳实践:不要主动给变量赋值为 undefined。因为 undefined 是语言用来表示 “缺失” 的默认值,如果你手动赋值 let a = undefined,会混淆 “本来就没有值” 和 “我故意把它清空了” 这两种语义。如果你想表示清空,应该使用 null

二、null:开发者的 “空指针”

2.1 语义:主动的清空

undefined 相反,null 的核心语义是 “空值” 。它代表的是一种 “主动的清空”。

null 意味着:“我,开发者,明确地告诉引擎,这个变量现在是空的,它不指向任何对象。”

这是一个有意为之的状态。通常我们用它来表示一个变量本来应该是一个对象,但现在暂时没有值。比如,在等待异步请求返回数据之前,我们可以把变量初始化为 null,表示 “数据还没加载好”。

2.2 常见的使用场景

  1. 初始化对象变量

    1.  // 表示用户对象目前为空,等待后续赋值
       let currentUser = null;
      
       // 登录成功后
       currentUser = { id: 1, name: 'John' };
      
  2. 主动释放引用:在一些手动内存管理的场景下,将对象引用置为 null 可以帮助垃圾回收。

    1.  let bigData = getBigData();
       // 处理完数据
       bigData = null; // 释放引用
      
  3. 函数返回 “无结果” :当查询数据库没有找到结果时,返回 null 而不是抛出错误,表示 “找到了,但结果是空的”。

2.3 那个著名的 Bug:typeof null === 'object'

这是 JavaScript 中最广为人知的历史遗留问题。当你使用 typeof 检测 null 时,你会得到:

console.log(typeof null); // 'object'

这其实是 JavaScript 最初实现时的一个错误。在最初的 JavaScript 引擎中,值是由一个标签和实际数据表示的。对象的标签是 0,而 null 表示空指针,在大多数平台下是空指针的引用也是 0x00,所以它的标签也被误写成了 0,导致 typeof 把它当成了对象。

虽然这个错误已经被所有人知道了,但由于兼容性的原因,ECMAScript 标准一直没有修复它。

因此,永远不要用 typeof 来检测 null 正确的检测方式是直接使用严格相等:

if (value === null) {
    // 这才是正确的判断方式
}

三、NaN:数字里的 “坏孩子”

3.1 语义:无效的数字

NaN 全称是 Not-a-Number,即 “非数字”。但这并不意味着它的类型不是数字。恰恰相反,NaN 是一个 数值类型 的特殊值。

它的语义是:“这本来应该是一个数字,但是运算失败了,所以我用 NaN 来表示这个无效的结果。”

比如,你试图把一个字符串 "abc" 转换成数字,或者对负数开平方,JavaScript 不会抛出异常,而是返回 NaN 来告诉你:“这次数字运算搞砸了。”

console.log(typeof NaN); // 'number'
// 没错,它的类型是 number!

这是因为 JavaScript 遵循了 IEEE 754 浮点数标准,而 NaN 正是该标准中定义的一个特殊数值,用来表示非法的计算结果。

3.2 最反直觉的特性:传染性与自不等

NaN 有两个极其特殊的性质,也是无数 bug 的来源:

  1. 传染性:只要你的数学运算中混入了 NaN,那么最终的结果一定是 NaN。它就像病毒一样会传染。

    1.  console.log(1 + NaN); // NaN
       console.log(2 * NaN); // NaN
       console.log(Math.max(1, 2, NaN, 3)); // NaN
      
    2.   这意味着,一旦你的计算链中某个环节出错产生了 NaN,它会一路污染到最终结果,而且很难定位到底是哪里出的错。
  2. 它不等于任何值,包括它自己:这是最反直觉的一点。

    1.  console.log(NaN === NaN); // false
       console.log(NaN == NaN); // false
      
    2.   为什么会这样?因为 IEEE 754 标准规定,NaN 不与任何值相等,包括它自己。这是为了让你能通过 x !== x 来检测 NaN

3.3 如何正确检测 NaN?

既然 === 不好使,那我们该怎么检测一个值是不是 NaN 呢?

  • 全局的 isNaN() :这是最早的方法,但它有坑。它会先把参数转换成数字,如果转换失败就返回 true。这意味着它会把很多非数字的值也误判为 NaN

    •   console.log(isNaN('hello')); // true!因为 'hello' 转数字失败了
        console.log(isNaN(undefined)); // true
      
    •   这显然不对,因为 'hello' 本身并不是 NaN,它只是个字符串。
  • Number.isNaN() :ES6 引入的正确方法。它不会做类型转换,只有当值真的是 NaN 时才返回 true

    •   console.log(Number.isNaN(NaN)); // true
        console.log(Number.isNaN('hello')); // false
        console.log(Number.isNaN(undefined)); // false
      
  • 利用自不等特性:这是一个古老的 trick,因为只有 NaN 才会不等于自己。

    •   function myIsNaN(value) {
            return value !== value;
        }
      

四、一张表看懂三者的区别

为了让你更直观地对比这三者的区别,我们整理了一张核心特性对比表:

从表中可以清晰地看到,虽然它们在布尔转换中都为 false,但在类型、语义和转换行为上完全不同。

五、相等性判断的迷局

搞清楚了它们各自的定义,接下来最容易踩坑的就是相等性判断了。JavaScript 提供了三种比较方式:=====Object.is(),它们对这三个值的处理各不相同。

5.1 == vs ===:null 和 undefined 的暧昧关系

我们都知道 == 会进行类型转换,而 === 不会。对于 nullundefined,ECMAScript 标准做了一个特殊的规定:

null == undefined 必须返回 true

这是因为语言设计者认为,这两者都表示 “无值”,在宽松比较下应该被视为相等。但在严格比较下,它们是不同的。

console.log(null == undefined);  // true
console.log(null === undefined); // false

这就导致了一个非常有用的简写技巧:如果你想同时检查一个变量是不是 null 或者 undefined,你可以直接写:

if (value == null) {
    // 这会同时匹配 null 和 undefined
    // 等价于 if (value === null || value === undefined)
}

这在实际开发中非常常用,因为很多时候我们并不关心到底是 null 还是 undefined,我们只关心 “这个值是不是空的”。

5.2 Object.is:终极的相等判断

ES6 引入了 Object.is() 方法,它解决了 === 无法处理 NaN 的问题。

我们来看一下三种比较方式的区别:

比较=====Object.is()
null == undefinedtruefalsefalse
NaN == NaNfalsefalsetrue
+0 == -0truetruefalse

Object.is() 是最严格的相等判断,它不会做任何类型转换,也不会对 NaN-0 做特殊处理。

console.log(Object.is(NaN, NaN)); // true!终于可以正常判断 NaN 了
console.log(Object.is(null, null)); // true
console.log(Object.is(undefined, undefined)); // true

下面的矩阵图展示了在严格相等(===)下,各个特殊值之间的比较结果:

六、实战避坑:那些年我们踩过的雷

6.1 坑 1:滥用 if (!value)

很多人喜欢用 if (!value) 来判断变量是否为空。但这会把所有的假值(Falsy Value)都过滤掉,包括 0''false

// 错误示范
function processAge(age) {
    if (!age) {
        console.log('年龄为空');
    } else {
        console.log('处理年龄', age);
    }
}

processAge(0); // 错误!0 是合法年龄,但被当成空了

正确做法:明确检查 nullundefined

if (age == null) {
    console.log('年龄为空');
}

6.2 坑 2:JSON 序列化的丢失

当你使用 JSON.stringify 序列化数据时,undefinedNaNInfinity 会被特殊处理:

  • undefined、函数、Symbol 会被忽略(在对象中)或者变成 null(在数组中)
  • NaNInfinity 会变成 null
JSON.stringify({ a: NaN, b: undefined, c: null });
// 结果: "{"a":null,"c":null}"

注意,这里 NaNundefined 都变成了 null!这意味着你序列化之后,就再也分不清原来的是 NaN 还是 undefined 还是 null 了,这在处理后端数据时要格外小心。

6.3 坑 3:默认参数只对 undefined 生效

ES6 的默认参数只有在参数是 undefined 的时候才会触发,null 不会!

function greet(name = 'Guest') {
    console.log(name);
}

greet(undefined); // Guest (触发默认值)
greet(null);      // null (不触发!因为 null 是一个明确的传值)

这也符合语义:undefined 表示 “我没传这个参数”,而 null 表示 “我传了,就是空”。

七、最佳实践总结

经过上面的分析,我们可以总结出一套最佳实践,帮助你在日常开发中正确使用这三个值:

  1. 语义优先

    1. undefined 处理 “缺失” 的情况,不要手动赋值它。
    2. null 表示 “主动清空”,当你想表示一个对象变量为空时使用它。
  2. 判断准则

    1. 检测 null:使用 value === null
    2. 检测 undefined:使用 value === undefined 或者 typeof value === 'undefined'(处理未声明变量)
    3. 检测 NaN:使用 Number.isNaN(value),永远不要用全局的 isNaN()
    4. 同时检测两者:使用 value == null 来同时匹配 nullundefined,这是一个安全的简写。
  3. 利用现代语法

    1. 使用空值合并运算符 ?? 来处理默认值,它只会在 null/undefined 时生效,不会误伤 0''

      •   const count = response.count ?? 0;
        
    2. 使用可选链运算符 ?. 来安全访问属性,避免 Cannot read property of undefined 错误。

结语

undefinednullNaN,这三个看似简单的值,背后却隐藏着 JavaScript 类型系统的设计哲学和历史包袱。

  • undefined 是系统告诉你 “这里没东西”。
  • null 是你告诉系统 “这里我故意清空了”。
  • NaN 是系统告诉你 “数字运算炸了”。

理解了它们的区别,你就能在日常开发中避开绝大多数与空值相关的 bug,写出更清晰、更健壮的前端代码。

参考资料

  1. MDN Web Docs. NaN
  2. MDN Web Docs. undefined
  3. MDN Web Docs. Object.is()
  4. MDN Web Docs. 相等比较和相同
  5. OpenReplay. The Strange Life of NaN in JavaScript