Double-Ended Iterator and Destructuring S1
中文标题:双端迭代器与解构
- 阶段: Stage 1
- 状态: 进行中
- ECMAScript 版本: —
- 同步时间: 2026年8月26日
- English original · 官方仓库
该提案引入了双端迭代器,可以通过 next() 和 nextLast() 从序列两端消耗元素,从而支持类似 [first, .rest, last] = iterable 的解构模式。它解决了朴素 rest 数组方法的低效和潜在无限循环问题,并包含了生成器和迭代器辅助方法的机制。
以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。
双端迭代器与解构
状态:Stage 1
作者:贺师俊(hax)
提案负责人:贺师俊(hax)
动机
Python 和 Ruby 支持 (first, *rest, last) = [1, 2, 3, 4],CoffeeScript 支持 [first, rest..., last] = [1, 2, 3, 4],Rust 支持 [first, rest @ .., last] = [1, 2, 3, 4],所有这些都使得 first 为 1,last 为 4,rest 为 [2, 3]。但令人惊讶的是 [first, ...rest, last] = [1, 2, 3, 4] 在 JavaScript 中不起作用。
在某些情况下,我们确实希望从末尾获取元素,例如从使用函数时 String.prototype.replace 中获取 matchIndex:
解决方案
一个朴素的解决方案是让 let [first, ...rest, last] = iterable 工作得像
问题在于它需要将所有的项都保存在 rest 数组中,尽管你可能只需要 last。一个可能的缓解措施是支持 [..., last] = iterable,这样可以节省 rest 的内存,但你仍然需要消耗整个迭代器。在 iterable 是很大的数组或类似 Number.range(1, 100000) 的情况下,这是非常低效的。而且像 let [first, ..., last] = repeat(10) 这样的情况中,repeat 是一个返回无限相同值的生成器,理论上 first 和 last 都可以是 10,但你只会陷入死循环。
与其采用朴素的解决方案,我们引入了双端迭代器(类似于 Rust std::iter::DoubleEndedIterator)。双端迭代器可以从两端消耗,next() 消耗序列剩余项中的第一项,nextLast() 消耗序列剩余项中的最后一项。
有了双端迭代器,let [a, b, ..., c, d] = iterable 大致会工作得像
生成器
生成器函数提供了一种在用户空间创建迭代器的简洁语法。例如,你可以编写 values(arrayLike),它返回所有类数组对象的迭代器:
要在用户空间实现 values(arrayLike) 的双端版本,我们可以使用 doubleEnded 辅助函数:
它还可以用作装饰器(stage 3 提案):
迭代器辅助方法和反向迭代器
迭代器辅助方法 为迭代器和异步迭代器添加了一些有用的方法。大多数方法很容易升级以支持双端。例如,map() 可以这样实现:
此外,可以引入一些额外的迭代器辅助方法,如 toReversed、takeLast、dropLast 和 reduceRight 等。
例如,toReversed():
有了双端迭代器和 toReversed() 辅助方法,我们可能不需要反向迭代器。即使我们仍然想要反向迭代器作为单独的协议,我们也可以轻松地为其提供默认实现。
常见问题
我喜欢允许 ... 出现在解构模式中间的想法,但为什么要引入“双端”迭代器?
因为 JavaScript 的 [first, ...rest] = sequence 解构是基于可迭代协议的,所以我们应该让 [first, ...rest, last] = sequence 也基于可迭代协议。而 [a, b, ...rest, c, d, e] = sequence 可以完美地解释为“从序列中取前两个元素,然后取最后三个,其余在中间”,也就是允许从序列的另一端取元素,这在概念上与常见数据结构双端队列中“双端”的含义相同。注意,JavaScript 已经有一些数组 API 表现得像双端:'indexOf/lastIndexOf, reduce/reduceRight, find/findLast' 等。因此,推广这个概念可以增强所有可能基于类似抽象的 API(包括用户库)的一致性。关于性能、心智负担和设计成本的进一步分析,请参见双端解构的可选机制。
迭代器/生成器如何回到之前的状态?
它不是“向后移动”或“退一步”,而是“从另一端消耗下一个值”或“从另一端缩短值的范围”。
有两个容易混淆的概念:双向 和 双端。双向意味着你可以调用 next()(向前移动)或 previous()(向后移动)。双端意味着你可以调用 next()(消耗序列剩余项的第一项)或 nextLast()(消耗序列剩余项的最后一项)。
本提案的初始版本使用了 next("back"),这遵循了 Rust 的 nextBack()。“back”一词可能来自 C++ 的 vector/deque(见 https://cplusplus.com/reference/vector/vector/back/),意为“最后一个元素”。这个用法在 JavaScript 社区中并不流行,并引起混淆,所以我们将这个词从“back”改为“last”。
什么是“双端”?它与“双向”有何不同?
为了帮助理解这些概念,你可以想象你用光标指向序列中的位置并获取该位置的值。普通迭代需要一个光标,最初光标在序列的最左侧。你只能将光标向右移动并通过 next() 获取该位置的值。双向意味着你也可以通过 previous() 向左移动光标,从而回到序列的前一个位置,并(再次)获取该位置的值。
双端意味着你有两个光标,最初一个在最左侧且只能向右移动,另一个在最右侧且只能向左移动。因此,你使用 next() 将第一个光标向右移动并获取其位置处的值,使用 nextLast() 将第二个光标向左移动并获取其位置处的值。如果两个光标相遇在同一位置,则序列已完全消耗。
你会发现这两个概念实际上是正交的,所以理论上我们可以同时拥有双向和双端。因此,next()/previous() 移动第一个光标向右/向左,nextLast()/previousLast() 移动第二个光标向左/向右。
注意,即使这两者可以共存,双向不兼容 JavaScript 迭代器协议,因为 JavaScript 迭代器是一次性消耗,当所有值被消耗时产生 {done: true},并且要求之后 next() 总是返回 {done: true},但 previous() 实际上需要恢复到之前的未完成状态。
已有先例
- Python 可迭代解包
- Ruby 数组分解
- CoffeeScript 带 splats 的解构赋值
- Rust 子切片模式
- Rust std::iter::DoubleEndedIterator
- Rust Macro improved_slice_patterns::destructure_iter
之前的讨论
- https://github.com/tc39/proposal-array-last/issues/31
- https://github.com/tc39/proposal-reverseIterator/issues/1
- https://es.discourse.group/t/bidirectional-iterators/339
旧的讨论
- https://esdiscuss.org/topic/early-spread-operator
- https://esdiscuss.org/topic/parameter-lists-as-arguments-destructuring-sugar#content-3
- https://mail.mozilla.org/pipermail/es-discuss/2012-June/023353.html
- http://web.archive.org/web/20141214094119/https://bugs.ecmascript.org/show_bug.cgi?id=2034
- https://esdiscuss.org/topic/rest-parameter-anywhere
- https://esdiscuss.org/topic/rest-parameters
- https://esdiscuss.org/topic/strawman-complete-array-and-object-destructuring
- https://esdiscuss.org/topic/an-update-on-rest-operator