Iterator-Generator

Iterator(迭代器)

查看

迭代器:使用户在容器对象(如数组)上遍访的对象。即迭代器是帮助我们对某个数据结构进行遍历的对象。

在JS中,迭代器是一个具体的对象,我们可以手动实现,但这个对象需要符合迭代器协议(iterator protocol)

  • 迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式。

  • 在JS中这个标准就是一个特定的next方法。

next方法有如下要求:

  • 一个无参数或着一个参数的函数,返回一个应当拥有以下两个属性的对象:

    • done(boolean)
      • 如果迭代器可以产生序列中的下一个值,则为false。
      • 如果迭代器已将序列迭代完毕,则为true。这种情况下,value是可选的,如果它依然存在,即为迭代结束后的默认返回值。
    • value
      • 迭代器返回的任何JavaScript值,done为true时可省略。

实现迭代器,以获取对象的键(包含Symbol)为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function createObjIterator(obj) {
let keys = Reflect.ownKeys(obj)
let index = 0
return {
next() {
if (index >= keys.length) {
return { done: true }
}

return { done: false, value: keys[index++] }
}
}
}

const obj = {
name: '111',
age: 12,
[Symbol()]: 222
}

const objIterator = createObjIterator(obj)
console.log(objIterator.next()) // {done: false, value: 'name'}
console.log(objIterator.next()) // {done: false, value: 'age'}
console.log(objIterator.next()) // {done: false, value: Symbol()}
console.log(objIterator.next()) // {done: true}

可迭代对象

查看

可迭代对象和迭代器是两个不同的概念,前面我们可以通过迭代器对对象进行遍历操作,但需遍历对象和迭代器时分离开的,如果我们把迭代器和对象结合在一起,就叫做可迭代对象。

但将迭代器和可迭代对象结合起来成为可迭代对象,可是有一定规范的:

  • 这个对象要实现迭代器协议(iterator protocol)。

  • 这个对象必须实现@@iterator方法,在代码层面是对象要实现[Symbol.iterator]属性。

可迭代对象是可以通过for of 循环进行遍历操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const obj = {
name: '111',
age: 12,
[Symbol()]: 222,
[Symbol.iterator]() {
const keys = Reflect.ownKeys(this)
let index = 0
return {
next() {
if (index < keys.length) {
return { done: false, value: keys[index++] }
}
return { done: true }
}
}
}
}
for (let key of obj) {
console.log(key)
}
// name
// age
// Symbol()
// Symbol(Symbol.iterator) 由于 Reflect.ownKeys()是Object.getOwnPropertyNames()和Object.getOwnPropertySymbols()的合集,所以Symbol()也会被遍历到

可迭代对象的使用场景:

  • JavaScript中语法:for …of、展开语法(spread syntax)、yield*(后面讲)、解构赋值(Destructuring_assignment)。

  • 创建一些对象时:new Map([Iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable])

  • 一些方法的调用:Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable)。

补充:

迭代器中其实还可以做一个操作,就是监听容器对象的中断操作:

  • 比如遍历的过程中通过break、return、throw中断了循环操作。

如果我们想实现这个操作,就需要在迭代器对象中加入return方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const obj = {
name: '111',
age: 12,
[Symbol()]: 222,
[Symbol.iterator]() {
const keys = Reflect.ownKeys(this)
let index = 0
return {
next() {
if (index < keys.length) {
return { done: false, value: keys[index++] }
}
return { done: true }
},
return() {
console.log(111)
return { done: true }
}
}
}
}
for (let key of obj) {
console.log(key)
if (key === 'age') {
break
}
}

Generator(生成器)

查看

生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等。

平时我们写的函数终止条件要么是return,要么是发生了异常,生成器中又提供了yield关键字来控制函数的执行。

生成器函数也是一个函数,但与普通的函数有一些区别:

  • 生成器函数需要在function后加上一个*

  • 生成器函数可以通过yield关键字控制函数的执行流程。

  • 生成器函数的返回值是一个Generator对象。

    • 生成器事实上是一种特殊的迭代器。
    • Instead, they return a special type of iterator, called a Generator.

生成器实例代码:

1
2
3
4
5
6
7
8
9
10
11
12
function* foo(y1) {
console.log(1, y1) // 1 111
const y2 = yield 1
console.log(2, y2) // 2 222
const y3 = yield 2
console.log(3, y3) // 3 333
}

const generator = foo('111')
console.log(generator.next()) // {value: 1, done: false}
console.log(generator.next('222')) // {value: 2, done: false}
console.log(generator.next('333')) // {value: undefined, done: true}

生成器函数中的代码并不会默认执行,但可以通过其返回的生成器调用next的方法再根据yield关键字控制代码的执行,一次next的调用运行到一个yield关键字的位置,并返回一个对象,对象为{value: yield关键字后面的值,done: false},当next的调用返回的done为true时,表示函数执行完毕,此时返回的对象为{value: undefined, done: true}。也可以通过next方法向构造器函数中传值,通过yield关键字获取到传进来的值,但第一次next的传值调用是无法被接收到的。

对于生成器,我们可以直接调用其return和throw方法来控制函数的提前结束和抛出异常。

提前结束
1
2
3
4
5
6
7
8
9
10
11
12
function* foo(y1) {
console.log(1, y1) // 1 111
const y2 = yield 1
console.log(2, y2)
const y3 = yield 2
console.log(3, y3)
}

const generator = foo('111')
console.log(generator.next()) // {value: 1, done: false}
console.log(generator.return('222')) // {value: '222', done: true}
console.log(generator.next('333')) // {value: undefined, done: true}

以上代码可以看出,生成器函数的调用会在生成器的return方法调用时提前结束,即使后面还有代码没执行完。return方法还会返回一个对象,对象为{value: return方法传进来的值,done: true},后面即使在调用next方法,结果也都是{value: undefined,done: true}了。

抛出异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function* foo(y1) {
console.log(1, y1) // 1 111
try {
const y2 = yield 1
console.log(2, y2)
const y3 = yield 2

console.log(3, y3)
} catch (error) {
console.log(error)
}
}

const generator = foo('111')
console.log(generator.next()) // {value: 1, done: false}
console.log(generator.throw('执行异常')) // {value: undefined, done: true}
console.log(generator.next('333')) // {value: undefined, done: true}

以上代码可以看出,生成器函数的调用会在生成器的throw方法调用时提前结束,并抛出异常,即使后面还有代码没执行完。throw方法还会返回一个对象,对象为{value: undefined,done: true},后面即使在调用next方法,结果也都是{value: undefined,done: true}了。

既然生成器是一种特殊的迭代器,那么我们可以用生成器替代迭代器,那上文的代码为例,用生成器重写后为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function* createObjIterator(obj) {
let keys = Reflect.ownKeys(obj)

for (let key of keys) {
yield key
}
}

const obj = {
name: '111',
age: 12,
[Symbol()]: 222
}

const objIterator = createObjIterator(obj)
console.log(objIterator.next()) // {value: 'name', done: false}
console.log(objIterator.next()) // {value: 'age', done: false}
console.log(objIterator.next()) // {value: Symbol(), done: false}
console.log(objIterator.next()) // {value: undefined, done: true}

用生成器实现可迭代对象的@@iterator方法,只需要在[Symbol.iterator](){}开头加一个*,那么它就是一个生成器方法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const obj = {
name: '111',
age: 12,
[Symbol()]: 222,
*[Symbol.iterator]() {
const keys = Reflect.ownKeys(this)
for (let key of keys) {
yield key
}
}
}
for (let key of obj) {
console.log(key)
}

上述我们实现迭代器或生成器内部获取元素的时候都是用的for of 循环,for of可以用于可迭代对象,如果内部循环的是一个可迭代对象的话,我们可以将操作简写以下。

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
name: '111',
age: 12,
[Symbol()]: 222,
*[Symbol.iterator]() {
const keys = Reflect.ownKeys(this)
yield* keys
}
}
for (let key of obj) {
console.log(key)
}

Generator解决异步回调地狱问题

查看

场景:当我们发送一个异步请求,需要根据该请求结果发送另一个请求,另一个请求的结果时其他请求所必需的,依次类推,给出回调地狱的代码,尝试对其优化。

1
2
3
4
5
6
7
8
9
function getData(){
requestData('111').then(res1 => {
requestData(res1).then(res2 => {
requestData(res2).then(res3 => {
console.log(res3)
})
})
})
}

我们可以通过返回Promise对其进行优化

1
2
3
4
5
6
7
8
9
function getData(){
requestData('111').then(res1 => {
return requestData(res1)
}).then(res2 => {
return requestData(res2)
}).then(res3 => {
console.log(res3)
})
}

以上代码看起来好多了,但阅读性其实还是比较差的,接下来我们用生成器进行优化。

1
2
3
4
5
6
function* getData(){
const res1 = yield requestData('111')
const res2 = yield requestData(res1)
const res3 = yield requestData(res2)
console.log(res3)
}

上述封装请求的生成器函数看起来挺清爽的,但是调用起来就麻烦了。

1
2
3
4
5
6
7
8
9
const generator = getData()

generator.next().value.then(res1 => {
generator.next(res1).value.then(res2 => {
generator.next(res2).value.then(res3 => {
console.log(res3)
})
})
})

但是这样又变成了上述用Promise解决时的问题,但好在生成器的每次next调用都会返回一个对象,对象中有一个属性done可以判断生成器函数中的代码是否执行完毕,所以我们可以对其进行封装优化。

封装自动执行generator函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function* getData() {
const res1 = yield requestData('111')
const res2 = yield requestData(res1)
const res3 = yield requestData(res2)
console.log(res3)
}

function execGenerator(genFn) {
const generator = genFn()

function exec(res) {
const result = generator.next(res)
if (result.done) return result.value
result.value.then(res => {
exec(res)
})
}
exec()
}