Clloz's Blog

Back

ES6 迭代器 Iterator Blur image

前言#

ES6 中我们可以用 for ... of 对很多对象进行遍历操作,包括数组,MapSet,甚至是类数组对象和字符串都可以。之所以能够进行这样的操作是因为 ES6 引入了迭代器 Iterator 的机制,来为不同的数据结构提供一种统一的访问机制。本文就来讨论一下迭代器的机制。

概念#

ES6 之前,我们遍历数组一般是使用 for 或者 mapforEach 等这样的 APIfor 循环使用太麻烦,我们有时仅仅是需要数组中元素的值,但是我们需要提前知道数组的长度,并且声明一个索引变量,当出现嵌套的循环的时候,代码更复杂。而 mapforEachAPI 则是 Array 对象特有的,使用起来也不够方便,比如无法中途跳出 forEach 循环, break 命令或 return 命令都不能奏效。。

所以 ES6 引入了迭代器和 for ... of 来统一和简化我们对对象的遍历。


要对对象进行遍历,首先要确定对象是否可遍历,以及如何进行遍历。ES6 就有两个协议:可迭代协议迭代器协议。前者用来确定一个对象是可遍历的,后者告诉 for ... of 遍历对象的规则。

要确定一个对象是不是可迭代的,只要看看它有没有实现 Symbol.iterator 方法。Symbol.iterator 是一个内置 Symbol,它指向一个方法(即它是一个方法名),是专门供 for ... of 遍历使用的,当用 for ... of 对一个对象进行遍历的时候,就会首先寻找这个对象的 Symbol.iterator 方法。一个对象可遍历,就表示它自身或者其原型链上的某个对象实现了 Symbol.iterator 方法,这个方法是一个无参数的方法,其返回值为一个符合迭代器协议的对象。。这一些规则就是所谓的 可迭代协议

Symbol.iterator 方法并不是没有要求的,这个方法必须要返回一个对象,这个对象必须实现了一个 next 方法。next() 方法必须返回一个对象,该对象应当有两个属性: donevalue,如果返回了一个非对象值(比如 falseundefined),则会抛出一个 TypeError 异常(iterator.next() returned a non-object value)。这里的 done 是一个布尔值,表示迭代器是否还有下一个值。当迭代器还有下一个值,donefalse,此时可省略;当迭代器已经遍历完毕,则此时 donetruevalue 则表示迭代器的返回值,可以是任意 javascript 值,当 donetrue 时刻省略。这就是 迭代器协议

当一个对象满足上面两个协议的时候,就表示这个对象是可迭代对象,可以用 for ... of 对它进行遍历。当你将一个可迭代对象放入 for ... of 进行遍历的时候,会先调用这个对象的 Symbol.iterator 方法,然后用返回对象中的 next 方法不断对对象进行迭代,直到 donetrue

一个可迭代对象的实现如下:

let obj = {
  num: 0,
  [Symbol.iterator]() {
    let num = this.num //在 Symbol.iterator 中我们可以用 this 访问可迭代对象的属性,传递给 next,最后由 next 方法返回出去
    return {
      next() {
        if (num < 10) {
          return {
            value: num++,
            done: false
          }
        } else {
          return {
            value: undefined,
            done: true
          }
        }
      }
    }
  }
}

for (let c of obj) {
  console.log(c)
}
//输出:0 1 2 3 4 5 6 7 8 9
javascript

我们可以看到只要满足了可迭代协议和迭代协议,任何对象都能够用 for ... of 进行遍历,并且遍历行为是我们自定义的。其实 for ... of 的实现也很简单,就是一个 while 循环:

function forOf(obj, cb) {
  if (!obj[Symbol.iterator]) throw new TypeError(typeof obj + ' is not iterable')
  if (typeof obj[Symbol.iterator] !== 'function')
    throw new TypeError('Result of the Symbol.iterator method is not an object')
  if (typeof cb !== 'function') throw new TypeError('cb must be callable')

  let iterator = obj[Symbol.iterator]()
  let result = iterator.next()
  while (!result.done) {
    cb(result.value)
    result = iterator.next()
  }
}

forOf(obj, function (val) {
  console.log(val)
})
javascript

但是这样将 next 写在 Symbol.iterator 方法的返回对象中我们没法直接用 this 访问 obj 的属性所以我们可以改成如下形式:

let obj = {
  num: 0,
  [Symbol.iterator]() {
    return this
  },
  next() {
    if (this.num < 10) {
      return {
        value: this.num++,
        done: false
      }
    } else {
      return {
        value: undefined,
        done: true
      }
    }
  }
}

for (let c of obj) {
  console.log(c)
}
javascript

从这个修改我们可以看出来,可迭代对象也可以是一个迭代器,这在后面还会应用到,生成器对象 Generator 其实是这种实现。

其实不止 for ... of,还有很多其他操作也是调用的迭代器进行遍历,包括对数组和 Set 的解构赋值,扩展运算符,yield*,以及任何接受数组作为参数的场景。而且很多内置的对象默认就是可迭代的,目前所有的内置可迭代对象如下:StringArrayTypedArrayMapSet,它们的原型对象都实现了 @@iterator 方法。

console.log(typeof 'clloz'[Symbol.iterator]) //function

let iterator = 'clloz'[Symbol.iterator]()

console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())

// { value: 'c', done: false }
// { value: 'l', done: false }
// { value: 'l', done: false }
// { value: 'o', done: false }
// { value: 'z', done: false }
// { value: undefined, done: true }
javascript

很多 API 可以接受一个可迭代对象作为参数:

  • new Map([iterable])
  • new WeakMap([iterable])
  • new Set([iterable])
  • new WeakSet([iterable])
  • Promise.all(iterable)
  • Promise.race(iterable)
  • Array.from(iterable)

内建迭代器#

for ... of 调用的迭代器只能返回键值,但有时候我们希望能够返回键名,或者两者都需求。针对这种情况,ES6 为数组,MapSet (其实 Object 也有这三个方法,不过不是返回迭代器)提供了三种不同的方法返回不同的迭代器:

  • entries() 返回一个遍历器对象,用来遍历 [键名, 键值] 组成的数组。对于数组,键名就是索引值。
  • keys() 返回一个遍历器对象,用来遍历所有的键名。
  • values() 返回一个遍历器对象,用来遍历所有的键值。

对于数组,for ... of 依次返回数组的元素值,keys() 返回每个元素的索引,values() 返回每个元素的值,entries() 返回每个元素所以和值组成的数组。

对于 Mapfor ... of 按插入顺序返回每个键值对组成的数组,keys() 按插入顺序返回每个键名,values() 按插入顺序返回每个键值,entries()for ... of 相同。

let m = new Map()
m.set(1, 'a')
m.set(2, 'b')
m.set(3, 'c')
m.set(4, 'd')

for (let c of m) {
  console.log(c)
}
// [ 1, 'a' ]
// [ 2, 'b' ]
// [ 3, 'c' ]
// [ 4, 'd' ]
for (let c of m.keys()) {
  console.log(c)
}
//1 2 3 4
for (let c of m.values()) {
  console.log(c)
}
//a b c d
for (let c of m.entries()) {
  console.log(c)
}
// [ 1, 'a' ]
// [ 2, 'b' ]
// [ 3, 'c' ]
// [ 4, 'd' ]
javascript

对于 Setfor ... ofkeys()values() 返回相同,说明 Set 结构的键名和键值是相同的, entries() 则和 Map 一样返回键名和键值组成的数组,只不过键名和键值相同。

let s = new Set()
s.add('a')
s.add('b')
s.add('c')

for (let c of s) {
  console.log(c)
}
// a b c
for (let c of s.keys()) {
  console.log(c)
}
// a b c
for (let c of s.values()) {
  console.log(c)
}
//a b c
for (let c of s.entries()) {
  console.log(c)
}
// [ 'a', 'a' ]
// [ 'b', 'b' ]
// [ 'c', 'c' ]
javascript

其实我们可以看出,对于 Set 来说,默认调用的是 values() 来获得迭代器,而 Map 默认是调用 entries() 来获得默认迭代器。

这里一定要注意数组,MapSetkeysvaluesentries 方法返回的是一个迭代器,而 Object 的对应方法返回的是一个数组。我们可以利用这些方法返回的迭代器来设置对象的 Symbol.iterator 方法让对象变为可迭代对象。

let obj = {
  nicknames: ['Jack', 'Jake', 'J-Dog'],
  [Symbol.iterator]() {
    console.log(Array.isArray(this.nicknames.entries()))
    return this.nicknames.entries()
  }
}

for (let c of obj) {
  console.log(c)
}
javascript

应用#

迭代器的知识其实并不多也不复杂,上面的两个协议掌握即可,下面我来说一说一些应用。

实现链表#

function List(value) {
  this.value = value
  this.next = null
}

List.prototype[Symbol.iterator] = function () {
  let current = this
  return {
    next() {
      if (current.next) {
        let value = current.value
        current = current.next
        return {
          value: value,
          done: false
        }
      } else {
        return {
          value: current.value,
          done: true
        }
      }
    }
  }
}
let a = new List('a')
let b = new List('b')
let c = new List('c')

a.next = b
b.next = c

for (let val of a) {
  console.log(val)
}
javascript
ES6 迭代器 Iterator
https://clloz.com/blog/es6-iterator
Author Clloz
Published at August 18, 2020
Comment seems to stuck. Try to refresh?✨