更聪明的默认值
JavaScript 一直有一个设置默认值的捷径:value || fallback。它大多数时候都能用,但自带一个 bug:它把任何假值都当作缺失。0、'' 和 false 都会触发后备值,即使它们才是真正的答案。
空值合并运算符 ?? 解决了这个问题。它只在 null 或 undefined 时才使用后备值:
0 和 '' 被保留下来,null 和 undefined 则不会。这个运算符的全部内容就是这些。
为什么 || 不够用
下面是 ?? 想要避免的经典 bug:
用户要的音量是 0(静音),昵称是空的。两个值都被悄悄覆盖了,因为 || 分不清“缺失”和“假值”。
换成 ??:
现在 0 和 '' 原样通过,只有真正缺失的字段才会得到默认值。这几乎总是你想要的效果。
理解思路
?? 只问一个问题:左侧是 null 或 undefined 吗?其他情况都不算。
| 左侧的值 | left || right 返回 | left ?? right 返回 |
|---|---|---|
null | 右侧 | 右侧 |
undefined | 右侧 | 右侧 |
0 | 右侧 | 左侧(0) |
'' | 右侧 | 左侧('') |
false | 右侧 | 左侧(false) |
NaN | 右侧 | 左侧(NaN) |
| 任意对象 | 左侧 | 左侧 |
当数据中的假值有实际意义时,选 ??。当你确实想排除所有假值时(短字符串、为零的计数等),选 ||。这种情况确实存在,但比人们以为的要少。
短路求值
和 ||、&& 一样,?? 也会短路。如果左侧不是空值,右侧根本不会执行:
expensiveDefault() 只运行了一次,是为 b 运行的。当后备值是一次函数调用、一次网络请求,或者其他在不需要时最好跳过的操作时,这一点很有用。
与可选链搭配
?? 和 ?. 在 ES2020 中一起推出,本来就是为了配合使用而设计的。可选链沿着一条可能缺失的路径访问,任何一步缺失都会得到 undefined;空值合并再补上一个合理的默认值:
没有这两个运算符,同样的代码就是一长串难看的 && 判断,或者一个 try/catch。有了它们,安全访问和合理的默认值一行就能写完。
空值赋值:??=
a ??= b 只在 a 为 null 或 undefined 时把 b 赋给 a。它是逻辑空值赋值运算符,同样会短路:如果 a 已经有值,右侧不会被求值。
注意 verbose: false 和 retries: 0 都被保留了:??= 只补全真正缺失的值。对比一下 ||=,它会把这两个值都覆盖掉。
优先级与括号规则
?? 故意不允许在没有括号的情况下和 || 或 && 混用。下面的代码会抛出 SyntaxError:
const x = a || b ?? c; // SyntaxError
语言设计者认为这里的优先级足够容易引起歧义,值得强制你写清楚。加上括号就没问题了:
分组不同,结果也不同。括号不是装饰:它在告诉以后读代码的人(以及解析器)你的本意。
与默认参数不同
函数签名中的默认参数有自己的规则:只在实参为 undefined 时生效,null 不会触发。这和同等对待两者的 ?? 有细微的差别。
如果希望 null 也能触发后备值,就在函数体内使用 ??:
区别虽小,但当某个 API 用 null 表示“未设置值”时,很容易在这里栽跟头。
什么时候用 ??
除非有特别的理由,否则就把 ?? 当作设置默认值的首选运算符。它最符合大多数数据的实际情况:0、'' 和 false 通常都是值得保留的真实值,只有真正缺失的数据才需要后备值。|| 留给那些确实想替换掉任何假值的场景。
下一章:类
对象和数组的内容就到这里,你已经掌握了安全地构建和访问数据结构的工具。下一章转向组织行为:类、继承,以及它们底层的原型系统。
常见问题
JavaScript 中的空值合并运算符是什么?
?? 只在左侧为 null 或 undefined 时返回右侧的值,否则原样返回左侧的值。value ?? fallback 是设置默认值的标准写法,不会误伤 0 或 '' 这类虽为假值但合法的值。
JavaScript 中 ?? 和 || 有什么区别?
|| 遇到任何假值都会使用后备值:0、''、false、NaN、null、undefined。?? 只在 null 和 undefined 时使用后备值。如果 0 或空字符串在你的数据中是合法的值,就用 ??。如果你确实想排除所有假值,|| 仍然是正确的选择。
空值合并可以和可选链一起用吗?
可以,而且这是很常见的搭配。user?.settings?.theme ?? 'light' 会安全地沿属性链访问,中途任何一环缺失都会得到 undefined,然后换成 'light'。这两个运算符在 ES2020 中一起推出,正是为了支持这种写法。
??= 运算符有什么作用?
a ??= b 只在 a 当前为 null 或 undefined 时把 b 赋给 a。它是逻辑空值赋值运算符,是 a = a ?? b 的简写,但带有短路效果:如果 a 已经有值,右侧根本不会被求值。用来给选项对象补全缺失的默认值很方便。