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/year/pending/proposal-deiter.md.
  • 简体中文
  • Double-Ended Iterator and Destructuring S1

    中文标题:双端迭代器与解构

    提案概览
    提案速览

    该提案引入了双端迭代器,可以通过 next() 和 nextLast() 从序列两端消耗元素,从而支持类似 [first, .rest, last] = iterable 的解构模式。它解决了朴素 rest 数组方法的低效和潜在无限循环问题,并包含了生成器和迭代器辅助方法的机制。

    Note

    以下 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],所有这些都使得 first1last4rest[2, 3]。但令人惊讶的是 [first, ...rest, last] = [1, 2, 3, 4] 在 JavaScript 中不起作用。

    在某些情况下,我们确实希望从末尾获取元素,例如从使用函数时 String.prototype.replace 中获取 matchIndex

    string.replace(pattern, (fullMatch, ...submatches, matchIndex, fullString) => {
      // `matchIndex` 总是倒数第二个参数(完整字符串是最后一个参数)。
      // 根据模式不同,可能有多个子匹配参数。
    })

    解决方案

    一个朴素的解决方案是让 let [first, ...rest, last] = iterable 工作得像

    let [first, ...rest] = iterable
    let last = rest.pop()

    问题在于它需要将所有的项都保存在 rest 数组中,尽管你可能只需要 last。一个可能的缓解措施是支持 [..., last] = iterable,这样可以节省 rest 的内存,但你仍然需要消耗整个迭代器。在 iterable 是很大的数组或类似 Number.range(1, 100000) 的情况下,这是非常低效的。而且像 let [first, ..., last] = repeat(10) 这样的情况中,repeat 是一个返回无限相同值的生成器,理论上 firstlast 都可以是 10,但你只会陷入死循环。

    与其采用朴素的解决方案,我们引入了双端迭代器(类似于 Rust std::iter::DoubleEndedIterator)。双端迭代器可以从两端消耗,next() 消耗序列剩余项中的第一项,nextLast() 消耗序列剩余项中的最后一项。

    let a = [1, 2, 3, 4, 5, 6]
    let deiter = a.values() // 假设 values() 会升级为返回双端迭代器
    deiter.next() // {value: 1}
    deiter.next() // {value: 2}
    deiter.nextLast() // {value: 6}
    deiter.next() // {value: 3}
    deiter.nextLast() // {value: 5}
    deiter.nextLast() // {value: 4}
    deiter.nextLast() // {done: true}
    deiter.next() // {done: true}

    有了双端迭代器,let [a, b, ..., c, d] = iterable 大致会工作得像

    let iter = iterable[Symbol.iterator]()
    let a = iter.next().value
    let b = iter.next().value
    let d = iter.nextLast().value
    let c = iter.nextLast().value
    iter.return()

    生成器

    生成器函数提供了一种在用户空间创建迭代器的简洁语法。例如,你可以编写 values(arrayLike),它返回所有类数组对象的迭代器:

    function *values(arrayLike) {
      let i = 0
      while (i < arrayLike.length) {
        yield arrayLike[i]
        i++
      }
    }

    要在用户空间实现 values(arrayLike) 的双端版本,我们可以使用 doubleEnded 辅助函数

    const values = doubleEnded(function *values(arrayLike, context) {
      let i = 0, j = 0
      while (i + j < arrayLike.length) {
        if (context.method == "nextLast") {
          yield arrayLike[arrayLike.length - 1 - j]
          j++
        } else { // context.method == "next"
          yield arrayLike[i]
          i++
        }
      }
    })

    它还可以用作装饰器(stage 3 提案):

    class IntRange {
      constructor(start, end) {
        if (!Number.isSafeInteger(start)) throw new TypeError()
        if (!Number.isSafeInteger(end)) throw new TypeError()
        this.start = start; this.end = end; Object.freeze(this)
      }
      @doubleEnded *[Symbol.iterator](context) {
        let {start, end} = this
        while (start < end) {
          if (context.method == "nextLast") yield --end
          else yield start++
        }
      }
    }

    迭代器辅助方法和反向迭代器

    迭代器辅助方法 为迭代器和异步迭代器添加了一些有用的方法。大多数方法很容易升级以支持双端。例如,map() 可以这样实现:

    // 仅用于演示,实际实现应使用内部槽位
    Iterator.prototype.map = function (fn) {
      let iter = {
        __proto__: Iterator.prototype,
        return(value) { this.return?.(); return {done: true, value} }
      }
      if (this.next) iter.next = () => fn(this.next())
      if (this.nextLast) iter.nextLast = () => fn(this.nextLast())
      return iter
    }
    // 用法
    let [first, ..., last] = [1, 2, 3].values().map(x => x * 2)
    first // 2
    last // 6

    此外,可以引入一些额外的迭代器辅助方法,如 toReversedtakeLastdropLastreduceRight 等。

    例如,toReversed()

    // 仅用于演示,实际实现应使用内部槽位
    Iterator.prototype.toReversed = function () {
      let next = this.nextLast?.bind(this)
      let nextLast = this.next?.bind(this)
      let ret = this.return?.bind(this)
      return {
        __proto__: Iterator.prototype,
        next, nextLast, return: ret,
      }
    }
    // 用法
    let a = [1, 2, 3, 4]
    let [first, ..., last] = a.values().toReversed()
    first // 4
    last // 1

    有了双端迭代器和 toReversed() 辅助方法,我们可能不需要反向迭代器。即使我们仍然想要反向迭代器作为单独的协议,我们也可以轻松地为其提供默认实现。

    Iterator.prototype[Symbol.reverseIterator] = function () {
      return this[Symbol.iterator].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() 实际上需要恢复到之前的未完成状态。

    已有先例

    之前的讨论

    旧的讨论