Skip to content与
语法约束:不能与
空值合并运算符(??)
概述
?? 解决一个精确的需求:为可能为 null 或 undefined 的值提供默认值。|| 也能做这件事,但它会把 0、''、false 等所有假值都视为“缺少值”,这在很多实用场景中会导致错误行为。
基本概念
?? 的求值逻辑仅将 null 与 undefined 视为“空”,其他值——包括 0、空字符串、false——都原样保留。
js
const height = 0;
const displayHeight = height || 100; // 100 —— 0 被错误替换
const displayHeight2 = height ?? 100; // 0 —— 正确上例中 height = 0 是有效数据,业务上应当展示“高度:0”。|| 遇到 0(falsy)就跳过了,引起 UI 显示为“高度:100”。这类问题在表单默认值、配置合并和 API 响应处理中反复出现。
工作原理
ECMAScript 规范 §12.14 对 ?? 的求值过程规定如下(简化):
- 对左操作数求值,得到
lref。 - 取得
lref的值lval。 - 若
lval是undefined或null,对右操作数求值并返回其值。 - 否则直接返回
lval。
其中两个细节值得注意。
左操作数仅求值一次。 在 a ?? b 中,a 只会执行一次。如果 a 不是 null/undefined,b 不会被求值——这是短路语义。
判断依据是 === undefined || === null。 虽然 x == null 等价于 x === null || x === undefined,但规范层面使用严格比较。这一差异在遇到 document.all 时才会显现:document.all == null 为 true(历史兼容),但 document.all ?? 'default' 返回 document.all 对象本身,因为它并非真正的 null 或 undefined。
js
document.all || 'fallback'; // 'fallback' —— document.all 是 falsy
document.all ?? 'fallback'; // HTMLAllCollection —— 它是已定义的值document.all 是 JavaScript 中少数“假值但非 null/undefined”的案例:为了兼容旧式浏览器检测代码,规范特意让它表现为 falsy。?? 是能够区分这种情形的少数运算符之一。
基本用法
js
const value = possiblyNull ?? defaultValue;当 possiblyNull 的实际值为 null 或 undefined 时,value 取得 defaultValue;否则保留原值。
与可选链的组合是设计上的自然配合:
js
const name = obj?.user?.name ?? '匿名';?. 遇到 null/undefined 短路并返回 undefined,?? 正好接住这个 undefined,形成“安全访问 + 默认值”的链路。两者出自同一提案(Optional Chaining + Nullish Coalescing),在互操作性上保持协调。
逻辑空赋值运算符(??=)
ES2021 引入了逻辑空赋值运算符 ??=,它结合了空值判断与赋值操作:
js
let x = null;
x ??= 'default'; // x 变为 'default'
let y = 0;
y ??= 100; // y 保持 0语义上,a ??= b 等价于 a ?? (a = b):仅当 a 为 null 或 undefined 时,才将 b 赋值给 a。与 ?? 相同,??= 也采用短路求值,且转译时需要临时变量以避免左侧表达式被求值两次。
与 || 的选择
选择 ?? 还是 ||,取决于一个基本问题:在当前业务语境中,假值(0、''、false)是“缺少值”还是“有效值”?
| 场景 | 假值的含义 | 建议使用 |
|---|---|---|
用户身高 height = 0 | 0 是有效物理量 | ?? |
配置项 timeout = 0 | 0 可能代表“不超时” | ?? |
布尔 enabled = false | false 是明确的“关闭” | ?? |
API 返回 count = 0 | 0 是有效计数 | ?? |
字符串 name = '' | 空字符串是否有效值要看业务定义 | 视业务而定 |
一个直接的参考规则:当变量类型为 number 或 boolean 时,更适合优先考虑 ??;对于 string,需要根据空字符串的业务语义决定。
在代码库中将 || 迁移为 ?? 时,风险不在于修改哪些文件,而在于 0、false、'' 的行为会发生静默变化。测试覆盖不足时,这类变化容易遗漏。较为稳妥的做法是先处理 number 和 boolean 类型的默认值,再逐个检查 string 类型的语义。
语法约束:不能与 && / || 混用
js
1 && 2 ?? 3; // SyntaxError这不是运行时限制,而是解析层的语法约束。TC39 的讨论记录显示,限制的原因并非“语义不可定义”,而是开发者在从 || 切换到 ?? 时容易引入逻辑错误。a || b ?? c 的求值顺序在不同语言中不一致,JavaScript 选择直接报错,要求开发者使用括号显式标明意图。
js
(1 && 2) ?? 3; // 明确优先级优先级
?? 的优先级与 || 相同,低于多数算术运算符(+、*)、成员访问(.),高于赋值运算符(=)和条件(三元)运算符(?:)。
js
const x = a ?? b; // = 优先级最低,不需要括号
const y = a ?? b ? c : d; // 等价于 (a ?? b) ? c : d
const z = (a ?? 1) + (b ?? 2); // 需要括号,否则先计算 1 + b
const area = (height ?? 100) * (width ?? 50); // 需要括号不熟悉优先级时,加括号不会引入运行时开销,却能准确表达求值顺序。
Babel/TypeScript 转译:重复求值问题
?? 是 ES2020 引入的。在需要兼容旧浏览器的项目中,Babel 和 TypeScript 会将其转译为等价的三元表达式。简单的转译会导致左操作数被求值两次:
js
// 源码
const result = getValue() ?? defaultValue;
// 错误转译——getValue() 调用两次
const result = getValue() !== null && getValue() !== undefined
? getValue()
: defaultValue;若 getValue() 包含副作用(如修改状态、移动读取指针),副作用会被执行两次,行为与源码不一致。
Babel 和 TypeScript 实际生成的代码通过临时变量避免此问题:
js
// 实际转译
var _ref;
const result = (_ref = getValue()) !== null && _ref !== void 0
? _ref
: defaultValue;_ref 保存左操作数的求值结果,后续比较和取值都使用 _ref。void 0 是 undefined 的最短安全表达——在 ES3 时代 undefined 可以被重写,而 void 0 始终返回真正的 undefined。
当用到 a ?? b ?? c 时,转译会产生嵌套的临时变量赋值。这一行为在编写 Babel 插件或分析 AST 时需要留意。
注意点
??仅将null和undefined视为空,不处理其他假值。假值语义的静默变化是迁移中的主要风险。- 与
&&或||直接组合会触发语法错误,必须用括号明确优先级。 - 转译代码要确保左操作数只被求值一次,否则副作用会被重复执行。
document.all这类历史遗留的特殊对象虽然表现为 falsy,却不会被??视为空。
