跳到主要内容

迭代器与生成器

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 语句提及到了可迭代对象的概念,那么什么是可迭代对象呢?

迭代协议 - JavaScript | MDN (mozilla.org)

迭代协议作为 ECMAScript 2015 的一组补充规范,分为可迭代协议迭代器协议

2.1 可迭代协议

  • 可迭代协议指出,要成为可迭代对象, 一个对象必须实现 @@iterator 方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator访问该属性:
let someString = "hi";
typeof someString[Symbol.iterator]; // "function"
  • 人话总结:一个对象或者原型链上有@@iterator(Symbol.iterator)属性,那么就称其为可迭代对象

2.2 可迭代对象示例

2.2.1 内置可迭代对象:

目前所有的内置可迭代对象如下:StringArrayTypedArrayMapSet,它们的原型对象都实现了 @@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 方法,返回包含valuedone属性的对象

下面我们尝试让一个普通对象可以使用 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 特点

  1. 每次调用生成器函数时,它都会返回一个新的 Generator 对象,该对象符合迭代器协议

  2. 生成器函数在执行时能暂停,后面又能从暂停处继续执行

  3. 调用一个生成器函数并不会马上执行内部的语句,而是返回一个这个生成器的迭代器对象

const a = gen() 
typeof a[Symbol.iterator] // 'function'
  1. 当返回的迭代器的next()方法被首次调用时,其内的语句会执行到第一个出现yield的位置为止
  2. yield后紧跟迭代器要返回的值
  3. 如果使用了yield*,则表示将执行权移交给另一个生成器函数(当前生成器暂停执行)
  4. 调用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
  1. 当在生成器函数中显式 return 时,会导致生成器立即变为完成状态,即调用 next() 方法返回的对象的 donetrue。如果 return 后面跟了一个值,那么这个值会作为当前调用 next() 方法返回的 value 值。可以理解为,return就是生成器最后一个yield

3.2 应用示例

下面是一些常见的生成器应用场景及代码示例:

  1. 遍历复杂的数据结构
   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
}
  1. 分步异步操作:异步操作可以通过生成器函数来逐步处理,常与 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));
  1. 控制流管理:控制异步操作的顺序,类似 async await 实现异步操作的同步写法。
   function* createFlow() {
const num1 = yield fetchNumber();
const num2 = yield fetchNumber();
console.log(num1 + num2);
}

// 假设fetchNumber返回一个Promise,这里省略fetchNumber的实现。
  1. 无限序列:生成器提供了一种优雅的方式来实现无限序列的生成,以下是创建一个无限自增序列的示例。
   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