For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/proposal-optional-chaining.md.
  • 简体中文
  • Optional Chaining S4

    中文标题:可选链

    提案概览
    提案速览

    本提案引入了可选链操作符(?),以简化对深层嵌套属性、方法或函数调用的访问,无需重复进行空值检查。如果基值为 null/undefined,则整个链会短路,并支持静态、动态和调用表达式。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    JavaScript 的可选链

    状态

    ECMAScript 提案 处于流程的 Stage 4 阶段。

    作者

    概述与动机

    当查找树状结构中深处的属性值时,常常需要检查中间节点是否存在:

    var street = user.address && user.address.street;

    此外,许多 API 要么返回对象,要么返回 null/undefined,并且可能只希望在其不为 null 时提取结果的属性:

    var fooInput = myForm.querySelector('input[name=foo]')
    var fooValue = fooInput ? fooInput.value : undefined

    可选链操作符允许开发者处理许多此类情况,而无需重复自身和/或将中间结果赋值给临时变量:

    var street = user.address?.street
    var fooValue = myForm.querySelector('input[name=foo]')?.value

    当缺失情况需要 undefined 以外的值时,通常可以通过 空值合并操作符 来处理:

    // 当 response.settings 缺失或为空值(response.settings == null)或 response.settings.animationDuration 缺失或为空值(response.settings.animationDuration == null)时,回退到默认值
    const animationDuration = response.settings?.animationDuration ?? 300;
    

    可选链的调用变体对于处理具有可选方法的接口很有用:

    iterator.return?.() // 手动关闭迭代器

    或者用于处理并非普遍实现的方法:

    if (myForm.checkValidity?.() === false) { // 在旧版浏览器中跳过测试
        // 表单验证失败
        return;
    }

    先前实践

    除非另有说明,以下语言中的语法由问号前置操作符组成(如 a?.ba?.b()a?[b]a?(b) 视情况而定)。

    以下语言实现的操作符与本提案具有相同的一般语义(即 1)防止空基值,2)对整个链进行短路):

    • C#: Null-conditional operator — 可空条件成员访问或索引,用于读取访问。
    • Swift: Optional Chaining — 可选的属性、方法或下标调用,用于读写访问。
    • CoffeeScript: Existential operator — 存在性操作符变体,用于属性访问器、函数调用、对象构造(new a?())。也适用于赋值和删除。

    以下语言具有类似功能,但不会在整个链长于一个元素时进行短路。这在这些语言中是合理的,因为方法或属性可能合法地用于 null(例如,在 Dart 中 null.toString() == "null"):

    以下语言具有类似功能。我们尚未检查它们与本提案在语义上是否存在显著差异:

    语法

    可选链操作符写作 ?.。它可以出现在三个位置:

    obj?.prop       // 可选静态属性访问
    obj?.[expr]     // 可选动态属性访问
    func?.(...args) // 可选函数或方法调用

    注意事项

    • 为了允许 foo?.3:0 被解析为 foo ? .3 : 0(为了向后兼容),在词法语法级别添加了一个简单的前瞻,使得字符序列 ?. 在那种情况下不会被解释为单个标记(?. 标记不能紧接着一个十进制数字)。

    语义

    基本情况

    如果 ?. 操作符左侧的操作数求值为 undefined 或 null,则整个表达式求值为 undefined。否则,正常触发目标属性访问、方法或函数调用。

    以下是基本示例,每个示例后面都有其脱糖版本。(脱糖版本并不完全准确,因为左侧应该只求值一次,并且 document.all 应该表现为对象。)

    a?.b                     // 如果 `a` 为 null/undefined,则为 undefined,否则为 `a.b`。
    a == null ? undefined : a.b
    
    a?.[x]                   // 如果 `a` 为 null/undefined,则为 undefined,否则为 `a[x]`。
    a == null ? undefined : a[x]
    
    a?.b()                   // 如果 `a` 为 null/undefined,则为 undefined
     a == null ? undefined : a.b() // 如果 `a.b` 不是函数则抛出 TypeError
                                // 否则,求值为 `a.b()`
    
    a?.()                    // 如果 `a` 为 null/undefined,则为 undefined
     a == null ? undefined : a()  // 如果 `a` 既不是 null/undefined,也不是函数,则抛出 TypeError
                                  // 否则调用函数 `a`

    短路

    如果 ?. 左侧的表达式求值为 null/undefined,则右侧不会被求值。这个概念称为短路

    a?.[++x]                  // 当且仅当 `a` 不为 null/undefined 时,`x` 才会递增
    a == null ? undefined : a[++x]

    长短路

    实际上,当触发短路时,不仅会跳过当前的属性访问、方法或函数调用,还会跳过紧随可选链操作符之后的整个属性访问、方法或函数调用链。

    a?.b.c(++x).d   // 如果 `a` 为 null/undefined,则求值为 undefined。变量 `x` 不会递增。
                    // 否则,求值为 `a.b.c(++x).d`。
    a == null ? undefined : a.b.c(++x).d

    请注意,空值检查只在 a 上进行。例如,如果 a 不为 null,但 a.b 为 null,则在尝试访问 a.b 的属性 "c" 时会抛出 TypeError。

    此功能由例如 C# 和 CoffeeScript 实现;参见 先前实践

    堆叠

    让我们将可选链定义为可选链操作符后跟一个属性访问、方法或函数调用链。

    一个可选链可以后跟另一个可选链。

    a?.b[3].c?.(x).d
    a == null ? undefined : a.b[3].c == null ? undefined : a.b[3].c(x).d
      // (通常,除了 `a` 和 `a.b[3].c` 只求值一次)

    边界情况:分组

    括号限制了短路的作用域:

    (a?.b).c
    (a == null ? undefined : a.b).c

    这是根据语法(如 && 操作符)来指定短路作用域的设计选择,而不是通过 Completion 的传播(如 break 指令)或临时的 Reference(如本提案的早期版本)。通常,语法不能被括号任意拆分:例如,({x}) = y 不是解构赋值,而是尝试给对象字面量赋值。

    请注意,无论语义如何,在那个位置使用括号实际上没有实际理由。

    可选删除

    由于 delete 操作符对其接受的内容非常宽松,我们免费获得了此功能:

    delete a?.b
    a == null ? true : delete a.b

    其中 true 是尝试删除非引用时的通常结果。

    为什么我们支持可选删除?
    • 懒惰(此论点放在第一位,不是因为它最重要,而是因为它为其他论点提供了正确的视角)。懒惰(连同急躁和傲慢)是规范编写者最重要的美德之一。也就是说,在其他条件几乎相等的情况下,选择需要更少内容并入规范的解决方案。

      现在,碰巧_支持_可选删除实际上不需要任何努力,而_不支持_它(通过使该构造成为早期语法错误)需要一些非平凡的努力。(技术细节参见 PR #73 (comment)。)

      因此,根据懒惰原则,正确的问题不是:“有充分理由支持可选删除吗?”,而是:“有充分理由_移除_对可选删除的支持吗?”

    • 缺乏强烈理由来_移除_支持。可选删除的支持语义是唯一可以预期的(前提是正确理解 delete 操作符的语义)。它不会以某种方式混淆程序员。事实上,唯一真正的理由是:“我们原本并未打算支持它。”

    • delete 操作符的一致性。这个操作符对其接受的内容非常宽松,甚至在给定没有意义的内容时也假装成功(例如,delete foo())。唯一的错误条件(早期或非早期)是在严格模式下,当尝试删除被禁止删除的内容(不可配置属性、变量等)时。支持可选删除与该模型很好地契合,而禁止它则不然。

    • 用例的存在。虽然不常见,但实践中确实存在(来自 Babel 的示例)。

    不支持的功能

    虽然为了完整性可以包含以下内容,但由于缺乏实际用例或其他令人信服的理由而不予支持;参见 Issue #22Issue #54 的讨论:

    • 可选构造:new a?.()
    • 可选模板字面量:a?.`string`
    • 可选链中或之后的构造函数或模板字面量:new a?.b()a?.b`string`

    以下内容不支持,尽管有一些用例;参见 Issue #18 的讨论:

    • 可选属性赋值:a?.b = c

    以下内容不支持,因为至少在实践上没有太大意义;参见 Issue #4 (comment)

    • 可选 super:super?.()super?.foo
    • 任何类似于属性访问或函数调用但不是的内容:new?.targetimport?.('foo') 等。

    所有上述情况都将被语法或静态语义禁止,以便将来可以添加支持。

    范围之外

    对于将“可选”思想应用于其他构造,有各种有趣的想法。然而,它们不属于本提案的一部分。例如:

    未解决问题

    私有类字段和方法

    Issue #28:可选链是否应该支持即将推出的私有类字段私有方法,例如 a?.#ba?.#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):0func?(x - 2) + 3 :1

    这两种情况的替代语法各有缺陷;决定哪一种看起来最不糟糕主要是个人品味问题。以下是我们做出选择的方式:

    • 为最常出现的 obj?.prop 情况选择最佳语法;
    • 将可识别的 ?. 字符序列的使用扩展到其他情况:obj?.[expr]func?.(arg)

    至于 <语言 X>,由于 <某些构造在 X 中不受支持或工作方式不同>,它有与 JavaScript 不同的语法约束。

    好吧,但我真的认为 <替代语法> 更好。

    过去已经探索并广泛讨论了各种替代语法。没有一个达成共识。搜索带有“alternative syntax”标签的问题,以及那些对语义有影响的带有“alternative syntax and semantics”标签的问题

    为什么 (null)?.b 求值为 undefined 而不是 null

    a.ba?.b 都不打算保留基对象 a 上的任意信息,而只是提供关于该对象属性 "b" 的信息。如果对象 a 上不存在属性 "b",这通过 a.b === undefineda?.b === undefined 来体现。

    特别是,值 null 被认为没有属性;因此,(null)?.b 是 undefined。

    为什么 foo?.() 在 foo 既不是空值也不是可调用时抛出异常?

    想象一个库,它将在用户提供时调用处理函数,例如 onChange。如果用户提供了数字 3 而不是函数,库可能会希望抛出异常并告知用户错误用法。这正是 onChange?.() 的提议语义所达到的效果。

    此外,这确保了 ?. 在所有情况下具有一致的含义。我们不是让调用成为检查 typeof foo === 'function' 的特殊情况,而是始终检查 foo == null

    最后,请记住可选链不是错误抑制机制

    为什么你想要长短路?

    参见 Issue #3 (comment)

    a?.b.c 中,如果 a.bnull,那么 a.b.c 会求值为 undefined,对吗?

    不对。在尝试获取 a.b 的属性 "c" 时会抛出 TypeError。

    短路的机会只发生一次,就在评估可选链操作符的左侧之后。如果该检查结果为否定,则继续正常评估。

    换句话说,?. 操作符只在被评估的那一刻起作用。它不会改变后续属性访问、方法或函数调用的语义。

    在像 a?.b?.c 这样深度嵌套的链中,为什么我必须在每一层写 ?.?我能否只对整个链写一次操作符?

    按设计,我们希望开发人员能够标记他们期望为 null/undefined 的每个位置,并且只标记那些位置。事实上,我们认为意外的 null/undefined 值,作为可能错误的症状,应该作为 TypeError 报告,而不是被掩盖。

    ...但是,在深度嵌套的链的情况下,我们几乎总是希望在每个级别测试 null/undefined,不是吗?

    深度嵌套的树状结构并不是可选链的唯一用例。

    另请参见 CoffeeScript 中可选链的使用统计,并比较“总 soak 操作”与“链在另一个 soak 之上的总 soak 操作”。

    这个功能看起来像一个错误抑制操作符,对吗?

    不对。可选链只是检查某个值是否为 undefined 或 null。它不会捕获或抑制由周围代码评估抛出的错误。例如:

    (function () {
        "use strict"
        undeclared_var?.b    // ReferenceError: undeclared_var is not defined
        arguments?.callee    // TypeError: 'callee' may not be accessed in strict mode
        arguments.callee?.() // TypeError: 'callee' may not be accessed in strict mode
        true?.()             // TypeError: true is not a function
    })()

    规范

    参见:https://tc39.github.io/proposal-optional-chaining/

    支持

    委员会讨论

    参考

    相关问题

    之前的讨论