迭代器: javascript高手必会

232 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第12天,点击查看活动详情

for 循环不是迭代器模式

简单的 for 循环并不是迭代器模式,因为 for 循环需要知道对象的内部结构,数据内部很多信息需要被公开。如: 要知道数组的长度;要知道通过 arr[i] 形式来得到 item。

const arr = [10, 20, 30]
const length = arr.length
for (let i = 0; i < length; i++) {
    console.log(arr[i])
}

上面这个数组还好,如果你获取的数据第三方服务,当这个服务提供的数据的结构和长度改变了,那么你就需要修改你的代码,这显示是不对的,不满足低耦合高内聚的,不符合设计原则

迭代器要解决的问题就是解决数据源里面的数据不确定的问题。所以,简单的for循环不是迭代器,迭代器就是为了解决for循环的问题。

简易迭代器

有些对象,并不知道他的内部结构:

  • 不知道长度
  • 不知道如何获取 item 那么, forEach 就是一个非常简单的迭代器,因为它不需要知道数据源的长度,也不需要知道如何获取数据。这就非常符合高内聚低耦合的特性。
const pList = document.querySelectorAll('p')
pList.forEach(p => console.log(p))

迭代器使用的特点

  • 顺序访问有序结构,比如数组,NodeList
  • 不知道数据的长度和结构(数据本身就要是一个黑盒,高内聚低耦合,满足软件开发的原则)

代码实现

class DataIterator {
  private data: number[]
  private index = 0
  constructor(container: DataContainer) {
    this.data = container.data
  }
  next(): number | null {
    if (this.hasNext()) {
      return this.data[this.index++]
    }
    return null
  }
  hasNext(): boolean {
    if (this.index >= this.data.length) {
      return false
    }
    return true
  }
}

class DataContainer {
  data: number[] = [10, 20, 30, 40]
  getIterator(): DataIterator {
    return new DataIterator(this)
  }
}

const container = new DataContainer()
// 获取迭代器
const iterator = container.getIterator()
while (iterator.hasNext()) {
  const num = iterator.next()
  console.log(num)
}

上面的功能就是为了遍历 data 数组,为什么要搞这么复杂呢?就是为了在不知道数据结构的情况下仍然能获取数据。我们只需要通过next函数获取数据就可以了,我不关心next函数里面的实现逻辑,这就满足低耦合高内聚的设计原则。

image.png

JS中内置的迭代器Symbol.iterator

迭代器用于js中有序结构的数据遍历。

有序结构

  • 字符串
  • 数组
  • NodeList 等 DOM 集合
  • Map
  • Set
  • arguments

【注意】对象 object 不是有序结构

JS内置Symbol.iterator

每个有序对象,都内置了 Symbol.iterator 属性,属性值是一个函数。 执行该函数讲返回 iterator 迭代器,有 next() 方法,执行返回 { value, done } 结构。

const arr = [10, 20, 30]
const iterator = arr[Symbol.iterator]()

iterator.next() // {value: 10, done: false}
iterator.next() // {value: 20, done: false}
iterator.next() // {value: 30, done: false}
iterator.next() // {value: undefined, done: true}
const map = new Map([ ['k1', 'v1'], ['k2', 'v2'] ])
const mapIterator = map[Symbol.iterator]()

mapIterator.next() // { value: [k1, v1], done: false }
mapIterator.next() // { value: [k2, v2], done: false }
mapIterator.next() // { value: undefined, done: true }

另外,有些对象的 API 也会生成有序对象,如: map 的 values(), keys(), entries()

const values = map.values() // 并不是 Array
const valuesIterator = values[Symbol.iterator]()

自定义一个Symbol.iterator

interface IteratorRes {
    value: number | undefined
    done: boolean
}

class CustomIterator {
    private length = 3
    private index = 0

    next(): IteratorRes {
        this.index++
        if (this.index <= this.length) {
            return { value: this.index, done: false }
        }
        return { value: undefined, done: true }
    }

    [Symbol.iterator]() {
        return this
    }
}

const iterator = new CustomIterator()
console.log( iterator.next() )
console.log( iterator.next() )
console.log( iterator.next() )
console.log( iterator.next() )

迭代器在JS中的实际应用

有序结构的作用

1. 所有有序结构,都支持 for...of 语法

也就是如果一个数据是有序就够,就能用for of来遍历。比如,上面我们自定义的迭代器,它也可以用for of来遍历。

const iterator = new CustomIterator()
for (let n of iterator) {
  console.log(n)
}

也就是for of的背后,就是不断的执行iterator.next()

【注意】普通的对象是无法用for of来遍历的,因为普通对象不是有序结构,必须使用其他的辅助静态函数,如 Object.keys()或 Object.entries()

const colorsHex = { 'white': '#FFFFFF', 'black': '#000000' }; 
for (const [color, hex] of Object.entries(colorsHex)) { 
    console.log(color, hex); 
} 
// 'white' '#FFFFFF' 
// 'black' '#000000'

2. 所有有序结构,都能使用解构,扩展操作符等

const set = new Set([10, 20, 30])
const [n1, n2] = set

因为Set是有序结构,所以我们可以使用解构的方法获取。解构的方式我不管数据里面的结构是什么,都能通过解构的方法获取,这样就符合低耦合高内聚的特点

上面我自定义的迭代器,也可以使用解构的方式获取:

const iterator = new CustomIterator()

const [n1, n2] = iterator
console.log(n1, n2) // 1, 2

3. 所有有序结构,都可以用来创建set map 创建set 和 map的时候,只要是有序结构即可,如:

const set = new Set('abc')

字符串也是一个有序结构,所以也可以创建Set。

生成器 Generator

基本用法

function* genNums() {
    yield 10
    yield 20
    yield 30
}

const numsIterator = genNums()
numsIterator.next() // {value: 10, done: false}
numsIterator.next() // {value: 20, done: false}
numsIterator.next() // {value: 30, done: false}
numsIterator.next() // {value: undefined, done: true}

从上面的代码可知,生成器返回的就是一个迭代器迭代器是什么呢?从上面的知识可以知道迭代器就是一个有有序结构,它内部实现了next等功能

所以,以后我们看到生成器就可以把它想象成成一个迭代器,迭代器就是一个有序结构。既然是有序结构,就可以通过 for of 的方式来实现

for (let n of numsIterator) {
    console.log(n) // 10 20 30
}

yield* 语法

yield * 后面接一个有序结构,因为它实现了 Symbol iterator。

function* genNums() {
    yield* [100, 200, 300] // 相当于:循环数组,分别 yield
}
const numsIterator = genNums()
numsIterator.next() // {value: 100, done: false}
numsIterator.next() // {value: 200, done: false}
numsIterator.next() // {value: 300, done: false}
numsIterator.next() // {value: undefined, done: true}

// 也可以使用for of
for (let n of numsIterator) {
    console.log(n)
}

yield* [100, 200, 300] 就类似于

const arr = [100, 200, 300]
for(let i = 0; i < arr.length; i++) {
    yield arr[i]
}

利用 yield* 重构自定义迭代器

根据上面的知识,可以知道生成器已经实现了迭代器的功能,那么这里我们自定义的迭代器就可以使用生成器帮我们实现迭代器的功能。

class CustomIterator {
    private data: number[]

    constructor() {
        this.data = [10, 20, 30]
    }
    
    * [Symbol.iterator]() {
        yield* this.data
    }
}

const iterator = new CustomIterator()
for (let n of iterator) {
    console.log(n)
}

利用 yield 遍历 DOM 树

DOM 结构

<div id="container">
  <p><span>这是一个span</span></p>
  <div>
    <p>这是一段话</p>
  </div>
</div>
function* traverse(elemList: Element[]): any {
  for (const elem of elemList) {
    yield elem

    const children = Array.from(elem.children)
    if (children.length) {
      yield* traverse(children)
    }
  }
}

const container = document.getElementById('container')
if (container) {
  for (let node of traverse([container])) {
    console.log(node)
  }
}

image.png

首先traverse([container]返回一个迭代器,那么迭代器是什么呢?迭代器就是有序结构,所以可以使用for of来循环。

for (let node of traverse([container])) {
    console.log(node)
}

yield* 后面需要接一个有序结构,traverse(children)刚好返回一个有序结构,没毛病。

if (children.length) {
    yield* traverse(children)
}