Optional Chaining S4
中文标题:可选链
- 阶段: Stage 4
- 状态: 已完成
- ECMAScript 版本: ES2020
- 同步时间: 2026年8月26日
- English original · 官方仓库
本提案引入了可选链操作符(?),以简化对深层嵌套属性、方法或函数调用的访问,无需重复进行空值检查。如果基值为 null/undefined,则整个链会短路,并支持静态、动态和调用表达式。
以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。
JavaScript 的可选链
状态
ECMAScript 提案 处于流程的 Stage 4 阶段。
作者
- Claude Pache (github)
- Gabriel Isenberg (github, twitter)
- Daniel Rosenwasser (github, twitter)
- Dustin Savery (github, twitter)
概述与动机
当查找树状结构中深处的属性值时,常常需要检查中间节点是否存在:
此外,许多 API 要么返回对象,要么返回 null/undefined,并且可能只希望在其不为 null 时提取结果的属性:
可选链操作符允许开发者处理许多此类情况,而无需重复自身和/或将中间结果赋值给临时变量:
当缺失情况需要 undefined 以外的值时,通常可以通过 空值合并操作符 来处理:
可选链的调用变体对于处理具有可选方法的接口很有用:
或者用于处理并非普遍实现的方法:
先前实践
除非另有说明,以下语言中的语法由问号前置操作符组成(如 a?.b、a?.b()、a?[b] 或 a?(b) 视情况而定)。
以下语言实现的操作符与本提案具有相同的一般语义(即 1)防止空基值,2)对整个链进行短路):
- C#: Null-conditional operator — 可空条件成员访问或索引,用于读取访问。
- Swift: Optional Chaining — 可选的属性、方法或下标调用,用于读写访问。
- CoffeeScript: Existential operator — 存在性操作符变体,用于属性访问器、函数调用、对象构造(
new a?())。也适用于赋值和删除。
以下语言具有类似功能,但不会在整个链长于一个元素时进行短路。这在这些语言中是合理的,因为方法或属性可能合法地用于 null(例如,在 Dart 中 null.toString() == "null"):
- Kotlin: 安全调用 — 可选属性读取;可选属性赋值用于写入。
- Dart: Conditional member access — 可选属性访问。
- Ruby: 安全导航操作符 — 写作:
a&.b
以下语言具有类似功能。我们尚未检查它们与本提案在语义上是否存在显著差异:
语法
可选链操作符写作 ?.。它可以出现在三个位置:
注意事项
- 为了允许
foo?.3:0被解析为foo ? .3 : 0(为了向后兼容),在词法语法级别添加了一个简单的前瞻,使得字符序列?.在那种情况下不会被解释为单个标记(?.标记不能紧接着一个十进制数字)。
语义
基本情况
如果 ?. 操作符左侧的操作数求值为 undefined 或 null,则整个表达式求值为 undefined。否则,正常触发目标属性访问、方法或函数调用。
以下是基本示例,每个示例后面都有其脱糖版本。(脱糖版本并不完全准确,因为左侧应该只求值一次,并且 document.all 应该表现为对象。)
短路
如果 ?. 左侧的表达式求值为 null/undefined,则右侧不会被求值。这个概念称为短路。
长短路
实际上,当触发短路时,不仅会跳过当前的属性访问、方法或函数调用,还会跳过紧随可选链操作符之后的整个属性访问、方法或函数调用链。
请注意,空值检查只在 a 上进行。例如,如果 a 不为 null,但 a.b 为 null,则在尝试访问 a.b 的属性 "c" 时会抛出 TypeError。
此功能由例如 C# 和 CoffeeScript 实现;参见 先前实践。
堆叠
让我们将可选链定义为可选链操作符后跟一个属性访问、方法或函数调用链。
一个可选链可以后跟另一个可选链。
边界情况:分组
括号限制了短路的作用域:
这是根据语法(如 && 操作符)来指定短路作用域的设计选择,而不是通过 Completion 的传播(如 break 指令)或临时的 Reference(如本提案的早期版本)。通常,语法不能被括号任意拆分:例如,({x}) = y 不是解构赋值,而是尝试给对象字面量赋值。
请注意,无论语义如何,在那个位置使用括号实际上没有实际理由。
可选删除
由于 delete 操作符对其接受的内容非常宽松,我们免费获得了此功能:
其中 true 是尝试删除非引用时的通常结果。
为什么我们支持可选删除?
-
懒惰(此论点放在第一位,不是因为它最重要,而是因为它为其他论点提供了正确的视角)。懒惰(连同急躁和傲慢)是规范编写者最重要的美德之一。也就是说,在其他条件几乎相等的情况下,选择需要更少内容并入规范的解决方案。
现在,碰巧_支持_可选删除实际上不需要任何努力,而_不支持_它(通过使该构造成为早期语法错误)需要一些非平凡的努力。(技术细节参见 PR #73 (comment)。)
因此,根据懒惰原则,正确的问题不是:“有充分理由支持可选删除吗?”,而是:“有充分理由_移除_对可选删除的支持吗?”
-
缺乏强烈理由来_移除_支持。可选删除的支持语义是唯一可以预期的(前提是正确理解 delete 操作符的语义)。它不会以某种方式混淆程序员。事实上,唯一真正的理由是:“我们原本并未打算支持它。”
-
delete 操作符的一致性。这个操作符对其接受的内容非常宽松,甚至在给定没有意义的内容时也假装成功(例如,
delete foo())。唯一的错误条件(早期或非早期)是在严格模式下,当尝试删除被禁止删除的内容(不可配置属性、变量等)时。支持可选删除与该模型很好地契合,而禁止它则不然。 -
用例的存在。虽然不常见,但实践中确实存在(来自 Babel 的示例)。
不支持的功能
虽然为了完整性可以包含以下内容,但由于缺乏实际用例或其他令人信服的理由而不予支持;参见 Issue #22 和 Issue #54 的讨论:
- 可选构造:
new a?.() - 可选模板字面量:
a?.`string` - 可选链中或之后的构造函数或模板字面量:
new a?.b()、a?.b`string`
以下内容不支持,尽管有一些用例;参见 Issue #18 的讨论:
- 可选属性赋值:
a?.b = c
以下内容不支持,因为至少在实践上没有太大意义;参见 Issue #4 (comment):
- 可选 super:
super?.()、super?.foo - 任何类似于属性访问或函数调用但不是的内容:
new?.target、import?.('foo')等。
所有上述情况都将被语法或静态语义禁止,以便将来可以添加支持。
范围之外
对于将“可选”思想应用于其他构造,有各种有趣的想法。然而,它们不属于本提案的一部分。例如:
未解决问题
私有类字段和方法
Issue #28:可选链是否应该支持即将推出的私有类字段和私有方法,例如 a?.#b、a?.#b() 或 a?.b.#c?引用 microsoft/TypeScript#30167 (comment):
这个还没有被纳入提案,仅仅因为私有字段本身还没有成熟。因此,如果那个提案碰巧停滞,我们不想因此而推迟这个提案。一旦那个达到 Stage 4,我们届时会处理它。
常见问题解答
obj?.[expr]和func?.(arg)看起来丑陋。为什么不使用obj?[expr]和func?(arg),就像 <语言 X> 那样?-
我们不使用
obj?[expr]和func?(arg)语法,因为解析器难以高效地区分这些形式与条件操作符,例如obj?[expr].filter(fun):0和func?(x - 2) + 3 :1。这两种情况的替代语法各有缺陷;决定哪一种看起来最不糟糕主要是个人品味问题。以下是我们做出选择的方式:
- 为最常出现的
obj?.prop情况选择最佳语法; - 将可识别的
?.字符序列的使用扩展到其他情况:obj?.[expr]、func?.(arg)。
至于 <语言 X>,由于 <某些构造在 X 中不受支持或工作方式不同>,它有与 JavaScript 不同的语法约束。
- 为最常出现的
- 好吧,但我真的认为 <替代语法> 更好。
-
过去已经探索并广泛讨论了各种替代语法。没有一个达成共识。搜索带有“alternative syntax”标签的问题,以及那些对语义有影响的带有“alternative syntax and semantics”标签的问题。
- 为什么
(null)?.b求值为undefined而不是null? -
a.b和a?.b都不打算保留基对象a上的任意信息,而只是提供关于该对象属性"b"的信息。如果对象a上不存在属性"b",这通过a.b === undefined和a?.b === undefined来体现。特别是,值
null被认为没有属性;因此,(null)?.b是 undefined。 - 为什么
foo?.()在 foo 既不是空值也不是可调用时抛出异常? -
想象一个库,它将在用户提供时调用处理函数,例如
onChange。如果用户提供了数字3而不是函数,库可能会希望抛出异常并告知用户错误用法。这正是onChange?.()的提议语义所达到的效果。此外,这确保了
?.在所有情况下具有一致的含义。我们不是让调用成为检查typeof foo === 'function'的特殊情况,而是始终检查foo == null。最后,请记住可选链不是错误抑制机制。
- 为什么你想要长短路?
- 在
a?.b.c中,如果a.b是null,那么a.b.c会求值为undefined,对吗? -
不对。在尝试获取
a.b的属性"c"时会抛出 TypeError。短路的机会只发生一次,就在评估可选链操作符的左侧之后。如果该检查结果为否定,则继续正常评估。
换句话说,
?.操作符只在被评估的那一刻起作用。它不会改变后续属性访问、方法或函数调用的语义。 - 在像
a?.b?.c这样深度嵌套的链中,为什么我必须在每一层写?.?我能否只对整个链写一次操作符? -
按设计,我们希望开发人员能够标记他们期望为 null/undefined 的每个位置,并且只标记那些位置。事实上,我们认为意外的 null/undefined 值,作为可能错误的症状,应该作为 TypeError 报告,而不是被掩盖。
- ...但是,在深度嵌套的链的情况下,我们几乎总是希望在每个级别测试
null/undefined,不是吗? -
深度嵌套的树状结构并不是可选链的唯一用例。
另请参见 CoffeeScript 中可选链的使用统计,并比较“总 soak 操作”与“链在另一个 soak 之上的总 soak 操作”。
- 这个功能看起来像一个错误抑制操作符,对吗?
-
不对。可选链只是检查某个值是否为 undefined 或 null。它不会捕获或抑制由周围代码评估抛出的错误。例如:
规范
参见:https://tc39.github.io/proposal-optional-chaining/
支持
- 引擎,参见:https://github.com/tc39/proposal-optional-chaining/issues/115
- 工具,参见:https://github.com/tc39/proposal-optional-chaining/issues/44
委员会讨论
参考
- TC39 Slide Deck: Null Propagation Operator
- es-optional-chaining (@claudepache)
- ecmascript-optionals-proposal (@davidyaha)