神奇的 Symbol.iterator
这篇文章将分享关于 Symbol.iterator
的一些基础知识和演示 Demo,内容相对基础,如果你熟练掌握 Iterator
和 Generator
,继续阅读下去可能不会有太多帮助。
认识 Iterator
Iterator
翻译中文是迭代器,在设计模式中,迭代器模式是一种简单常见的模式,它可以让用户透过特定的接口遍历集合中的每一个元素,而不用了解每个容器底层的实现。
在这个模式中,每个可迭代的集合类需要实现 next
接口(继续遍历下一个元素) 和 hasNext
接口(是否已遍历结束)
———图片来自维基百科
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
每个迭代器有三个方法 next
,return
和 throw
,其中只有 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
或者出现异常,那么return
或throw
就会被执行,可以在这里做状态重置,不是这篇文章的重点就不展开了。
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 为数字的属性呢,还是一视同仁遍历这个对象的全部属性呢?
毋庸置疑,Array
,Map
,Set
,String
等等类可以在被 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] }
而 Number
,Object
就没有实现 [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.prototype
的iterator
实现,效果也相同。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
的状态,自行判断迭代状态返回 done
为 true
或 false
。
ES6
中其实提供了 GeneratorFunction
(生成器方法)来为迭代器服务,编写一个生成器方法很简单,在 function
后面加一个 *
号,接着在函数体中使用 yield
关键字来中断代码执行,移交代码执行权,外部就可以拿到 yield
的 value。
以返回多个数字为例:
function* gen () {
yield 0
yield 1
yield 2
}
生成器方法让函数执行过程可以中断和恢复,且中断时函数的执行上下文会保留。以往一个 Function 只能 return 一个结果,现在可以 yield 多次了。每次
yield
,迭代器都会中断代码执行并移交代码执行权;迭代器next
方法被调用时,生成器方法又会在上次中断的位置恢复执行。
还是一样的,调用这个方法获得 Iterator
对象,然后一直调用它的 next
方法获取返回值,直到 done
为 true
。
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
,执行权委托给了 g1
,yield 2
和 yield 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
中都可以使用。
参考文章
转载自:https://juejin.cn/post/7105280776786149389