迭代

ECMAScript 6 规范新增了两个高级特性:迭代器和生成器。

循环是迭代的基础,但通过循环执行例程并不理想。

原因有两点:

  • 迭代之前需要预先知道如何使用数据结构:数组中没意向都只能先通过引用取得数组对象,在取得特定索引位置上的项,但不适用于所有数据结构
  • 遍历顺序并不是数据结构固有的:通过递增索引来访问数据是特定于数组类型的方式,并不适用于其他具有隐式顺序的数据结构

Array.prototype.forEach()

迭代器模式

迭代器模式描述了一个方案,把有些结构称为 "可迭代对象",因为它们实现了正式的 Iterable 接口,而且可以通过迭代器 Iterator 消费。

可迭代对象不一定是数组或集合对象,也可以是类数组。

可迭代协议

实现 Iterator 接口要求同时具备两种能力:

  • 支持迭代的自我识别能力
  • 创建实现 Iterator 接口的对象的能力

必须使用特殊的 Symbol.iterator 作为键,作为 "默认迭代器",必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。

很多内置类型都实现了 Iterator 接口:

  • 字符串
  • 数组
  • 映射
  • 集合
  • arguments 对象
  • NodeList 等 DOM 集合类型

实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性。接收可迭代对象的原生语言特性包括:

  • for-of 循环
  • 数组结构
  • 扩展操作符
  • Array.from()
  • 创建集合
  • 创建映射
  • Promise.all() 接收由期约组成的可迭代对象
  • Promise.race() 接收由期约组成的可迭代对象
  • yield* 操作符,在生成器中使用

如果对象原型链上的父类实现了 Iterable 接口,那这个对象也就实现了这个接口。

迭代器协议

迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。 迭代器 API 使用 next() 方法在可迭代对象中遍历数据,返回一个 IteratorResult 对象。

IteratorResult 迭代器对象包含两个属性:done 和 value。

迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象。

自定义迭代器

任何实现 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)

for (let i of counter) {
  console.log(i)
}

// 1
// 2
// 3

为了让一个可迭代对象能够创建多个迭代器,必须每创建一个迭代器就对应一个新计数器。为此,可以把计数器变量放到闭包里,必须通过闭包返回迭代器

class Counter {
  constructor(limit) {
    this.limit = limit
  }
  [Symbol.iterator]() {
    let count = 1,
      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

提前终止迭代器

可选的 return() 方法用于指定在迭代器提前关闭时执行的逻辑。

  • for-of 循环通过 break、continue、return 或 throw 提前退出
  • 解构操作并未消费所有值。

return() 方法必须返回一个有效的 IteratorResult 对象。

class Counter {
  constructor(limit) {
    this.limit = limit
  }

  [Symbol.iterator]() {
    let count = 1,
      limit = this.limit
    return {
      next() {
        if (count <= limit) {
          return {done: false, value: count++}
        } else {
          return {done: true}
        }
      },
      return() {
        console.log('Exiting early')
        return {done: true}
      }
    }
  }
}
let counter1 = new Counter(5)
for (let i of counter1) {
  if (i > 2) {
    break
  }
  console.log(i)
}

// 1
// 2
// Exiting early

let counter2 = new Counter(5)

try {
  for (let i of counter2) {
    if (i > 2) {
      throw 'err'
    }
    console.log(i)
  }
} catch (e) {}

// 1
// 2
// Exiting early

let counter3 = new Counter(5)
let [a, b] = counter3

// Exiting early

如果迭代器没有关闭,那么还可以继续从上次离开的地方继续迭代。

并非所有的迭代器都是可关闭的。数组的迭代器就是不可关闭的。

生成器

生成器 是 ECMAScript 6 新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。

使用生成器可以自定义迭代器和实现协程。

生成器基础

生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。

// 生成器函数声明
function* generatorFn() {}
// 生成器函数表达式
let generatorFn = function* () {}
// 作为对象字面量方法的生成器函数
let foo = {
  * generatorFn() {}
}
// 作为类实例方法的生成器函数
class Foo {
  * generatorFn() {}
}
// 作为类静态方法的生成器函数
class Bar {
  static * generatorFn() {}
}

箭头函数不能用来定义生成器函数.

调用生成器函数会产生一个生成器对象。生成器对象一开始处于暂停执行(suspended)状态。也具有 next() 方法。调用这个方法可以让生产器开始或恢复执行。

生成器对象实现了 Iterable 接口,它们默认的迭代器是自引用的。

通过 yield 中断执行

yield 关键字可以让生成器停止和开始执行,也是生成器最有用的地方。

生成器函数在遇到 yield 关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过生成器对象上调用 next() 方法来恢复执行。

  1. 生成器对象作为可迭代对象
  2. 使用 yield 实现输入和输出
  3. 产生可迭代对象
  4. 使用 yield* 实现递归算法

生成器作为默认迭代器

class Foo {
  constructor() {
    this.values = [1,2,3]
  }

  *[Symbol.iterator]() {
    yield* this.values
  }
}

const f = new Foo()
for (const x of f) {
  console.log(x)
}
// 1
// 2
// 3

for-of 循环调用了默认迭代器,恰好是一个生成器函数,产生了一个生成器对象。

提前终止生成器

  1. return()
  2. throw()