可迭代对象及其相关的迭代器是 ES6 的一个特性,数组(包括 Typed Array)、字符串、Set 对象和 Map 对象都是可迭代的。这意味着可以通过 for/of
循环来迭代它们,...
运算符能够扩展 可迭代对象
,迭代器可以用于结构赋值。而且有些接收 Array 对象的内置函数/构造函数也可以接收任意迭代器。
本章解释迭代器的原理,并展示如何创建可迭代的数据结构。理解迭代后,再学习生成器,它是 ES6 的一个新特性,用于简化迭代器的创建。
迭代器原理
要理解 JavaScript 中的迭代,必须理解 3 个不同的类型:
- 首先是可迭代对象,类似于 Array、Set、Map,都是可以迭代的
- 其次是迭代器对象,用于执行迭代
- 最后是迭代结果对象,保存每次迭代的结果
具体来说:
可迭代对象
是指任何具有特定迭代器方法的对象,且该方法返回迭代器对象
迭代器对象
指的是任何具有next()
方法的对象,且该next()
方法返回迭代结果对象
迭代结果对象
是具有属性value
和done
的对象
要迭代一个 可迭代对象
,首先要调用其迭代器方法获得一个 迭代器对象
。然后,重复调用这个 迭代器对象
的 next()
方法,直至返回 done 属性为 true 的迭代结果对象。
可迭代对象的迭代器方法没有使用惯用名称,而是使用了符号 Symbol.iterator
作为名字。下面展示了可迭代对象的迭代过程:
1 | let iterable = [99, 100]; |
特别地,内置可迭代数据类型的迭代器对象本身也是可迭代的(即迭代器对象本身也具有名为 Symbol.iterator
的方法,返回它们自己):
1 | > let list = [1, 2, 3, 4, 5]; |
实现可迭代对象
在 ES6 中,可迭代对象非常重要。因此,只要你的数据类型表示某种可迭代的结构,就应该考虑把它们实现为可迭代对象。
- 为了让类可迭代,必须实现一个名为
Symbol.iterator
的方法。这个方法必须返回一个迭代器对象 - 迭代器对象必须有一个
next()
方法,而这个next()
方法必须返回一个迭代结果对象,该对象有一个 value 属性和/或一个布尔值 done 属性
1 | class Range { |
除了可以把类变成可迭代的类之外,定义返回可迭代值的函数也很有用。如下是一个示例:
1 | function map(iterable, f) { |
可迭代对象与迭代器有一个重要的特点,即它们天性懒惰:如果计算下一个值需要一定的计算量,则相应计算会推迟到实际需要下一个值的时候再发生。
关闭
迭代器方法:return()
方法
除了 next()
方法,迭代器对象还可以实现 return()
方法:
- 如果迭代在 next() 返回 done 属性为 true 的迭代结果之前停止(最常见的原因是通过 break 语句提前退出 for/of 循环),那么解释器就会检查迭代器对象是否有
return()
方法 - 如果有,解释器就会调用它(不传参数),让迭代器有机会关闭文件、释放内存,或者做一些其他清理工作
- 这个 return() 方法必须返回一个迭代器结果对象。这个对象的属性会被忽略,但返回非对象值会导致报错
生成器
生成器是一种使用强大的新 ES6 语法定义的迭代器,特别适合要迭代的值不是某个数据结构的元素,而是计算结果的场景。生成器极大地简化了迭代器的创建。
要创建生成器,首先必须定义一个生成器函数。生成器函数在语法上类似常规的 JavaScript
函数,但使用的关键字是 function*
而非 function
。
- 调用
生成器函数
并不会实际执行函数体,而是返回一个生成器
。这个生成器是一个迭代器 - 调用生成器的
next()
方法会导致生成器函数的函数体从头(或从当前位置)开始执行,直至遇见一个yield
语句 - yield 是 ES6 的新特性,类似于 return 语句。yield 语句的值会成为调用迭代器的 next() 方法的返回值
1 | // 生成器函数的定义 |
与常规函数一样,也可以使用表达式定义生成器。同样,只要在 function 关键字前面加个星号即可:
1 | const seq = function*(from, to) { |
在类和对象字面量中,定义方法时可以使用简写形式,省略 function 关键字。在这种情况下定义生成器,只要在应该出现 function 关键字的地方(如果用的话)加一个星号:
1 | let o = { |
不能使用箭头函数语法定义生成器函数。
生成器在定义可迭代类时特别有用,例如上面的 Range 示例,其 [Symbol.iterator]
方法可以改写为生成器函数:
1 | *[Symbol.iterator]() { |
如下是一个更复杂的生成器函数示例,它可以交替回送多个可迭代对象的元素:
1 | function* zip(...iterables) { |
yield* 与递归生成器
有时候我们有如下需求,在生成器函数中回送其他可迭代对象元素:
1 | function* sequence(...iterables) { |
这种在生成器函数中回送其他可迭代对象元素的操作很常见,所以 ES6 为它定义了特殊语法。yield*
关键字与 yield 类似,但它不是只回送一个值,而是迭代可迭代对象并回送得到的每个值。所以上述代码可以简化为:
1 | function* sequence(...iterables) { |
注意,yield
和 yield*
只能在生成器函数中使用,而箭头函数不能定义成生成器函数,因此箭头函数中不能出现 yield。
yield*
可以用来迭代任何可迭代对象,包括通过生成器实现的。这意味着使用 yield*
可以定义递归生成器,利用这个特性可以通过简单的 非递归迭代
遍历 递归定义的树结构
。
高级生成器特性
生成器函数最常见的用途是创建迭代器,但生成器的基本特性是可以暂停计算,回送中间结果,然后在某个时刻再恢复计算,这意味着生成器拥有超越迭代器的特性。
生成器函数的返回值
与其他函数一样,生成器函数也可以返回值(通过 return 返回值)。对于迭代器/生成器而言,其 next()
返回迭代结果对象,通常:
- 如果 value 属性有定义,则 done 属性未定义或者为 false
- 如果 done 是 true,那么 value 属性就是未定义的
但是当生成器返回值时,最后一次调用 next() 返回的对象 value 和 done 都有定义:value 是生成器返回的值,done 是 true(表示没有可迭代的值了)。
最后这个值会被 for/of
循环和扩展操作符忽略(因为 done 为 true),但手工迭代时可以通过显式调用 next()
得到:
1 | function *oneAddDone() { |
yield 表达式的值
事实上,yield是一个表达式(回送表达式),它可以也可以有值。
- 调用生成器的
next()
方法时,生成器函数会一直运行直到到达一个 yield 表达式 - yield 关键字后面的表达式会被求值,该值成为
next()
调用的返回值。此时,生成器函数就在求值yield 表达式
的中途停了下来 - 下一次调用生成器的 next() 方法时,传给
next()
的参数会变成暂停的 yield 表达式的值
如下是一个说明性质的示例:
1 | function* smallNumbers() { |
1 | generator created |
尤其要注意,注意以上代码是不对称的。第一次调用 next()
启动生成器,但传入的值无法在生成器中访问到。
生成器的 return() 和 throw() 方法
综上我们得知,我们可以接收生成器回送或返回的值。同时,也可以通过生成器的 next()
方法给运行中的生成器传值。
除了通过 next()
为生成器提供输入之外,还可以调用它的 return()
和 throw()
方法,改变生成器的控制流。顾名思义,在生成器上调用这两个方法会导致它返回值或抛出异常,就像生成器函数中的一下条语句是 return 或 throw 一样。
之前说过,对于迭代器,如果迭代器定义了 return()
方法且迭代提前停止,解释器会自动调用 它的 return()
方法,从而让迭代器有机会关闭文件或做一些其他清理工作。对生成器而言:
- 我们无法定义这样一个
return()
方法来做清理工作,但可以在生成器函数中使用try/finally
语句,保证生成器返回时(在 finally 块中)做一些必要的清理工作 - 在强制生成器返回时,生成器内置的
return()
方法可以保证这些清理代码运行
1 | function* test() { |
生成器的 throw()
方法为我们提供了(以异常形式)向生成器发送任意信号的途径。调用 throw()
方法就会导致生成器函数抛出异常。如果生成器函数中有适当的异常处理代码,则这个异常就不一定致命,而是可以成为一种改变生成器行为的手段。
生成器小结
生成器是一种非常强大的通用控制结构,它赋予我们通过 yield 暂停计算并在未来某个时刻以任意输入值重新启动计算的能力:
- 可以使用生成器在单线程 JavaScript 代码中创建某种协作线程系统
- 也可以利用生成器来掩盖程序中的异步逻辑,这样尽管某些函数调用依赖网络事件,实际上是异步的,但代码看起来还是顺序的、同步的
利用生成器的代码通常比较晦涩,JavaScript 专门新增了 async/await 语法(后续介绍),来简化生成器的这种用法。