迭代器与生成器
1 for...in 和 for...of 的区别(迭代方式)
无论是for...in
还是for...of
语句都是迭代一些东西。它们之间的主要区别在于它们的迭代方式。
for...in
语句以任意顺序迭代对象的可枚举属性(包括原型链上的属性,不包括 Symbol)。(适合对象属性遍历)for...of
语句遍历可迭代对象的值序列。(适合数组遍历)
以下示例显示了与Array一起使用时,for...of
循环和for...in
循环之间的区别。
// 每个对象将继承objCustom属性,Array的每个对象将继承arrCustom属性,
// 由于继承和原型链,对象iterable会继承属性objCustom和arrCustom
Object.prototype.objCustom = function() {};
Array.prototype.arrCustom = function() {};
let iterable = [3, 5, 7];
iterable.foo = 'hello';
// 此循环仅以原始插入顺序记录 iterable 对象的可枚举属性名,遍历出来了数组索引以及arrCustom和objCustom
for (let i in iterable) {
console.log(i); // 0, 1, 2, "foo", "arrCustom", "objCustom"
}
// 遍历自身的可枚举属性
for (let i in iterable) {
if (iterable.hasOwnProperty(i)) {
console.log(i); // 0, 1, 2, "foo"
}
}
// 该循环迭代并记录iterable作为可迭代对象定义的迭代值,这些是数组元素3, 5, 7,而不是任何对象的属性
for (let i of iterable) {
console.log(i); // 3, 5, 7
}
2 迭代协议
For of 语句提及到了可迭代对象的概念,那么什么是可迭代对象呢?
迭代协议作为 ECMAScript 2015 的一组补充规范,分为可迭代协议与迭代器协议
2.1 可迭代协议
- 可迭代协议指出,要成为可迭代对象, 一个对象必须实现
@@iterator
方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为@@iterator
的属性,可通过常量Symbol.iterator
访问该属性:
let someString = "hi";
typeof someString[Symbol.iterator]; // "function"
- 人话总结:一个对象或者原型链上有
@@iterator
(Symbol.iterator)属性,那么就称其为可迭代对象
2.2 可迭代对象示例
2.2.1 内置可迭代对象:
目前所有的内置可迭代对象如下:String
、Array
、TypedArray
、Map
和Set
,它们的原型对象都实现了 @@iterator
方法。
2.2.2 自定义可迭代对象:
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable]; // [1, 2, 3]
2.2.3 接受可迭代对象的内置API
new Map([iterable])
、new WeakMap([iterable])
new Set([iterable])
、new WeakSet([iterable])
Promise.all([iterable])
、Promise.race([iterable])
Array.from([iterable])
2.2.4 需要可迭代对象的语法
一些语句和表达式:
- for..of
for(let value of ["a", "b", "c"]){
console.log(value);
}
- 展开语法
[..."abc"]; // ["a", "b", "c"]
- 生成器
function* gen() {
yield* ["a", "b", "c"];
}
gen().next(); // { value: "a", done: false }
- 解构赋值
[a, b, c] = new Set(["a", "b", "c"]);
a // "a"
2.3 迭代器协议
只有一个 Symbol.iterator 迭代器属性还不够,对于这个属性也有一些要求:
- 对象的迭代器属性必须返回一个拥有以下语义的
next()
方法:
一个无参数的或者可以接受一个参数的函数,返回一个应当拥有以下两个属性的对象:
done
(boolean) 如果迭代器可以产生序列中的下一个值,则为false
。(这等价于没有指定done
这个属性。) 如果迭代器已将序列迭代完毕,则为true
。这种情况下,value
是可选的,如果它依然存在,即为迭代结束之后的默认返回值。value
迭代器返回的任何 JavaScript 值。done 为 true 时可省略。
- 人话翻译:迭代器指的是具有next()方法的类或函数。
next()
是一个无参数或者可以接受一个参数的函数,返回一个拥有done和value属性的对象
2.4 迭代器示例
2.4.1 自定义迭代器
function makeIterator(array) {
let nextIndex = 0;
return {
next: function () {
return nextIndex < array.length ? {
value: array[nextIndex++],
done: false
} : {
done: true
};
}
};
}
let it = makeIterator(['gsq', 'zs']);
console.log(it.next().value); // 'gsq'
console.log(it.next().value); // 'zs'
console.log(it.next().done); // true
2.4.2 使用class改写
class MyIterator {
constructor(data) {
this.data = data
}
[Symbol.iterator]() {
// 注意这里index没有定义为原型属性
// 每个迭代之间的index是相互独立的
// 使得多次迭代更安全?
let index = 0;
return {
next: () => {
if (index < this.data.length) {
return {value: this.data[index++], done: false}
} else {
return {done: true}
}
}
}
}
}
let mi = new MyIterator(['gsq', 'zs']);
console.log(mi.next().value); // 'gsq'
console.log(mi.next().value); // 'zs'
console.log(mi.next().done); // true
2.4.3 使用Generator实现迭代器
function* myGeneratorIterator(array) {
let nextIndex = 0;
while(nextIndex < array.length) {
yield array[nextIndex++];
}
}
let gen = makeSimpleGenerator(['gsq', 'zs']);
console.log(gen.next().value); // 'gsq'
console.log(gen.next().value); // 'zs'
console.log(gen.next().done); // true
生成器对象既是可迭代对象也是迭代器
2.5 总结
可迭代对象:一个对象或者原型链上具有迭代器属性(Symbol.iterator
),该属性返回迭代器对象,该对象具有 next
方法,返回包含value
和 done
属性的对象
下面我们尝试让一个普通对象可以使用 for of
遍历:
const obj = { name: 'gsq', age: 10 };
obj[Symbol.iterator] = function () {
// 获取对象自身的键数组
const keys = Object.keys(this);
// 设置数组索引
let index = 0;
// 定义 next 方法
const next = () => {
if (index < keys.length) {
const key = keys[index++];
const value = this[key];
return { value: [key, value], done: false };
} else {
return { done: true };
}
};
// 返回迭代器对象
return { next };
};
for (const [key, value] of obj) {
console.log(`${key}: ${value}`);
}
3 生成器
生成器(Generator)是一种特殊的函数,它可以暂停执行并且在稍后可以从停止的地方继续执行。使用生成器可以轻松创建一个迭代器,对于处理大数据集合、控制复杂的异步逻辑等都非常有用。
生成器的名字来源于它能“生成”(即产生)一系列值的能力。这个“生成”是通过它的迭代器接口逐个“产生”值来实现的。每当生成器的迭代器方法 next()
被调用时,生成器函数会执行到下一个 yield
表达式处并暂停,此时它将产生(generate)一个值(通过 yield
表达式返回的值),然后等待下一次的 next()
调用。
简单地说,生成器是一种特殊的迭代器的生成器。与普通的迭代器(通常一次性返回所有值)不同,生成器允许我们按需一次产生一个值,这种按需产生可以是同步的,也可以是异步的。生成器提供的这种能力特别适用于以下情况:
- 延迟产生值,从而避免不必要的计算,节省内存资源。
- 逐个处理一个大的
生成器函数通过 function*
语法来定义,使用 yield
关键字来暂停函数的执行。每次调用生成器函数都会返回一个迭代器,这个迭代器可用于控制生成器函数的执行。
function* gen(){
yield 1
}
3.1 特点
每次调用生成器函数时,它都会返回一个新的
Generator
对象,该对象符合迭代器协议。生成器函数在执行时能暂停,后面又能从暂停处继续执行
调用一个生成器函数并不会马上执行内部的语句,而是返回一个这个生成器的迭代器对象
const a = gen()
typeof a[Symbol.iterator] // 'function'
- 当返回的迭代器的
next()
方法被首次调用时,其内的语句会执行到第一个出现yield
的位置为止 yield
后紧跟迭代器要返回的值- 如果使用了
yield*
,则表示将执行权移交给另一个生成器函数(当前生成器暂停执行) - 调用
next()
方法时,如果传入了参数,那么这个参数会传给上一条执行的yield
语句左边的变量
function *gen(){
yield 10;
x = yield 'foo';
yield x;
}
var gen_obj=gen();
console.log(gen_obj.next());// 执行 yield 10,返回 10
console.log(gen_obj.next());// 执行 yield 'foo',返回 'foo'
console.log(gen_obj.next(100));// 将 100 赋给上一条 yield 'foo' 的左值,即执行 x=100,最后返回 100
console.log(gen_obj.next());// 执行完毕,value 为 undefined,done 为 true
- 当在生成器函数中显式
return
时,会导致生成器立即变为完成状态,即调用next()
方法返回的对象的done
为true
。如果return
后面跟了一个值,那么这个值会作为当前调用next()
方法返回的 value 值。可以理解为,return就是生成器最后一个yield
3.2 应用示例
下面是一些常见的生成器应用场景及代码示例:
- 遍历复杂的数据结构:
function* iterateTree(tree) {
yield tree.value;
if (tree.left) {
yield* iterateTree(tree.left);
}
if (tree.right) {
yield* iterateTree(tree.right);
}
}
// 假设有如下树结构
const tree = {
value: 1,
left: { value: 2, left: null, right: null },
right: { value: 3, left: null, right: null }
};
for (const value of iterateTree(tree)) {
console.log(value); // 1, 2, 3
}
- 分步异步操作:异步操作可以通过生成器函数来逐步处理,常与
Promise
或者库例如co
结合使用。
function* fetchUser(id) {
const userResponse = yield fetch(`/api/user/${id}`);
const userData = yield userResponse.json();
return userData;
}
// 接收一个生成器,并执行它返回promise的函数
const runGen = function(gen) {
const g = gen();
function iter(result) {
if (result.done) return result.value;
return result.value.then(res => iter(g.next(res)));
}
return iter(g.next());
};
// 使用定义的 runGen 函数来执行生成器
runGen(fetchUser).then(userData => console.log(userData));
- 控制流管理:控制异步操作的顺序,类似 async await 实现异步操作的同步写法。
function* createFlow() {
const num1 = yield fetchNumber();
const num2 = yield fetchNumber();
console.log(num1 + num2);
}
// 假设fetchNumber返回一个Promise,这里省略fetchNumber的实现。
- 无限序列:生成器提供了一种优雅的方式来实现无限序列的生成,以下是创建一个无限自增序列的示例。
function* infiniteSequence() {
let i = 0;
while (true) {
yield i++;
}
}
const generator = infiniteSequence();
console.log(generator.next().value); // 0
console.log(generator.next().value); // 1
console.log(generator.next().value); // 2