引入
首先看一个标准的for循环代码,通过变量 i 来跟踪数组的索引,循环每次执行时,如果 i 小于数组长度则加 1,并执行下一次循环
const list = ['a', 'b', 'c']
for(let i = 0; i < list.length; i++) {
console.log(list[i])
}
虽然循环语句语法简单,但如果将多次循环嵌套则需要追踪多个变量,代码复杂度会大大增加。迭代器的出现旨在消除这种复杂性并减少循环中的错误
迭代器
迭代器是一种特殊对象,它具有一些专门为迭代过程设计的专有接口,所有的迭代器对象都有一个 next() 方法,每次调用都返回一个结果对象,结果对象有两个属性:value(表示下一个将要返回额值)、done(一个布尔值,当没有更多可返回数据时返回 true)。
下面用ES5语法创建一个迭代器
function testIterator(items) {
var i = 0
return {
next: function() {
var done = i >= items.length
var value = !done ? items[i++] : undefined
return {
done: done,
value: value
}
}
}
}
var iterator = testIterator([1, 2, 3])
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 2, done: false }
console.log(iterator.next()) // { value: 3, done: false }
console.log(iterator.next()) // { value: undefined, done: true }
上面这段代码中,testIterator() 方法返回的对象有一个 next() 方法,每次调用都会将 items 数组的下一个值作为 value 返回。在 ES6 中引入了一个生成器对象,它可以让创建迭代器对象的过程变得更简单。
生成器
生成器是一种返回迭代器的函数,通过 function 关键字后的 * 来表示,函数中会用到新的关键字 yield。
function *testIterator() {
yield 1;
yield 2;
yield 3;
}
// 生成器能像普通函数那样被调用,但会返回一个迭代器
var iterator = testIterator()
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 2, done: false }
console.log(iterator.next()) // { value: 3, done: false }
console.log(iterator.next()) // { value: undefined, done: true }
yield 关键字是 ES6 的新特性,可以通过它来指定调用迭代器的 next() 方法时的返回值及返回顺序。
生成器最有趣的部分时,每当执行完一条 yield 语句后函数就会自动停止执行。比如在上面的代码中,执行完语句 yield 1
之后,函数便不再执行其他任何语句,直到再次调用迭代器的 next() 方法才会继续执行 yield 2
语句。
yield 关键字只可再生成器内部使用,在其他地方使用会导致程序抛出错误
function *testIterator(items) {
items.forEach(function(item) {
// 语法错误
yield item
})
}
从字面上看,yield 关键字确实在 testIterator() 函数内部,但是它与 return 关键字一样,都不能穿透函数边界。嵌套函数中的 return 语句不能用作外部函数的返回语句,而此处嵌套函数中的 yield 语句会导致程序抛出语法错误。
生成器函数表达式
只需要在 function 关键字和小括号中间添加 * 即可。(注意:不能使用箭头函数来创建生成器)
let testIterator = function *(items) {
for (let i = 0; i < items.length; i++) {
yield items[i]
}
}
let itreator = testIterator([1, 2, 3])
生成器对象的方法
由于生成器本身就是函数,因此可以将它们添加到对象中。
// ES5
var obj = {
testIterator: function *(items) {
for(let i = 0; i < items.length; i++) {
yield items[i]
}
}
}
// ES6
var obj = {
*testIterator(items) {
...
}
}
let itreator = testIterator([1, 2, 3])
状态机
生成器的一个常用功能是生成状态机
let state = function *() {
while(1) {
yield 'A';
yield 'B';
yield 'C';
}
}
let status = state()
console.log(status.next().value) // A
console.log(status.next().value) // B
console.log(status.next().value) // C
console.log(status.next().value) // A
console.log(status.next().value) // B
console.log(status.next().value) // C
可迭代对象
ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,或者说,一个数据结构只要具有 Symbol.iterator 属性,就可以认为是”可遍历的”(iterable)。 Symbol.iterator 通过指定的函数可以返回一个作用于附属对象的迭代器。
在 ES6 中,所有的集合对象(数组、set集合、Map集合)和字符串都是可迭代对象,这些对象中都有默认的迭代器。(由于生成器默认会为 Symbol.iterator 属性赋值,因此所有通过生成器创建的迭代器都是可迭代对象)
for-of 循环(面试题 for of 原理)
for-of 循环每次执行都会调用可迭代对象的 next() 方法,并将迭代器返回的结果对象的value属性存储在一个变量中,循环将持续执行这一过程,直到返回对象的 done 属性值为 true。
let values = [1, 2, 3]
for (let num of values) {
// 1
// 2
// 3
console.log(num)
}
这段循环的代码通过调用 Symbol.iterator 方法来获取迭代器,这一过程是在 JS 引擎背后完成的。随后迭代器的 next() 方法被多次调用,从其返回对象的 value 属性读取值并存储在变量 num 中,依次为 1、2、3,当结果对象的 done 属性值为 true 时,循环结束,所以 num 不会被赋值为 undefined。
如果只需迭代数组或集合中的值,用 for-of 代替 for 循环是个不错的选择。相比传统的 for 循环,for-of 循环的控制条件更简单,不需要追踪复杂的条件,更少出错。
主意:如果将 for-of 语句用于 不可迭代对象、null、undefined 将会导致程序抛出错误。例如:
function testIterator(items) {
var i = 0
return {
next: function() {
var done = i >= items.length
var value = !done ? items[i++] : undefined
return {
done: done,
value: value
}
}
}
}
var iterator = testIterator([1, 2, 3])
for (let value of iterator) { // Uncaught TypeError: iterator is not iterable
console.log(value)
}
所以 for-of 循环可使用的范围包括:
- 数组
- Set
- Map
- 类数组对象(arguments、DOM NodeList)
- Generator 对象
- 字符串
访问默认迭代器
通过 Symbol.iterator 来访问对象默认的迭代器
let values = [1, 2, 3]
let iterator = values[Symbol.iterator]()
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 2, done: false }
console.log(iterator.next()) // { value: 3, done: false }
console.log(iterator.next()) // { value: undefined, done: true }
具有 Symbol.iterator 属性的对象都有默认的迭代器,因此可以用它来检测对象是否为可迭代对象
function isIterator(object) {
return typeof object[Symbol.iterator] === 'function'
}
console.log(isIterable([1, 2, 3])) // true
console.log(isIterable('Hello')) // true
console.log(isIterable(new Map())) // true
console.log(isIterable(new Set())) // true
console.log(isIterable(new WeakMap())) // false
console.log(isIterable(new WeakSet())) // false
模拟实现 for of
通过 Symbol.iterator 属性获取迭代器对象,然后 while 遍历一下:
function forOf(obj, cb) {
let iterable, result
if (typeof obj[Symbol.iterator] !== 'function') throw new TypeError(result + ' is not iterable')
if (typeof cb !== 'function') throw new TypeError('cb must be callable')
iterable = obj[Symbol.iterator]()
result = iterable.next()
while(!result.done) {
cb(result.value)
result = iterable.next()
}
}
创建可迭代对象
默认情况下,开发者定义的对象都不是可迭代对象,如果给 Symbol.iterator 属性添加一个生成器,则可以将其变成可迭代对象
let obj = {
items: [],
*[Symbol.iterator]() {
for(let item of this.items) {
yield item
}
}
}
for(let value of obj) {
...
}
内建迭代器
ES6中的集合对象、数组、Set集合和Map集合,都内建了三种迭代器:
- entries() 返回一个遍历器对象,用来遍历[键名, 键值]组成的数组。对于数组,键名就是索引值。
- keys() 返回一个遍历器对象,用来遍历所有的键名。
- values() 返回一个遍历器对象,用来遍历所有的键值。
高级迭代器
迭代器传参
迭代器可以用 next() 方法返回值,也可以在生成器内部使用 yield 关键字来生成值。如果给迭代器的 next() 方法传递参数,则这个参数的值就会替代生成器内部上一条 yield 语句的返回值。
function *testIterator() {
let first = yield 1
let second = yield first + 2
yield second + 3
}
let iterator = testIterator()
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next(4)) // { value: 6, done: false } // 4 + 2 = 6
console.log(iterator.next(5)) // { value: 8, done: false } // 5 + 3 = 8
console.log(iterator.next()) // { value: undefined, done: false }
在迭代器中抛出错误
通过 throw() 方法,当迭代器恢复执行时可令其抛出一个错误。这种主动抛出错误的能力对于异步编程而言至关重要,也能提供模拟结束函数执行的两种方法(返回值或抛出错误),从而增强生成器内部的编程弹性。
function *testIterator() {
let first = yield 1
let second = yield first + 2
yield second + 3
}
let iterator = testIterator()
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next(4)) // { value: 6, done: false } // 4 + 2 = 6
console.log(iterator.throw(new Error('err'))) // 从生成器中抛出错误
在这个示例中,前两个表达式正常求值,而调用 throw() 方法后,在继续执行 let second 求值前,错误就会被抛出并阻止代码继续执行。
可以在生成器内部通过 try-catch 来捕获这些错误
function *testIterator() {
let first = yield 1
let second
try {
second = yield first + 2
} catch(e) {
second = 6
}
yield second + 3
}
let iterator = testIterator()
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next(4)) // { value: 6, done: false } // 4 + 2 = 6
console.log(iterator.throw(new Error('err'))) // { value: 9, done: false }
console.log(iterator.next()) // { value: undefined, done: true }
生成器返回语句
由于生成器也是函数,因此可以通过 return 语句提前退出函数执行,对于最后一次 next() 调用,可以主动为其指定一个返回值。在普通函数中可以通过 return 指定一个返回值,在生成器中,return 表示所有操作已经完成,属性 done 会被设置为 true
function *testIterator() {
yield 1
return
yield 2
yield 3
}
let iterator = testIterator()
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: undefined, done: false }
return 语句也可以返回一个指定的值,该值将被赋值给返回对象的 value 属性
function *testIterator() {
yield 1
return 10
yield 2
yield 3
}
let iterator = testIterator()
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 10, done: false }
console.log(iterator.next()) // { value: undefined, done: false }
注意:展开运算符与 for-of 循环语句会直接忽略通过 return 语句指定的任何返回值,只要 done 变成 true 就立即停止读取其他的值。
委托生成器
在某些情况下,我们需要将两个迭代器合二为一,这时可以创建一个生成器,再给 yield 语句添加 * ,就可以将生成数据的过程委托给其他生成器。
function *createNumberIterator() {
yield 1;
yield 2;
return 3;
}
function *createRepeatingIterator(count) {
for(let i=0; i<count; i++) {
yield "repeat";
}
}
function *createCombinedIterator() {
let result = yield *createNumberIterator();
yield *createRepeatingIterator(result);
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: undfined, done: true }"