持续对红宝书进行输出(四)

97 阅读10分钟

1. 迭代器和生成器

  1. 内容纪要
    1. 迭代
    2. 迭代器模式
    3. 生成器

1.1 理解迭代

  1. 在JavaScript中,计数循环就是一种最简单的迭代:
for (let i = 0; i < 10; i++) {
  console.log(i);  
}
1. 循环时迭代机制的基础,这是因为它可以指定迭代的次数,以及每次迭代要执行什么操作。每次循环都会在下一次迭代开始之前完成,而每次迭代的顺序都是事先定义好的。
2. 迭代会在一个有序集合上进行。
const collection = ['foo','bar','baz']
for (let index = 0; index < collection.length; index++) {
  console.log(collection[index]);  
}
3. 因为数组有已知的长度,且数组每一项都可以通过索引获取,所以整个数组可以通过递增索引来遍历。
4. 问题:`迭代之前需要事先知道如何使用数据结构。`数组中的每一项都只能先通过引用取得数组对象,然后再通过[]操作符取得特定索引位置上的项。这种情况不适用于所有数据结构。
5. 遍历顺序并不是数据结构固有的。通过递增索引来访问数据是特定于数组类型的方式,并不适用于其他具有隐式顺序的数据结构。
6. ES5新增了Array.prototype.forEach()方法,向通用迭代需求买进了一步(但是仍然不够理想)
const collection = ['foo','bar','baz']

collection.forEach((item)=>{
  console.log(item);
})
7. 这个方法解决了单独记录索引和通过数组对象取得值的问题,不过,没有办法标识迭代何时终止。因此这个方法只适用于数组,而且回调结构也比较笨拙。

1.2 迭代器模式

  1. 迭代器模式描述了一个方案,即可以把有些结构称为"可迭代对象(iterable)",因为它们实现了正式的Iterable接口,而且可以通过迭代器Iterator消费。
  2. 可迭代对象是一种抽象的语法。基本上,可以把可迭代对象理解成数组或集合这样的集合类型的对象。它们包含的元素都是有限的,而且都具有无歧义的遍历顺序。
  3. 任何实现Iterable接口的数据结构都可以被实现Iterator接口的结构消费。迭代器是按需创建的一次性对象。每个迭代器都关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的API。
  4. 迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值。

1.2.1 可迭代协议

  1. 实现Iterable接口要求同时具备两种能力:支持迭代的自我辨识能力和创建实现Iterator接口的对象的能力。这意味着必须暴露一个属性作为"默认迭代器",而且这个属性必须使用特殊的Symbol.iterator作为键。这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。
    1. 很多内置类型都实现了Iterable接口
    2. 字符串
    3. 数组
    4. 映射
    5. 集合
    6. arguments对象
    7. NodeList的DOM集合
  2. 检查是否存在默认迭代器属性可以暴露这个工厂函数
let num = 1
let obj = {}

// 这两种类型没有实现迭代器工厂函数
console.log(num[Symbol.iterator]); // undefined
console.log(obj[Symbol.iterator]); // undefined

let str = 'abc'
let arr = ['1','2','3']
let m = new Map().set("val1","key1").set("val2","key2")
let s = new Set().add(1).add(2).add(3)

// 这些类型都实现了迭代器工厂函数
console.log(str[Symbol.iterator]); // [Function: [Symbol.iterator]]
console.log(arr[Symbol.iterator]); // [Function: values]
console.log(m[Symbol.iterator]); // [Function: entries]
console.log(s[Symbol.iterator]); // [Function: values]

// 调用工厂函数会生成一个迭代器
console.log(str[Symbol.iterator]()); // Object [String Iterator] {}
console.log(arr[Symbol.iterator]()); // Object [Array Iterator] {}
console.log(m[Symbol.iterator]()); // [Map Entries] { [ 'val1', 'key1' ], [ 'val2', 'key2' ] }
console.log(s[Symbol.iterator]()); // [Set Iterator] { 1, 2, 3 }

  1. 在实际的开发中,不需要显式调用这个工厂函数来生成迭代器。实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性。
    1. for-of循环
    2. 数组解构
    3. 扩展操作符
    4. Array.from()
    5. 创建集合
    6. 创建映射
    7. Promise.All()接收期约组成的可迭代对象
    8. Promise.race()接收由期约组成的可迭代对象
    9. yield* 操作符
// 如果对象原型链上的父类实现了Iterable接口,那么这个对象也就实现了这个接口
class fooArray extends Array {}
let arr = new fooArray('foo','zoo')
for (const iterator of arr) {
  console.log(iterator);
}

1.2.2 迭代器协议

  1. 迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器API使用next()方法在可迭代对象中遍历数据。每次成功调用next(),都会返回一个IteratorResult对象,其中包含迭代器返回的下一个值。若不调用next(),则无法知道迭代器的当前位置。
  2. next()方法返回的迭代器对象IteratorResult包含两个属性:done和value。done是一个布尔值,表示是否还可以再次调用next()取得下一个值;value包含可迭代对象的下一个值。done为true状态时成为耗尽。
// 可迭代对象
let arr = ['foo','zoo']
// 迭代器工厂函数
console.log(arr[Symbol.iterator]); // [Function: values]
// 迭代器
let iter = arr[Symbol.iterator]()
console.log(iter); // Object [Array Iterator] {}

// 执行迭代
/**
 *  创建迭代器并调用next()方法按顺序迭代数组,直至不在产生新值。迭代器并不知道怎么从
 *  可迭代对象中取得下一个值,也不知道可迭代对象有多大。只要迭代器到达done:true状态
 *  后续调用next()就返回同样的值。
 */
console.log(iter.next()); // { value: 'foo', done: false }
console.log(iter.next()); // { value: 'zoo', done: false }
console.log(iter.next()); // { value: undefined, done: true }

/**
 *  每个迭代器都表示对可迭代对象的一次性有序遍历,不同迭代器的实例相互之间没有联系,
 *  指挥独立地遍历可迭代对象。
 * 
 */
let arr1 = ['a','b']
let iter1 = arr1[Symbol.iterator]()
let iter2 = arr1[Symbol.iterator]()

console.log(iter1.next()); // { value: 'a', done: false }
console.log(iter2.next()); // { value: 'a', done: false }
console.log(iter1.next()); // { value: 'b', done: false }
console.log(iter2.next()); // { value: 'b', done: false }

/**
 *  迭代器并不与可迭代对象某个时刻的快照绑定,而仅仅是使用游标来记录遍历可迭代对象的历程,
 *  如果迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化
 * 
 */
let arr2 = ['foo','bar']

let iter3 = arr2[Symbol.iterator]()
console.log(iter3.next()); // { value: 'foo', done: false }

arr2.splice(1,0,'bzz')
console.log(iter3.next()); // { value: 'bzz', done: false }
console.log(iter3.next()); // { value: 'bar', done: false }
console.log(iter3.next()); // { value: undefined, done: true }

1.2.3 自定义迭代器

  1. 与Iterable接口类似,任何实现Iterator接口的对象都可以作为迭代器使用。

class Counter {
  constructor(limit){
    this.count = 1
    this.limit = limit
  }
  next(){
    if(this.count <= this.limit){
      return { done: false, value: this.count++ }
    }else{
      return { done: true, value: undefined }
    }
  }
  [Symbol.iterator](){
    return this
  }
}
// 创建实例对象
let counter = new Counter(3)

// 这个类实现了Iterator接口,但不理想,因为它的每个实例只能被迭代一次
for (let iterator of counter) {
  console.log(iterator);
}
for (let i  of counter) {
  console.log(i);
  console.log(111); // 不会输出
}
  1. 为了让一个可迭代对象能够创建多个迭代器,必须每创建一个迭代器就对应一个新计数器,为此,可以把计数器变量放在闭包里,然后通过闭包返回迭代器。

class Counter {
  constructor(limit){
    this.limit = limit
  }
  [Symbol.iterator](){
    let count = 1
    let limit = this.limit
    return {
      next(){
        if(count <= limit){
          return { done: false, value: count++ }
        }else {
          return { done: true, value: undefined }
        }
      }
    }
  }
}

let counter = new Counter(3)

for (let i of counter) {
  console.log(i);
}
// 1
// 2
// 3
for (let i of counter) {
  console.log(i);
}
// 1
// 2
// 3

1.2.4 提前终止迭代器

  1. 可选的return()方法用于指定在迭代器提前关闭时执行的逻辑,可能的出现提前关闭的情况:
    1. for of 循环通过break contiune return 或 throw提前退出
    2. 解构操作并未消费所有值
  2. return()方法必须返回一个有效的IteratorResult对象,简单情况下,可以只返回{ done: true, value: undefined }
class Counter {
  constructor(limit) {
    this.limit = limit;
  }
  [Symbol.iterator]() {
    let count = 1;
    let limit = this.limit;
    return {
      next() {
        if (count <= limit) {
          return { done: false, value: count++ };
        } else {
          return { done: true, value: undefined };
        }
      },
      return() {
        console.log("Exiting early...");
        return { done: true, value: undefined };
      },
    };
  }
}

let counter = new Counter(3);

for (const i of counter) {
  if (i > 2) {
    break;
  }
  console.log(i);
}
// 1
// 2
// Exiting early...

let counter2 = new Counter(5);
try {
  for (const i of counter2) {
    if (i > 2) {
      throw "err";
    }
    console.log(i);
  }
} catch (error) {}
// 1
// 2
// Exiting early...

// 如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代,比如:数组的迭代器就是不能关闭的
let a = [1, 2, 3, 4, 5];
let iter = a[Symbol.iterator]();
for (const i of iter) {
  console.log(i);
  if (i > 3) {
    break;
  }
}
// 1
// 2
// 3
for (const i of iter) {
  console.log(i);
}
// 4
// 5

1.3 生成器

  1. 生成器是ECMAScript6新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。
  2. 生成器的形式是一个函数,函数名称前面加一个星号( * )表示它是一个生成器。只要是可以定义函数的地方,就可以定义生成器。
  3. (箭头函数不能用来定义生成器函数),标识生成器函数的星号不受两侧空格的影响。
// 生成器函数声明
function* generatorFn(){}
// 生成器函数表达式声明
let generatorFn2 = function* (){}
// 作为对象字面量方法的生成器函数
const foo = {
   *generatorFn(){}
}
// 作为类实例方法的生成器函数
class Foo {
  *generatorFn(){}
}
// 作为静态方法的生成器函数
class Animal {
  static * generatorFn(){}
}
  1. 调用生成器函数会产生一个生成器对象。生成器对象一开始处于暂停执行(suspended)的状态。与迭代器相似,生成器对象也实现了Iterator接口,因此具有next()方法。调用这个方法会让生成器开始或恢复执行。
  2. next()方法的返回值类似于迭代器,有done和value属性,函数体为空的生成器函数中间不会停留,调用一次next()就会让生成器到达done:true状态。
  3. value属性是生成器函数的返回值,默认值为undefined,可以通过生成器函数的返回值指定:
  4. 生成器函数只会在初次调用next()方法后开始执行。
// 生成器函数声明
function* generatorFn(){}

// 调用生成器函数会产生一个生成器对象
const fn = generatorFn()
console.log(fn); // Object [Generator] {}
// next()方法的返回值类似于迭代器,有done和value属性,函数体为空的生成器函数中间不会停留,调用一次next()就会让生成器到达done:true状态
console.log(fn.next()); // { value: undefined, done: true }

// 生成器函数表达式声明
let generatorFn2 = function* (){
  return 'foo'
}
let fn2 = generatorFn2()
console.log(fn2.next()); // { value: 'foo', done: true }

// 生成器函数只会在初次调用next()方法后开始执行,
let generatorFn3 = function *() {
  console.log('foobar');
}
let fn3 = generatorFn3()
console.log(fn3); // 初次调用不会打印日志
fn3.next() // foobar
  1. 生成器对象实现了Iterable接口,它们默认的迭代器是自引用的。
function *generatorFn(){}
console.log(generatorFn); // [GeneratorFunction: generatorFn]
console.log(generatorFn()[Symbol.iterator]); // [Function: [Symbol.iterator]]

console.log(generatorFn()[Symbol.iterator]()); // Object [Generator] {}

const g = generatorFn()

console.log(g === g[Symbol.iterator]()); // true

1.3.1 通过yield中断执行

  1. yield关键字可以让生成器停止和开始执行,生成器函数在遇到yield关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用next()方法来恢复执行。
  2. yield关键字像函数中间返回语句,它生成的值会出现在next()方法返回的对象里。通过yield关键字退出的生成器函数会处于done:false的状态;通过return关键字退出的生成器函数会处于done:true状态。
  3. 生成器函数内部的执行流程会针对每个生成器对象区分作用域,在一个生成器对象上调用next()不会影响其他生成器。
  4. yield关键字只能在生成器函数内部使用,用在其他地方会抛出错误。
function *generatorFn(){
  yield 'foo'
  yield 'bar'
  return 'baz'
}

let g = generatorFn()
console.log(g.next()); // { value: 'foo', done: false }
console.log(g.next()); // { value: 'bar', done: false }
console.log(g.next()); // { value: 'baz', done: true }

// 生成器函数内部的执行流程会针对每个生成器对象区分作用域,在一个生成器对象上调用next()不会影响其他生成器。
let g1 = generatorFn()
console.log(g1.next()); // { value: 'foo', done: false }

  1. 生成器对象作为可迭代对象
// 如果把生成器对象当成可迭代对象,使用遍历会更加方便
function *generatorFn(){
  yield 'foo'
  yield 'bar'
  yield 'baz'
}
for (const x of generatorFn()) {
  console.log(x);
}
// foo
// bar
// baz
  1. 使用yield实现输入和输出,除了可以作为函数的中间返回语句使用,yield关键字还可以作为函数的中间参数使用。上一次让生成器函数暂停的yield关键字会接收到传给next()方法的第一个值。
function *generatorFn(initial){
  console.log(initial);
  console.log(yield);
  console.log(yield);
}
let g = generatorFn('foo')
g.next() // foo
g.next('bar') // bar
g.next('baz') // baz

// yield关键字可以同时用于输入和输出
function *generatorFn2(){
  return yield 'foo'
}
let g2 = generatorFn2()
console.log(g2.next()); // { value: 'foo', done: false }
console.log(g2.next('bar')); // { value: 'bar', done: true }
  1. 产生可迭代对象, 可以使用星号增强yield的行为,让它能够迭代一个可迭代对象,从而一次产出一个值。
function *generatorFn(){
  yield* [1,2,3]
}
let g = generatorFn()
for (const x of g) {
  console.log(x);
}
// 1
// 2
// 3

1.3.2 提前终止生成器

  1. 与迭代器类似,生成器也支持"可关闭",一个实现Iterator接口的对象一定有next()方法,还有一个可选的return()方法用于提前终止迭代器。生成器对象除了有这两个方法,还有第三个方法:throw()
  2. return()和throw()方法都可以用于强制生成器进入关闭状态。
// return() 方法会强制生成器进入关闭状态。提供给return()方法的值,就是终止迭代器对象的值
function *generator(){
  for (const x of [1,2,3]) {
    yield * x
  }
}
let g = generator()
console.log(g); // Object [Generator] {}
// 与迭代器不同,所有生成器对象都有return方法,只要通过它进入关闭状态,就无法恢复了,后续调用next()会显示done:true状态
// 而提供的任何返回值都不会被存储或传播。
console.log(g.return(4)); // { value: 4, done: true }
console.log(g); // Object [Generator] {}
console.log(g.next()); // { value: undefined, done: true }



// throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭。
function *generatorFn(){
  for (const x of [1,2,3]) {
    yield  x
  }
}
let g2 = generatorFn()
console.log(g2); // Object [Generator] {}
try {
  g2.throw('foo')
} catch (error) {
  console.log(error); // foo
}
console.log(g2.next()); // { value: undefined, done: true }

// 不过,假如生成器函数内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行。错误处理会跳过对象的yield

function *generatorFn2(){
  for (const x of [1,2,3]) {
    try {
      yield x
    } catch (error) {
      
    }
  }
}
let g3 = generatorFn2()
console.log(g3.next()); // { value: 1, done: false }
g3.throw()
console.log(g3.next()); // { value: 3, done: false }
  • 任何实现Iterable接口的对象都有一个Symbol.Iterator属性,这个属性引用默认的迭代器。默认迭代器就像一个迭代器工厂,也就是一个函数,调用之后会产生一个Iterator接口对象。
  • 生成器是一种特殊的函数,调用之后会返回一个生成器对象。生成器对象实现了Iterable接口,因此可用在任何消费可迭代对象的地方。