阅读《JavaScript高级程序设计(第4版)》第七章及视频讲解
迭代器
迭代的意思是按照顺序反复多次执行一段程序,通常会有明确的终止条件。
可迭代对象(iterable): 实现了Iterator接口,必须属性”默迭代器(Symbol.iterator)“。
迭代器(iterator):按需创建的一次性对象,next方法返回IteratorResult对象。
IteratorResult对象:done表示是否还有更多值可以访问,true表示“耗尽”,value表示当前值,默认undefined
interface IteratorResult{
done: boolean;
value: any
}
interface Iterator{
next(): IteratorResult
}
interface Iterable{
[Symbol.iterator](): Iterator
}
可迭代协议
内置类型实现了Iterable接口
- 字符串
- 数组
- 映射
- 集合
- arguments对象
- NodeList等DOM集合
接受可迭代对象的原生特性:
-
for-of
-
数组解构
-
扩展操作符
-
Array.from()
-
创建集合
-
创建映射
-
Promise.all() 接收有Promise组成的可迭代对象
-
Promise.racel() 接收有Promise组成的可迭代对象
-
yield*操作符。生成器中使用
迭代器协议
迭代器next()方法:在可迭代对象中遍历数据,不调用next(),则无法知道迭代器的当前位置。
next()方法返回迭代器对象IteratorResult
const arr = ['foo', 'bar']
let iter = arr[Symbol.iterator]()
console.log(iter.next()) // {done: false, value: 'foo'}
console.log(iter.next()) // {done: false, value: 'bar'}
console.log(iter.next()) // {done: true, value: undefined}
迭代器并不知道怎么从可迭代对象中取得下一个值,也不知道可迭代对象有多大,只要迭代器到达done:true状态,后续调用next()就一直返回undefined。
console.log(iter.next()) // {done: true, value: undefined}
console.log(iter.next()) // {done: true, value: undefined}
不同迭代器的实例相互之间没有联系。
let iter1 = arr[Symbol.iterator]()
let iter2 = arr[Symbol.iterator]()
console.log(iter1.next()) // {done: false, value: 'foo'}
console.log(iter2.next()) // {done: false, value: 'foo'}
如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化
const arr = ['foo', 'bar']
let iter = arr[Symbol.iterator]()
console.log(iter.next()) // {done: false, value: 'foo'}
arr.splice(1, 0, 'Jane')
console.log(iter.next()) // {done: false, value: 'Jane'}
console.log(iter.next()) // {done: false, value: 'bar'}
console.log(iter.next()) // {done: true, value: undefined}
注意:迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象
提前终止迭代器
可选的return()方法用于指定在迭代器提前关闭时的执行逻辑。
- for-of 循环通过break,continue,return 和 throw 提前退出。
- 解构操作并未消费所有值
return()方法必须返回一个有效的IteratorResult对象,简单情况下,可以只返回{ done: true }
如果迭代器没有关闭,则还可以继续从上一次离开的的地方继续迭代,比如数组的迭代器就是不能关闭的。
const arr = [1, 2, 3, 4]
let iter = arr[Symbol.iterator]()
for(let i of iter){
console.log(i)
if(i > 1) break
}
// 1, 2
for(let i of iter){
console.log(i)
}
// 3, 4
return()方法是可选的。要知道某个迭代器是否可关闭,可以测试这个迭代器实例的return属性是不是函数对象。
给一个不可关闭的迭代器添加return()方法并不能让它变得可关闭。return()方法不会强制迭代器进入关闭状态,但是return()方法还是会调用的。
生成器
生成器是ES6新增加的一个极为灵活的结构,拥有在一个函数内部暂停和恢复代码执行的能力。
生成器基础
生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。只要可以定义函数的地方,就可以定义生成器。
function* generatorFn(){}
let generatorFn = function* (){}
let foo = {
* generatorFn(){}
}
class Foo {
* generatorFn(){}
}
class Foo {
static * generatorFn(){}
}
注意:箭头函数不能用来定义生成器。
调用生成器函数会产生一个生成器对象。生成器对象一开始处于暂停执行的状态。生成器对象实现了Iteration接口,因此具有next()方法。
next()方法的返回值类似迭代器,有一个done属性和一个value属性。函数体为空的生成器函数中间不会停留。
function* generatorFn(){}
const g = generatorFn()
console.log(g.next()) // {done: true, value: undefined} 函数体为空
value属性是生成器的返回值,默认值为undefined,可以通过生成器函数的返回值指定
function* generatorFn(){
return 'foo'
}
const g = generatorFn()
console.log(g.next()) // {done: true, value: 'foo'}
生成器函数只会在初次调用next()方法后开始执行。
function* generatorFn(){
console.log('bar')
}
const g = generatorFn() // 初次调用生成器函数并不会打印
g.next() // bar
生成器对象实现了Iterable接口。它们默认的迭代器是自引用。
通过yield中断执行
yield关键字可以使生成器停止和开始执行。也是生成器最有用的地方。
生成器函数在遇到yield之前会正常执行,之后执行会停止,函数作用域的状态会被保留。
停止执行的生成器只能使用next()方法恢复。
function* generatorFn(){
yield 'foo';
yield 'bar';
return 'Jane'
}
const g = generatorFn()
console.log(g.next()) // {done: false, value: 'foo'}
console.log(g.next()) // {done: false, value: 'bar'}
console.log(g.next()) // {done: true, value: 'Jane'}
生成器函数会区分生成器对象的作用域,生成器对象之间不会相互影响
...
const g1 = generatorFn()
const g2 = generatorFn()
console.log(g1.next()) // {done: false, value: 'foo'}
console.log(g2.next()) // {done: false, value: 'foo'}
yield 关键字只能在生成器函数内部使用。用在其他地方会抛出错误。
- 嵌套函数
- 箭头函数
function* generatorFn(){
function a(){
yield // 错误
}
}
function* generatorFn(){
const a = () => {
yield // 错误
}
}
生成器对象作为可迭代对象
在需要自定义迭代对象时,这样使用生成器对象会特别有用。比如,我们需要定义一个可迭代对象,而它会产生一个迭代器,这个迭代器会执行指定次数。
function* nTimes(n){
while (n--){
yield;
}
}
for(let _ of nTimes(3)){
console.log('foo')
}
// foo foo foo
使用yield实现输入输出
yield关键字可以作为函数的中间值参数使用。上一次让生成器函数暂停的yield关键字会接收到传给next()方法的第一个值。第一次调用next()传入的值不会被使用。因为这一次调用时为了开始执行生成器函数。
function* generatorFn(ini){
console.log(1, ini)
console.log(2, yield)
console.log(3, yield)
}
const g = generatorFn('foo')
g.next('Jane') // 1, foo
g.next('baz') // 2, baz
g.next('Sue') // 3, Sue
yield关键字可以同时用于输入和输出
function* generatorFn(){
return yield 'foo'
}
const g = generatorFn()
console.log(g.next()) // {value: "foo", done: false}
console.log(g.next('bar')) // {value: "bar", done: true}
// 因为函数必须对整个表达式求值才能确定要返回的值,所以它遇到yield关键字时暂停执行并计算出要产生的值:’foo‘
// 下一次调用next()传入了'bar',作为交给同一个yield的值。然后这个值被确定为本次生成器函数要返回的值。
产生可迭代对象
可以使用星号增强yield的行为,让它能够迭代一个可迭代对象,从而一次产出一个值。
function* generatorFn(){
yield* [1, 2, 3]
}
const g = generatorFn()
for(const x of g){
console.log(x)
}
// 1
// 2
// 3
// 因为yield*实际上知识将一个可迭代对象序列化为一连串可以单独产出的值,所以这根把yield放到一个循环里没有什么不同。
使用yield*实现递归算法
yield*最有用的地方是实现递归操作,此时生成器可以产生自身
function* nTimes(n){
if(n > 0){
yield* nTimes(n-1)
yield n-1
}
}
for(const x of nTimes(3)){
console.log(x)
}
// 1
// 2
// 3
// 每个生成器首先都会从新创建的生成器对象产出每个值,然后再产出一个整数。结果就是生成器函数会递归地减少计数器值,并实例化另一个生成器对象。
生成器作为默认迭代器
因为生成器对象实现了Iterator接口,而且生成器函数和默认迭代器被调用之后都产生迭代器,所以生成器格外适合作为默认迭代器。
class Foo {
constructor(){
this.value = [1, 2, 3]
}
* [Symbol.iterotor](){
yield* this.value
}
}
const f = new Foo()
for (const x of f){
console.log(x)
}
// 1
// 2
// 3
// for-of 循环调用了默认迭代器并产生了一个生成器对象。这个生成器对象是可迭代的。
提前终止生成器
与迭代器类似,生成器也支持’可关闭‘'的概念。一个实现了Iterator接口的对象一定有next()方法,还有一个可选的return()方法用于提前终止迭代器。生成器对象除了有这两个方法,还要第三个方法:throw()。
return()
return()方法会强制生成器进入关闭状态。提供return()方法的值,就是终止迭代器对象的值。
与迭代器不同,所有生成器对象都有return()方法,只要通过它进入关闭状态,就无法恢复了。后续调用next(),会显示done:true,而且提供任何返回值都不会被存储或传播。
function* generatorFn(){
for(const x of [1, 2, 3]){
yield x
}
}
const g = generatorFn()
console.log(g.next()) // {value: 1, done: false}
console.log(g.return(4)) // {value: 4, done: true}
console.log(g.next('rrr')) // {value: undefined, done: true}
console.log(g.next()) // {value: undefined, done: true}
for-of等内置语言结构会忽略状态为done:true的IteratorObject内部返回值。
function* generatorFn(){
for(const x of [1, 2, 3]){
yield x
}
}
const g = generatorFn()
for(const x of g ){
if(x > 1){
g.return(5)
}
console.log(x)
}
// 1
// 2
console.log(g.next()) // {value: undefined, done: true}
throw()
throw()会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未处理,生成器就会关闭。
function* generatorFn(){
for(const x of [1, 2, 3]){
yield x
}
}
const g = generatorFn()
console.log(g.next()) // {value: 1, done: false}
try{
g.throw('foo')
}catch(e){
console.log(e) // foo
}
console.log(g.next()) // {value: undefined, done: true}
假如生成器函数内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行,错误处理会跳过对应的yield。
function* generatorFn(){
for(const x of [1, 2, 3]){
try{
yield x
} catch(e){}
}
}
const g = generatorFn()
console.log(g.next()) // {value: 1, done: false}
g.throw('foo')
console.log(g.next()) // {value: 3, done: false}
// 错误是在生成器的try/catch块中抛出的,所以在生成器内部捕获,可是,由于yield抛出了那个错误,生成器就不会再产出值2了,下一次迭代在遇到yield关键字时产出了值3.
如果生成器对象还没有开始执行,那么调用throw()抛出的错误不会再函数内部捕获,因为这相当于在函数块外抛出错误。