Skip to content

Iterator 和 for...of 循环

Iterator 概念

四种数据集合:数组(Array)和对象(Object),ES6 又添加了 Map 和 Set。

问题:缺乏统一的接口机制,来处理所有不同的数据结构

解决:遍历器(Iterator),为各种不同的数据结构提供统一的访问机制。只要部署 Iterator 接口,就可以完成遍历操作

遍历器作用

  1. 各种数据结构,提供一个统一的、简便的访问接口
  2. 使数据结构的成员能够按某种次序排列
  3. 一种新的遍历命令 for...of 循环

遍历过程

  1. 创建一个指针对象,指向当前数据结构的起始位置。
  2. 第一次调用指针对象的 next 方法,将指针指向数据结构的第一个成员
  3. 第二次调用 next,指向第二个成员
  4. 不断调用 next,直到结束

遍历器对象本质上,就是一个指针对象

next 方法:返回数据结构当前成员信息。返回值是对象,包含 value 和 done 两个属性

  • value : 当前值
  • done:表示遍历是否结束

模拟 next

js
var it = makeIterator(['a', 'b'])

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

function makeIterator(array) {
  var nextIndex = 0
  return {
    next: function () {
      return nextIndex < array.length ? { value: array[nextIndex++], done: false } : { value: undefined, done: true }
    }
  }
}

对于遍历器对象来说,done: falsevalue: undefined属性都是可以省略

注意:遍历器与它所遍历的那个数据结构,实际上是分开的。遍历器只是添加接口规格

typescript 相关

tsx
// 遍历器接口
interface Iterable {
  [Symbol.iterator](): Iterator
}

// 指针对象
interface Iterator {
  next(value?: any): IterationResult
}

// next 返回值
interface IterationResult {
  value: any
  done: boolean
}

默认 Iterator

Iterator 接口目的:为所有数据结构,提供了一种统一的访问机制

可遍历数据结构:部署了 Iterator 接口的数据结构。

遍历时,会自动去寻找 Iterator 接口

ES6 规定,默认 Iterator 接口部署在数据结构的 Symbol.iterator 属性。即一个数据结构只要具有 Symbol.iterator 属性,就可以认为是“可遍历的”(iterable)。

Symbol.iterator 属性:一个当前数据结构默认遍历器的生成函数。

js
const obj = {
  [Symbol.iterator]: function () {
    return {
      next: function () {
        return {
          value: 1,
          done: true
        }
      }
    }
  }
}

Symbol 作为属性名,需方括号包裹

原生具备 Iterator 接口的数据结构。它们可以直接 通过 for...of 遍历

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象
js
let arr = ['a', 'b', 'c']
let iter = arr[Symbol.iterator]()

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }

对象(Object)没有默认部署 Iterator 接口的原因:对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定

一个对象想要通过 for...of 调用,必须在Symbol.iterator的属性上部署遍历器生成方法(原型链上存在也行)

js
class RangeIterator {
  constructor(start, stop) {
    this.value = start
    this.stop = stop
  }

  [Symbol.iterator]() {
    return this
  }

  next() {
    var value = this.value
    if (value < this.stop) {
      this.value++
      return { done: false, value: value }
    }
    return { done: true, value: undefined }
  }
}

function range(start, stop) {
  return new RangeIterator(start, stop)
}

for (var value of range(0, 3)) {
  console.log(value) // 0, 1, 2
}

例子:遍历 “链表”结构

js
function Obj(value) {
  this.value = value
  this.next = null
}

Obj.prototype[Symbol.iterator] = function () {
  var iterator = { next: next }

  var current = this

  function next() {
    if (current) {
      var value = current.value
      current = current.next
      return { done: false, value: value }
    }
    return { done: true }
  }
  return iterator
}

var one = new Obj(1)
var two = new Obj(2)
var three = new Obj(3)

one.next = two
two.next = three

for (var i of one) {
  console.log(i) // 1, 2, 3
}

类数组对象简便方法

js
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]
// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator]
;[...document.querySelectorAll('div')] // 可以执行了

类数组对象调用 Symbol.iterator

js
let iterable = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
}
for (let item of iterable) {
  console.log(item) // 'a', 'b', 'c'
}

普通对象调用 Symbol.iterator 会 报错

js
let iterable = {
  a: 'a',
  b: 'b',
  c: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
}
for (let item of iterable) {
  console.log(item) // undefined, undefined, undefined
}

Symbol.iterator 方法对应的不是遍历器生成函数(即会返回一个遍历器对象)会报错。

js
var obj = {}

obj[Symbol.iterator] = () => 1
;[...obj] // TypeError: [] is not a function

调用 Iterator

默认调用 iterator 的场合

(1)解构赋值

对数组和 Set 结构进行解构赋值时,会默认调用 Symbol.iterator 方法。

(2)扩展运算符

扩展运算符(...)也会调用默认的 Iterator 接口。这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。

(3)yield*

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

js
let generator = function* () {
  yield 1
  yield* [2, 3, 4]
  yield 5
}

var iterator = generator()

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

(4)其他场合

数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合都调用了遍历器接口

  • for...of
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]])
  • Promise.all()
  • Promise.race()

字符串 Iterator

字符串是一个类似数组的对象,也原生具有 Iterator 接口。

js
var someString = 'hi'
typeof someString[Symbol.iterator]
// "function"

var iterator = someString[Symbol.iterator]()

iterator.next() // { value: "h", done: false }
iterator.next() // { value: "i", done: false }
iterator.next() // { value: undefined, done: true }

覆盖原生的 Symbol.iterator 方法

js
var str = new String('hi')

;[...str] // ["h", "i"]

str[Symbol.iterator] = function () {
  return {
    next: function () {
      if (this._first) {
        this._first = false
        return { value: 'bye', done: false }
      } else {
        return { done: true }
      }
    },
    _first: true
  }
}
;[...str] // ["bye"]
str // "hi"

Iterator 和 Generator

Symbol.iterator()简单实现

js
let myIterable = {
  [Symbol.iterator]: function* () {
    yield 1
    yield 2
    yield 3
  }
}
;[...myIterable] // [1, 2, 3]

// 或者采用下面的简洁写法

let obj = {
  *[Symbol.iterator]() {
    yield 'hello'
    yield 'world'
  }
}

for (let x of obj) {
  console.log(x)
}
// "hello"
// "world"

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

遍历器对象除了具有 next(),还具有 return()和 throw()。

自己实现遍历器对象生成函数,next 必须实现,return 和 throw 为可选

return 用法:一个对象在完成遍历前,需要清理或释放资源,可以部署 return()方法。

return 必须返回一个对象,这是 Generator 语法决定的

下面的两种情况,都会触发 return

  • break
  • 报错
js
// 情况一
for (let line of readLinesSync(fileName)) {
  console.log(line)
  break
}

// 情况二
for (let line of readLinesSync(fileName)) {
  console.log(line)
  throw new Error()
}

for...of

引入了 for...of 循环,作为遍历所有数据结构的统一的方法。

只要部署 Symbol.iterator 属性,就被视为具有 iterator 接口,就可以用 for...of 循环遍历。

for...of 使用范围

  • 数组
  • Set 和 Map 结构
  • 某些类似数组的对象(比如 arguments 对象、DOM NodeList 对象)
  • Generator 对象
  • 字符串

数组

数组原生具备 iterator 接口

  1. for...of 代替 forEach

  2. for...in 只能获取对象键名,不能获取键值。for...of 可以获取键值

js
var arr = ['a', 'b', 'c', 'd']

for (let a in arr) {
  console.log(a) // 0 1 2 3
}

for (let a of arr) {
  console.log(a) // a b c d
}
  1. for...of 只返回具有数字索引的属性
js
let arr = [3, 5, 7]
arr.foo = 'hello'

for (let i in arr) {
  console.log(i) // "0", "1", "2", "foo"
}

for (let i of arr) {
  console.log(i) //  "3", "5", "7"
}

Set 和 Map 结构

Set 和 Map 结构也原生具有 Iterator 接口

  1. 遍历顺序:各个成员被添加顺序

  2. Set 遍历时,返回的是一个值

  3. Map 遍历时,返回的是一个数组,数组的成员分别为键名和键值。

js
var engines = new Set(['Gecko', 'Trident', 'Webkit', 'Webkit'])
for (var e of engines) {
  console.log(e)
}
// Gecko
// Trident
// Webkit

var es6 = new Map()
es6.set('edition', 6)
es6.set('committee', 'TC39')
es6.set('standard', 'ECMA-262')
for (var [name, value] of es6) {
  console.log(name + ': ' + value)
}
// edition: 6
// committee: TC39
// standard: ECMA-262

计算生成 数据结构

有些数据结构在现有数据结构的基础上,计算生成的。比如,ES6 的数组、Set、Map,都部署了以下三个方法

entries() :返回一个遍历器对象,用来遍历[键名, 键值]组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 Iterator 接口,默认就是调用 entries 方法。

keys(): 返回一个遍历器对象,用来遍历所有的键名。

values(): 返回一个遍历器对象,用来遍历所有的键值。

类数组对象

类似数组的对象包括好几类。

js
// 字符串
let str = 'hello'

for (let s of str) {
  console.log(s) // h e l l o
}

// DOM NodeList对象
let paras = document.querySelectorAll('p')

for (let p of paras) {
  p.classList.add('test')
}

// arguments对象
function printArgs() {
  for (let x of arguments) {
    console.log(x)
  }
}
printArgs('a', 'b')
// 'a'
// 'b'

for...of 循环还有一个特点,会正确识别 32 位 UTF-16 字符。

js
for (let x of 'a\uD83D\uDC0A') {
  console.log(x)
}
// 'a'
// '\uD83D\uDC0A'

并不是类数组对象都具有 Iterator 接口,可以使用 Array.from 方法将其转为数组。

js
let arrayLike = { length: 2, 0: 'a', 1: 'b' }

// 报错
for (let x of arrayLike) {
  console.log(x)
}

// 正确
for (let x of Array.from(arrayLike)) {
  console.log(x)
}

对象

普通对象,for...of 结构不能直接使用,会报错。需要部署了 Iterator 接口。

js
let es6 = {
  edition: 6,
  committee: 'TC39',
  standard: 'ECMA-262'
}

for (let e in es6) {
  console.log(e)
}
// edition
// committee
// standard

for (let e of es6) {
  console.log(e)
}
// TypeError: es6[Symbol.iterator] is not a function

一种解决方法,使用 Object.keys 方法将对象的键名生成一个数组,然后遍历这个数组。

js
for (var key of Object.keys(someObject)) {
  console.log(key + ': ' + someObject[key])
}

另一个方法是使用 Generator 函数将对象重新包装一下

js
const obj = { a: 1, b: 2, c: 3 }

function* entries(obj) {
  for (let key of Object.keys(obj)) {
    yield [key, obj[key]]
  }
}

for (let [key, value] of entries(obj)) {
  console.log(key, '->', value)
}
// a -> 1
// b -> 2
// c -> 3

与其他遍历语法比较

  1. 原始写法 for 循环
js
for (var index = 0; index < myArray.length; index++) {
  console.log(myArray[index])
}
  1. forEach 方法
  • 无法中途跳出 forEach 循环,break 或 return 都不行。
js
myArray.forEach(function (value) {
  console.log(value)
})
  1. for...in 循环:遍历数组键名
js
for (var index in myArray) {
  console.log(myArray[index])
}

for...in 缺点

  • 数组的键名是数字,但 for...in 循环是以字符串作为键名“0”、“1”、“2”
  • for...in 循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
  • 某些情况下,for...in 循环会以任意顺序遍历键名。

for...in 循环:为遍历对象而设计的,不适用于遍历数组。

for...of 优点

  • 没有 for...in 缺点
  • 可以与 break、continue 和 return 配合使用
  • 提供了遍历所有数据结构的统一操作接口

相关链接

[-] Iterator 和 for...of 循环