likes
comments
collection

神奇的 Symbol.iterator

作者站长头像
站长
· 阅读数 12

这篇文章将分享关于 Symbol.iterator 的一些基础知识和演示 Demo,内容相对基础,如果你熟练掌握 IteratorGenerator,继续阅读下去可能不会有太多帮助。

认识 Iterator

Iterator 翻译中文是迭代器,在设计模式中,迭代器模式是一种简单常见的模式,它可以让用户透过特定的接口遍历集合中的每一个元素,而不用了解每个容器底层的实现。

在这个模式中,每个可迭代的集合类需要实现 next 接口(继续遍历下一个元素) 和 hasNext 接口(是否已遍历结束)

神奇的 Symbol.iterator ———图片来自维基百科

Java 示例代码:

public class IteratorExample {
    public static Iterator<Integer> range(int start, int end) {
        return new Iterator<>() {
            private int index = start;
      
            @Override
            public boolean hasNext() {
                return index < end;
            }

            @Override
            public Integer next() {
                if (!hasNext()) {
                    throw new NoSuchElementException();
                }
                return index++;
            }
        };
    }
    
    public static void main(String[] args) {
        var iterator = range(0, 10);
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

ES6 中的 Iterator

类型定义

ES6 规范了 Iterator 的实现,我们先来看看几个 Typescript 类型定义。

Iterable

为了实现可迭代,一个对象需要实现 Iterable接口,这个接口要求对象(或其原型链上)拥有一个 Symbol.iterator 属性,它是一个方法,返回一个 Iterator 对象。

interface Iterable<T> {
    [Symbol.iterator](): Iterator<T>;
}

Symbol.iterator 是个专门用于定义迭代器实现的标志符,由于 Symbol 有唯一的特性,不用担心会和用户自定义的字符串属性冲突。

实现了这个接口,引擎就知道如何理解这个需求,在遵循这个迭代标准的语法中也就可以使用了,例如 for...of,展开运算法 ... 等等,详情看后面的例子,接着看 Iterator 类型。

Iterator

每个迭代器有三个方法 nextreturnthrow,其中只有 next 是必须的,它们都需要返回 IteratorResult 类型。

interface Iterator<T, TReturn = any, TNext = undefined> {
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    return?(value?: TReturn): IteratorResult<T, TReturn>;
    throw?(e?: any): IteratorResult<T, TReturn>;
}

如果在迭代过程中执行 break 或者出现异常,那么 returnthrow 就会被执行,可以在这里做状态重置,不是这篇文章的重点就不展开了。

IteratorResult 又是什么?

IteratorResult

IteratorResult 拥有两个属性

  • done 用于标记迭代是否已完成,值为 false 或者 undefined 时,迭代可以继续;值为 true 迭代完成,不能再继续遍历了。
  • value 是每个迭代器元素的值;
interface IteratorYieldResult<TYield> {
    done?: false;
    value: TYield;
}

interface IteratorReturnResult<TReturn> {
    done: true;
    value: TReturn;
}

type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;

示例

For...Of

前面说了,只要实现 Iterable 接口的对象,就可以在 for...of 迭代,我们手动写一个 Demo。首先创建一个普通的类数组对象,并用 for...of 中尝试遍历它(你可以把代码复制出来再 console 中执行)。

const obj = {
   0: 'Hello',
   1: 'World',
   length: 2
}

// Uncaught TypeError: obj is not iterable
for (const v of obj) { 
  console.log(v)
}

执行之后抛出了错误 Uncaught TypeError: obj is not iterable,因为引擎不知道怎么遍历这个对象,是要遍历所有 key 为数字的属性呢,还是一视同仁遍历这个对象的全部属性呢?

毋庸置疑,ArrayMapSetString 等等类可以在被 for...of 中执行,因为它们都实现了 Symbol.iterator 接口。

[][Symbol.iterator]
// ƒ values() { [native code] }

('')[Symbol.iterator]
// ƒ values() { [native code] }

(new Map())[Symbol.iterator]
// ƒ values() { [native code] }

(new Set())[Symbol.iterator]
// ƒ values() { [native code] }

NumberObject 就没有实现 [Symbol.iterator]

({})[Symbol.iterator]
// undefined

(0)[Symbol.iterator]
// undefined

为了让例子中的 obj 可以被 for...of 正确识别,也需要实现可迭代接口。

obj[Symbol.iterator] = function () {
  // 保留 obj 的引用
  const self = this
  return {
    // 当前迭代下标
    cur: 0,
    next: function () {
      // 迭代结束需要返回 done = true
      if (this.cur >= self.length) {
          return { done: true }
      }
      // 迭代尚未结束,返回 done = false 或 undefined,以及每个迭代过程中的 value。
      return { value: self[this.cur++] }
    }
  }
}

for (const v of obj) { console.log(v) }
// Hello
// World

由于这个例子是类数组,遍历模式和数组一样,更简单地,可以直接复用 Array.prototypeiterator 实现,效果也相同。

const obj = {
   0: 'Hello',
   1: 'World',
   length: 2
}
obj[Symbol.iterator] = Array.prototype[Symbol.iterator]
for (const v of obj) { console.log(v) }
// Hello
// World

手动调用

我们也可以主动调用 Symbol.iterator 属性,获取 Iterator 对象,然后手动控制迭代过程。

const arr = ['0', '1', '2']

const iterator = arr[Symbol.iterator]()

iterator.next()
// {value: '0', done: false}

iterator.next()
// {value: '1', done: false}

iterator.next()
// {value: '2', done: false}

iterator.next()
// {value: undefined, done: true}

把调用迭代器的 next 方法,判断迭代是否完成包装成一个方法,就可以实现一个自己的 for...of

function myForOf (iterable, callback) {
    let iterator = iterable[Symbol.iterator]()
    let result = iterator.next()
    while (!result.done) {
      callback(result.value)
      result = iterator.next()
    }
}

再举一个更贴合实际场景的例子,我们有一个链表节点的类,每个对象拥有两个属性 val 节点值和 next 指针指向下一个节点。

function LinkNode (val, next) {
   this.val = val
   this.next = next || undefined
}

为了隐藏链表集合背后的实现逻辑,决定为这个类提供迭代器模式,方便标准化遍历。举个例子,通过下面的代码我们创建一条 2->1->0 的链表。

const head = new LinkNode(2, new LinkNode(1, new LinkNode(0)))

编写 Symbol.iterator 的实现逻辑。

LinkNode.prototype[Symbol.iterator] = function () {
    return {
        cur: this,
        next: function () {
            if (!this.cur) {
                return { done: true }
            }
            const res = { value: this.cur.val, done: false }
            this.cur = this.cur.next
            return res
        }
    }
}

实现完之后,使用 for...of ,展开运算符,或者 Array.from 都可以输出链表了。

[...head]
// [2, 1, 0]

Array.from(head)
// [2, 1, 0]

注意📢,外部迭代 LinkNode 的过程并不需要理解其内部实现。

GeneratorFunction

上面的例子我们都是手动维护 Iterator 的状态,自行判断迭代状态返回 donetruefalse

ES6 中其实提供了 GeneratorFunction(生成器方法)来为迭代器服务,编写一个生成器方法很简单,在 function 后面加一个 * 号,接着在函数体中使用 yield 关键字来中断代码执行,移交代码执行权,外部就可以拿到 yieldvalue

以返回多个数字为例:

function* gen () {
   yield 0
   yield 1
   yield 2
}

生成器方法让函数执行过程可以中断和恢复,且中断时函数的执行上下文会保留。以往一个 Function 只能 return 一个结果,现在可以 yield 多次了。每次 yield,迭代器都会中断代码执行并移交代码执行权;迭代器 next 方法被调用时,生成器方法又会在上次中断的位置恢复执行。

还是一样的,调用这个方法获得 Iterator 对象,然后一直调用它的 next 方法获取返回值,直到 donetrue

const iteraotr = gen()

iteraotr.next()
// {value: 0, done: false}
iteraotr.next()
// {value: 1, done: false}
iteraotr.next()
// {value: 2, done: false}
iteraotr.next()
// {value: undefined, done: true}

执行结果和我们手动维护 Iterator 结构体是一模一样的。基于 GeneratorFunction 编写 Iterator 方法就会简短很多。

const obj = {
   0: 'Hello',
   1: 'World',
   length: 2
}

obj[Symbol.iterator] = function* () {
    for (let i = 0; i < this.length; i ++) {
        yield this[i]
    }
}

[...obj]
// ['Hello', 'World']

yield*

除了 Function 可以带 *yield 关键字也可以,yield* 表示将执行权委托给另一个 Generator 或可迭代的对象。

直接上示例:

function* g1() {
  yield 2;
  yield 3;
}

function* g2() {
  yield 1;
  yield* g1();
  yield 4;
}

const iterator = g2();

[...iterator]
// [1, 2, 3, 4]

可以看到一开始进入 g2 方法,执行完 yield 1,遇到 yield* g1();,由于 g1() 返回结果也是 Iterator,执行权委托给了 g1yield 2yield 3,g1 方法执行完成之后,g2 恢复执行又 yield 4,所以根据迭代协议,最终得到结果是 [1,2,3,4]

试试利用 yield* 的特性,改造上面👆🏻的 LinkNode Symbol.iterator 实现只需要几行代码。

function LinkNode (val, next) {
   this.val = val
   this.next = next || undefined
}

LinkNode.prototype[Symbol.iterator] = function* () {
    yield this.val
    if (this.next) {
        yield* this.next
    }
}

const head = new LinkNode(2, new LinkNode(1, new LinkNode(0)))

[...head]
// [2, 1, 0]

解读:由于 LinkNode 原型链上实现了迭代协议,每个 LinkNode 对象都是可迭代的,每个节点在迭代过程中 yield 出自身的 value,如果还有 next 节点,就将 Iterator 委托给 next 节点。

总结

ES6 中新增了 Map、Set、TypedArray 等等集合的数据类型,为了更好地访问各种集合不同的数据结构,制定了标准的迭代协议,只要实现了迭代协议的对象即可被迭代。

实现可迭代的过程是在对象或对象的原型链上增加 Symbol.iterator 属性的方法,该方法需要返回 Iterator 结构体,可以使用 GeneratorFunction 轻松实现。

实现这个协议的好处是,可以屏蔽各种数据结构的内部实现(迭代 Set 对象并不需要了解它内部是如何存储的),在 for...of...展开运算符,Array.from 中都可以使用。

参考文章