Menu

JavaScript 可选链(?.):安全访问嵌套属性

?. 运算符如何让你深入访问嵌套的对象、数组和方法,而不会因为 null 或 undefined 而出错。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

问题所在:嵌套访问很脆弱

深入访问一个嵌套对象没什么问题,直到某一层不存在为止:

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,后面的部分都不会执行。

Coddy 编程语言插图

和 Coddy 一起学编程

开始