问题所在:嵌套访问很脆弱
深入访问一个嵌套对象没什么问题,直到某一层不存在为止:
user.address 是 undefined,从 undefined 上读取 .city 会抛出 TypeError: Cannot read properties of undefined。真实世界的数据,比如 API 响应、解析后的 JSON、DOM 查询,到处都是可能存在也可能不存在的字段,为每一层都写防御性检查,代码很快就会变得杂乱:
const city = user && user.address && user.address.city;
两层还勉强能读,四层就很痛苦了。?. 运算符是更干净的解决办法。
?. 遇到 null 和 undefined 时短路
可选链只在前面的值不是 null 或 undefined 时才读取属性。如果是,整个表达式的结果就是 undefined,并就此停止。
第一行正常读取 name。第二行遇到 user.address,发现是 undefined,于是退出,属性链的其余部分都不会执行。第三行如果没有 ?. 就会抛出异常,因为 .toUpperCase() 会在 undefined 上调用;有了 ?.,整个表达式会安静地得到 undefined。
可以这样理解:?. 的意思是“如果我前面的值是 null 或 undefined,就放弃并返回 undefined,否则继续往下走”。
它也适用于数组和函数调用
三种语法,同一个思路:
?.[...]用于数组访问或动态属性访问。?.()用于调用一个可能是函数、也可能不是函数的东西。?.name用于普通的属性访问。
三者的短路方式完全相同。尽管 notAMethod 不存在,user?.notAMethod?.() 也不会抛出异常:第二个 ?. 看到 undefined 就停下了。
这对可选的回调特别方便:
再也不用在每次调用前写 if (typeof onDone === "function") 这样的判断了。
只有 null 和 undefined 会触发短路
这是让人出错的细节。?. 只关心空值。其他假值,比如 0、""、false、NaN,都可以正常地继续链式访问(至少在自动装箱允许的范围内):
data.count 是 0,它是假值但不是空值,所以 ?.toFixed(2) 会在它上面运行,返回 "0.00"。和 && 对比一下:
&& 版本返回 0,因为 data.count 是假值,于是发生了短路。?. 版本返回 "0.00",因为 0 不是空值。如果你确实想在 0 处停下,&& 是对的。如果你只想在“值缺失”时停下,?. 才是你要的。
? 放在哪里很重要
?. 保护的是它前面的值,而不是后面的。所以要把它放在可能缺失的那一层:
这三种写法在这里都能运行,因为实际上没有缺失任何东西。但如果 config.server 可能是 undefined,你就需要 config.server?.host:放在 server 前面的 ?. 帮不上忙,因为问题出在从缺失的 server 上读取 .host。
一条好的规则:只在左侧的值确实可能为空值的那一层使用 ?.。为了“以防万一”在每个点号上都加 ?.,会掩盖 bug,还会让代码更杂乱。
不能通过 ?. 赋值
可选链是只读的。你不能把它用在赋值语句的左侧:
user?.address?.city = "Paris"; // SyntaxError
仔细想想这很合理:给 undefined 的属性赋值又意味着什么呢?如果你只想在父对象存在时设置值,就明确地写出来:
什么时候该用,什么时候不该用
当一个值确实是可选的时候,?. 最能发挥作用:
- 字段可能包含也可能不包含的 API 响应。
- DOM 查询:
document.querySelector(".banner")?.remove()。 - 可能没有传入的回调:
options.onError?.(err)。 - 中间结果可能为
null的链式库调用。
当一个值本该始终存在时,它就不是合适的工具。在这种情况下用 ?. 来消除错误,会把一个明显、容易定位的 bug(“第 42 行出现 TypeError”)变成一个无声的 bug(某个变量在三个函数之外莫名其妙地变成了 undefined)。当某样东西必须存在时,就让它抛出异常:调用栈信息是在帮你的忙。
一个贴近实际的例子
从一个可能不完整的 API 响应中取值:
每个 ?. 处理一层不确定性。avatarUrl 干净地得到 undefined。onClick?.() 会在处理函数恰好存在时调用它。
你会注意到 displayName 那一行用了 ??,这是这种写法的另一半。?. 在某样东西缺失时给你 undefined;?? 让你可以替换成默认值,同时不会误伤 0 或 "" 这样合法的假值。
下一章:空值合并
?. 和 ?? 是同一种思路在不同问题上的应用:两者都把 null 和 undefined 视为“缺失”,而不去管其他假值。接下来我们会看到 ?? 如何给你合理的默认值,以及为什么 || 多年来一直在悄悄地把这件事做错。
常见问题
JavaScript 中 ?. 有什么作用?
?. 运算符只在它前面的值不是 null 或 undefined 时,才去访问属性、数组索引或方法。如果是,整个表达式会短路并得到 undefined,而不是抛出异常。当 user 或 address 缺失时,user?.address?.city 不会崩溃。
JavaScript 中什么时候应该用可选链?
当一个值确实允许缺失时就用 ?.:带有可选字段的 API 响应、可能找不到节点的 DOM 查询、可能传入也可能不传入的回调。不要用它来掩盖本该始终存在的值所引起的 bug;在这些情况下,值缺失是你需要看到的真实信息。
?. 和 && 有什么区别?
大多数情况下它们的结果相同,但 ?. 只在 null 或 undefined 时短路,而 && 遇到任何假值都会短路,包括 0、'' 和 false。当 count 为 0 时,obj && obj.count 返回 0;obj?.count 也返回 0,但属性链只会在空值处干净地停下。
可选链可以用于数组和函数调用吗?
可以。arr?.[0] 可以安全地读取数组索引,fn?.() 只在函数存在时才调用它。两者遵循同样的规则:如果 ?. 前面的值是 null 或 undefined,表达式的结果就是 undefined,后面的部分都不会执行。