learn-typescript

Iterator

Iterator 概念

Iterator 是一种接口,为各种不同的数据结构提供统一访问的机制(使用 for...of 循环)。任何数据结构只要部署了 Iterator 接口,就可以完成遍历操作。

Iterator 的作用有三个:

Iterator 遍历的过程是这样的:

  1. 创建一个指针对象,指向当前数据结构的起始位置;
  2. 调用指针对象的 next() 方法,将指针指向数据结构的第一个成员,该方法返回一个结构为 {value: any, done: boolean } 的对象,表示当前数据成员的信息;
  3. 不断调用指针对象的 next() 方法,直至它指向数据结构的结束位置。

使用函数模拟 Iterator 机制如下:

function generateIterator(arr: any[]) {
  let index = 0
  return {
    next() {
      if (index < arr.length) {
        const value = arr[index]
        index++
        return {
          value,
          done: false
        }
      } else {
        return { value: undefined, done: true }
      }
    }
  }
}

const gi = generateIterator([1, 3, 2])
console.log(gi.next()) // { value: 1, done: false }
console.log(gi.next()) // { value: 3, done: false }
console.log(gi.next()) // { value: 2, done: false }
console.log(gi.next()) // { value: undefined, done: true }
console.log(gi.next()) // { value: undefined, done: true }

默认的 Iterator 接口

ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,具有 Symbol.iterator 属性就是部署了 Iterator 接口,就可遍历。

Symbol.iterator 属性本身是一个函数,就是当前数据结构的遍历器生成函数 ,返回一个遍历器对象 Iterator ,该对象的根本特征是具有 next() 方法,每次调用 next() 方法都会返回一个代表当前成员的信息对象,用 TypeScript 来描述 遍历器接口(Iterable)遍历器对象(Iterator)next方法返回值(IterationResult) 的结构如下:

interface Iterable {
  [Symbol.iterator](): Iterator
}

interface Iterator {
  next(value?: any): IterationResult
}

interface IterationResult {
  value: any
  done: boolean
}

ES6 的有些数据结构默认可被 for...of 遍历(如数组),原因在于这些数据结构原生部署了 Symbol.iterator 属性:

let arr = [1, 2, 3]
let it = arr[Symbol.iterator]()

it.next() // { value: 1, done: false }
it.next() // { value: 2, done: false }
it.next() // { value: 3, done: false }
it.next() // { value: undefined, done: true }

其他数据结构如果想要能够被遍历,需要手动部署 Symbol.iterator 属性:

class IterableDemo {
  constructor(public data: string[]) {}

  [Symbol.iterator]() {
    let index = 0
    return {
      next: () => {
        if (index < this.data.length) {
          return {
            value: this.data[index++],
            done: false
          }
        } else {
          return {
            value: undefined,
            done: true
          }
        }
      }
    }
  }
}

let iterableDemo = new IterableDemo(['a', 'b', 'c'])
for (const item of iterableDemo) {
  console.log(item)
}
// a b c

使用 Generator 函数实现 Iterator 接口

Symbol.iterator 方法最简单的实现办法是使用 Generator 函数。

上面例子中的 [Symbol.iterator] 方法写起来十分繁琐,可以改写成 Generator函数

  *[Symbol.iterator]() {
    let index = 0
    while (index < this.data.length) {
      yield this.data[index++]
    }
  }

可以看到,几乎不用部署任何代码,只需要使用 yield 命令给出每一步的返回值即可。

类数组部署 Iterator 接口的简便方法

对于类似数组的对象(存在数值键名和 length 属性),部署 Iterator 接口有一个简便方法 —— 使用数组的 Symbol.iterator 属性:

let obj = {
  0: 'A',
  1: 'B',
  length: 2
}
;(obj as any)[Symbol.iterator] = Array.prototype[Symbol.iterator]

for (const item of obj as any) {
  console.log(item)
}
// A B

字符串也是类数组的对象,也原生具有 Iterator 接口:

let str = 'ABC'
let itStr = str[Symbol.iterator]()

console.log(itStr.next()) // { value: 'A', done: false }
console.log(itStr.next()) // { value: 'B', done: false }
console.log(itStr.next()) // { value: 'C', done: false }
console.log(itStr.next()) // { value: undefined, done: true }

调用 Iterator 接口的场合

  1. 解构赋值 可遍历对象进行解构赋值时,会默认调用 Iterator 接口:

    let [a, , b] = [1, 2, 3]
    console.log(a, b) // 1 3
    
    let [s, ...rest] = 'abcd'
    console.log(s, rest) // a [ 'b', 'c', 'd' ]
    
  2. 拓展运算符

    let arrx = [1, 2]
    console.log([1, 2, ...arrx]) // [ 1, 2, 1, 2 ]
    
  3. yield*

    yield* 后面跟的是一个可遍历的结构,会调用其遍历器接口:

    function* foo() {
      yield 'x'
      yield* [1, 2]
      yield 'y'
    }
    console.log([...foo()]) // [ 'x', 1, 2, 'y' ]
    
  4. 其他场合

遍历器对象的 return()和 throw()

遍历器对象除了具有 next() 方法,还可以具有 return()throw() 方法。

  1. return()方法 如果 for...of遍历过程中通过 break 或者由于错误而中途退出,就会调用 return() 方法,可以该方法来进行资源清理工作。

    const iteratorReturnArr = [1, 3, 2, 3, 4, 5]
    const itr = iteratorReturnArr[Symbol.iterator]()
    iteratorReturnArr[Symbol.iterator] = () => {
      itr.return = function() {
        console.log('release memory')
        return {
          value: undefined,
          done: true
        }
      }
      return itr
    }
    for (const item of iteratorReturnArr) {
      if (item % 2 === 0) {
        break
      }
      console.log(item)
    }
    // 1
    // 3
    // release memory
    
    for (const item of iteratorReturnArr) {
      if (item % 2 === 0) {
        throw new Error('even number')
      }
      console.log(item)
    }
    // 1
    // 3
    // release memory
    // Error info...
    
  2. throw()方法
    throw() 方法主要配合 Generator函数 使用,一般的遍历器对象用不到该方法。

for…of 循环

ES6 引入了 for...of 作为遍历所有数据结构的统一方法, for...of 内部调用的是数据结构的 Symbol.iterator 方法。

与其他遍历方法的比较

  1. forEach
    forEach() 方法无法中途跳出循环,break 命令或者 return 命令都无效;
    for...of 可以中途跳出循环。

  2. for…in
    for...in 为遍历对象属性而设计,会遍历对象及其原型链上的所有键名,不适合遍历数组(如果遍历数组将返回数组的数字索引);
    for...of 适用于遍历具有 Iterator 接口的数据结构。

总结:
for...of 用于遍历数组等可遍历对象,并且可以与 break , continue, return 配合使用来灵活控制遍历过程。