JavaScript世界: 再看一眼 null 和 undefined

485 阅读4分钟

许多编程语言都有空值的概念,并使用null关键字意指 "没有值" 或是 "未知的值".

但是在JavaScript的世界里,存在着两种空值的表示方法:undefinednull.本文简单描述了它们的区别以及各自适宜的使用场景.

1. Undefined vs. null

两个关键字都很常用,并且在许多场景下可以相互替换.因此,它们之间的差异是非常微妙的.

1.1 ECMAScript 关于二者的定义

  • undefined 用于未定义变量的值
  • null 为已申明的变量分配值的时候使用,表示故意缺少任何对象值,其值不存在.

熟悉两个定义是合理使用undefinednull的关键.

接下来将配合一些代码进行说明.

1.2 两个"空值"是一个错误

JavaScript 的创建者 Brendan Eich 曾表示: 在 JavaScript 中具有两个"空值"的表示是一种设计错误.

之所以不将其中之一从JavaScript世界中抹去,其原因是 JavaScript 遵守一个设计准则: 始终不破坏向前兼容性.

这个准则有许多好处,但是最大的坏处就是无法修复设计错误.

1.3 undefined 和 null 的历史

Java的世界里,成员变量中,引用类型的变量初始化的时候默认值null.

JavaScript的世界里,每个变量可以同时包含对象值原始值.因此,如果null表示对象值,其值为,则JavaScript 需要一个原始值来表示一种未定义的状态值.这个未定义的值(原始值)就是undefined.

2. undefined 出现场景

如果一个变量没有被初始化,则其具有原始值undefined:

let foo;
assert.equal(foo, undefined); // true

如果一个对象的属性某个属性没有申明,则其原始值为undefined:

const obj = {};
assert.equal(obj.name, undefined); // true

如果一个函数未指定返回值,或者不存在return关键字,则默认返回undefined:

function foo() {}
assert.equal(foo(), undefined); // true
function far() {
  return;
}
assert.equal(far(), undefined); // true

如果调用函数的时候,未提供函数定义时声明的参数,并且未指定默认值的时候,参数具有原始值undefined:

function foo(value) {
  assert.equal(value, undefined); // true
}

以及ES2020新增的Optional chaining语法,默认返回值是undefined:

const obj = {};
obj?.prop // undefined

optional chaining 中只要出现异常,一律返回 undefined.

比如: val?.name, 无论 val 是 null 还是 undefined,都返回 undefined.

3. null 出现场景

Object的原型也是一个对象,只是此对象的原型值为null:

Object.getPrototypeOf(Object.prototype) // null

正则表达式匹配不到结果,其值为null:

/a/.exec('x') // null

另外,JSON规范不支持值为undefined,如下转换将会忽略部分属性.

JSON 语义中存在表示空值的null,不存在undefined这个类型.

JSON.stringify({
  a: undefined,
  b: null
})
// {"b": null}

4. undefined 和 null 的特殊对待方式

比如我们有一个简单函数如下:

function foo(name='balabala') {
  return name;
}
foo(); // balabala
foo(null); // null 传入 null,优先级高于默认值
foo(undefined); // balabala,传入 undefined 相当于传入原始值,优先级低于默认值

在对象解构赋值中的表现也一样:

const [a = 'a'] = [];
// a => 'a'
const [b = 'b'] = [undefined];
// b => 'b'
const {prop: c = 'c'} = {}
// c => 'c'
const {prop: d = 'd'} = {prop: undefined}
// d => 'd'

如果赋值为null:

const [b = 'b'] = [null];
// b => null
const {prop: d = 'd'} = {prop: null}
// d => null

在空值合并的操作中,??让我们在值为null或者undefined的时候使用默认值.

null ?? 1 
// 1
undefined ?? 1 
//1

那么在空值合并赋值时,有什么表现呢?

function setName(obj) {
  obj.name ??= '(Unnamed)';
  return obj.name;
}
setName({
  name: null
})
// '(Unnamed)'
  setName({
  name: undefined
})
// '(Unnamed)'

5. 处理 undefined 和 null

undefinednull都不用做实际值.举个🌰,如果我们希望一个属性: file.title始终存在,并且始终为字符串.

我们可以用以下两种方案实现:

5.1 禁用 undefined 和 null

示例代码:

function createFile(title) {
  if (title === undefined || title === null) {
    throw new Error('`title` must not be nullish');
  }
  // ···
}

5.2 undefined 和 null 一致性处理

示例代码:

function createFile(title) {
  title ??= '(Untitled)';
  // ···
}

上述代码并未使用默认参数赋值,如果使用默认参数,则只能对undefined做处理.相比于禁用undefinednull,使用空值合并运算符既可以实现更好的一致性处理方案,而且代码更健壮优雅.

6. 额外总结

以下总结具有很强的主观性,望合理探讨.

  • null 表示一个值被定义了,不过值是空的.设置一个值为null 是合理的.
  • undefined 表示不存在的定义,设置一个值为undefined应该是不合理的.
  • 判断值的存在与否,应该使用undefined进行判断.

我刚开始写技术分享😂,欢迎大伙指点和探讨.

7. 参考