Iterator
Iteration Protocol
Iteration protocol:
- 一个数据结构只要实现了
[Symbol.iterator]()接口, 便可成为可迭代数据结构 (Iterable):- String:
StringIterator. - Array:
ArrayIterator. - Map:
MapIterator. - Set:
SetIterator. arguments对象.- DOM collection (
NodeList):ArrayIterator.
- String:
- 接收可迭代对象的原生语言特性:
for...in/for...of.- Destructing: 数组解构.
...: 扩展操作符 (Spread Operator).Array.from().new Map().new Set().Promise.all().Promise.race().yield*操作符.
for...in/for...of隐形调用迭代器的方式, 称为内部迭代器, 使用方便, 不可自定义迭代过程.{ next, done, value }显式调用迭代器的方式, 称为外部迭代器, 使用复杂, 可以自定义迭代过程.- All built-in ES6 iterators are
Self Iterable Iterator.
interface Iterable<T> {
[Symbol.iterator]: () => Iterator<T>
}
interface Iterator<T> {
next: (...args: []) => IteratorResult<T>
return?: (value?: T) => IteratorResult<T> // Closable iterator
throw?: (e?: any) => IteratorResult<T>
}
interface IterableIterator<T> extends Iterator<T> {
[Symbol.iterator]: () => IterableIterator<T>
}
interface AsyncIterable<T> {
[Symbol.asyncIterator]: () => AsyncIterator<T>
}
interface AsyncIterator<T> {
next: (...args: []) => Promise<IteratorResult<T>>
throw?: (e?: any) => Promise<IteratorResult<T>>
// Closable iterator
return?: (value?: T | PromiseLike<T>) => Promise<IteratorResult<T>>
}
interface AsyncIterableIterator<T> extends AsyncIterator<T> {
[Symbol.asyncIterator]: () => AsyncIterableIterator<T>
}
interface IteratorResult<T> {
done: boolean
value: T
}
Synchronous Iterator
Iterable Object
function methodsIterator() {
let index = 0
const methods = Object.keys(this)
.filter((key) => {
return typeof this[key] === 'function'
})
.map(key => this[key])
// iterator object
return {
next: () => ({
// Conform to Iterator protocol
done: index >= methods.length,
value: methods[index++],
}),
}
}
const myMethods = {
toString() {
return '[object myMethods]'
},
sumNumbers(a, b) {
return a + b
},
numbers: [1, 5, 6],
[Symbol.iterator]: methodsIterator, // Conform to Iterable Protocol
}
for (const method of myMethods)
console.log(method) // logs methods `toString` and `sumNumbers`
function zip(...iterables) {
const iterators = iterables.map(i => i[Symbol.iterator]())
let done = false
return {
[Symbol.iterator]() {
return this
},
next() {
if (!done) {
const items = iterators.map(i => i.next())
done = items.some(item => item.done)
if (!done)
return { value: items.map(i => i.value) }
// Done for the first time: close all iterators
for (const iterator of iterators) {
if (typeof iterator.return === 'function')
iterator.return()
}
}
// We are done
return { done: true }
},
}
}
const zipped = zip(['a', 'b', 'c'], ['d', 'e', 'f', 'g'])
for (const x of zipped)
console.log(x)
// Output:
// ['a', 'd']
// ['b', 'e']
// ['c', 'f']
Iterable Class
class Counter {
constructor(limit) {
this.limit = limit
}
[Symbol.iterator]() {
let count = 1
const limit = this.limit
return {
next() {
if (count <= limit)
return { done: false, value: count++ }
else
return { done: true }
},
return() {
console.log('Exiting early')
return { done: true }
},
}
}
}
const counter1 = new Counter(5)
for (const i of counter1) {
if (i > 2)
break
console.log(i)
}
// 1
// 2
// Exiting early
const counter2 = new Counter(5)
try {
for (const i of counter2) {
if (i > 2)
throw new Error('err')
console.log(i)
}
} catch (e) {}
// 1
// 2
// Exiting early
const counter3 = new Counter(5)
const [a, b] = counter3
// Exiting early
Class Iterator
// Class Iterator:
class MatrixIterator {
constructor(matrix) {
this.x = 0
this.y = 0
this.matrix = matrix
}
next() {
if (this.y === this.matrix.height)
return { done: true }
const value = {
x: this.x,
y: this.y,
value: this.matrix.get(this.x, this.y),
}
this.x++
if (this.x === this.matrix.width) {
this.x = 0
this.y++
}
return { value, done: false }
}
}
// Iterable Class:
class Matrix {
constructor(width, height, element = (x, y) => undefined) {
this.width = width
this.height = height
this.content = []
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++)
this.content[y * width + x] = element(x, y)
}
}
get(x, y) {
return this.content[y * this.width + x]
}
set(x, y, value) {
this.content[y * this.width + x] = value
}
[Symbol.iterator]() {
return new MatrixIterator(this)
}
}
const matrix = new Matrix(2, 2, (x, y) => `value ${x},${y}`)
for (const { x, y, value } of matrix)
console.log(x, y, value)
// → 0 0 value 0, 0
// → 1 0 value 1, 0
// → 0 1 value 0, 1
// → 1 1 value 1, 1
Asynchronous Iterator
const AsyncIterable = {
[Symbol.asyncIterator]() {
return AsyncIterator
},
}
const AsyncIterator = {
next() {
return Promise.resolve(IteratorResult)
},
return() {
return Promise.resolve(IteratorResult)
},
throw(e) {
return Promise.reject(e)
},
}
const IteratorResult = {
value: any,
done: boolean,
}
// Tasks will chained:
ait
.next()
.then(({ value, done }) => ait.next())
.then(({ value, done }) => ait.next())
.then(({ done }) => done)
// Tasks will run in parallel:
ait.next().then()
ait.next().then()
ait.next().then()
function remotePostsAsyncIteratorsFactory() {
let i = 1
let done = false
const asyncIterableIterator = {
// the next method will always return a Promise
async next() {
// do nothing if we went out-of-bounds
if (done) {
return Promise.resolve({
done: true,
value: undefined,
})
}
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${i++}`
).then(r => r.json())
// the posts source is ended
if (Object.keys(res).length === 0) {
done = true
return Promise.resolve({
done: true,
value: undefined,
})
} else {
return Promise.resolve({
done: false,
value: res,
})
}
},
[Symbol.asyncIterator]() {
return this
},
}
return asyncIterableIterator
}
;(async () => {
const ait = remotePostsAsyncIteratorsFactory()
await ait.next() // { done:false, value:{id: 1, ...} }
await ait.next() // { done:false, value:{id: 2, ...} }
await ait.next() // { done:false, value:{id: 3, ...} }
// ...
await ait.next() // { done:false, value:{id: 100, ...} }
await ait.next() // { done:true, value:undefined }
})()
Closable Iterator
- An iterator is closable if it has a method
return().
interface ClosableIterator {
next: () => IteratorResult
return: (value?: any) => IteratorResult
}
- Not all iterators are closable: e.g.
Array Iterator.
const iterable = ['a', 'b', 'c']
const iterator = iterable[Symbol.iterator]()
console.log('return' in iterator)
// => false
- If an iterator is not closable, you can continue iterating over it after an abrupt exit.
- If an iterator is closable, you can't continue iterating over it after an abrupt exit.
function* elements() {
yield 'a'
yield 'b'
yield 'c'
}
function twoLoops(iterator) {
// eslint-disable-next-line no-unreachable-loop -- break loop
for (const x of iterator) {
console.log(x)
break
}
for (const x of iterator) {
console.log(x)
}
}
class PreventReturn {
constructor(iterator) {
this.iterator = iterator
}
[Symbol.iterator]() {
return this
}
next() {
return this.iterator.next()
}
return(value = undefined) {
return { done: false, value }
}
}
twoLoops(elements())
// Output:
// a
twoLoops(new PreventReturn(elements()))
// Output:
// a
// b
// c
twoLoops(['a', 'b', 'c'][Symbol.iterator]())
// Output:
// a
// b
// c
- Manually call
iterator.return():
function take(n, iterable) {
const iter = iterable[Symbol.iterator]()
return {
[Symbol.iterator]() {
return this
},
next() {
if (n > 0) {
n--
return iter.next()
} else {
iter?.return()
return { done: true }
}
},
return() {
n = 0
iter?.return()
},
}
}