云计算百科
云计算领域专业知识百科平台

JavaScript 迭代器与生成器

JavaScript 迭代器与生成器

  • 前言:从 for…of 说起
  • 理解迭代和迭代器模式
    • 什么是迭代?
    • 迭代器模式
    • 迭代器模式示意图
  • 可迭代协议与迭代器协议
    • 可迭代协议(Iterable Protocol)
    • 迭代器协议(Iterator Protocol)
    • 自定义迭代器
    • 提前终止迭代器
    • 普通对象可迭代
  • 生成器
    • 生成器基础
    • 生成器的几种形式
      • 生成器函数声明
      • 生成器函数表达式
      • 作为对象方法的生成器
      • 作为类方法的生成器
    • yield 关键字
    • 生成器的执行流程
    • 生成器的双向通信
    • 提前终止生成器
      • return()
      • throw()
  • 生成器与迭代器的关系
  • 用生成器实现 async/await 的 Polyfill
  • 结语

当我们需要在 JavaScript 遍历数组、处理异步数据流,甚至实现自定义的数据结构时,迭代器和生成器是其中强大而优雅的工具。理解它们不仅能写出更简洁的代码,还能深入理解 JavaScript 异步编程的底层原理。

前言:从 for…of 说起

const arr = [1, 2, 3];

// 传统的遍历方式
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]); // 1, 2, 3
}

// for…of
for (const item of arr) {
console.log(item); // 1, 2, 3
}

当我们在使用 for…of 这背后发生了什么?为什么对象不能直接用 for…of?

这都和 JavaScript 的迭代协议有关。理解这个协议,我们就能让任何对象都支持这种优雅的遍历方式。

理解迭代和迭代器模式

什么是迭代?

迭代是指按照某种顺序逐个访问集合中的每个元素的过程。在开发中,我们几乎每天都在进行迭代操作:

// 1. 数组迭代
const arr = [1, 2, 3];
arr.forEach(task => console.log(task));

// 2. 字符串迭代
const str = 'hello';
for (const char of str) {
console.log(char);
}

// 3. Map/Set 迭代
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) {
console.log(key, value);
}

迭代器模式

迭代器模式是一种设计模式,它提供了一种方法,顺序访问一个聚合对象(可迭代对象)中的各个元素,而又不暴露该对象的内部表示。

迭代器模式示意图

┌─────────┐
│ 聚合对象 │
└─────────┘


┌────────────┐ ┌─────────┐
│ 迭代器工厂 │───────→ │ 元素 │
└────────────┘ └─────────┘
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ 迭代器 │───────────→ │ 元素 │
└─────────┘ └─────────┘
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ 迭代器 │───────────→ │ 元素 │
└─────────┘ └─────────┘
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ │
└─────────┘ └─────────┘

可迭代协议与迭代器协议

JavaScript 通过两个协议定义了迭代行为:

可迭代协议(Iterable Protocol)

对象需要实现 Iterable 接口(可迭代协议),并暴露一个默认属性作为“默认迭代器”,而且这个属性必须使用特殊的 Symbol.interator 作为键。

迭代器协议(Iterator Protocol)

对象需要实现 next() 方法,返回一个 IteratorResult 对象,其中包括迭代器返回的下一个值;如果不调用 next() 方法,则无法知道迭代器的当前位置。

自定义迭代器

class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}

// 实现可迭代协议
[Symbol.iterator]() {
let current = this.start;
const end = this.end;

// 返回迭代器对象(实现迭代器协议)
return {
next() {
if (current <= end) {
const value = current;
current += 1;
return { value, done: false };
}
return { value: undefined, done: true };
}
};
}
}

// 使用自定义迭代器
const range = new Range(1, 5);
const iterator = range[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: 4, done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

提前终止迭代器

上述代码中,我们自定义了一个迭代器,其实,它还有两个可选的方法,用来提前终止迭代器:return() 和 throw() :

class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}

// 实现可迭代协议
[Symbol.iterator]() {
let current = this.start;
const end = this.end;

// 返回迭代器对象(实现迭代器协议)
return {
next() {
if (current <= end) {
const value = current;
current += 1;
return { value, done: false };
}
return { value: undefined, done: true };
},
// 可选:实现 return 方法,用于提前终止迭代
return() {
console.log('迭代提前终止');
return { done: true };
},

// 可选:实现 throw 方法,用于抛出异常
throw(error) {
console.log('迭代器抛出异常:', error);
return { done: true };
}
};
}
}

普通对象可迭代

在引言的例子中,我们有提到普通对象不能使用 for…of 迭代遍历,因此普通对象不属于可迭代对象,那我们应该如何做,才能让普通对象也可以迭代呢?

const obj = { a: 1, b: 2, c: 3 };
obj[Symbol.iterator] = function* () {
for (const key in this) {
if (this.hasOwnProperty(key)) {
yield [key, this[key]];
}
}
};
for (o of obj) {
console.log(o);
}

生成器

生成器基础

生成器ES6 引入的特殊函数,它可以暂停和恢复执行,是创建迭代器的强大工具。其形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。

注:箭头函数不能用来定义生成器。

生成器的几种形式

生成器函数声明

function* generatorFunction() {
yield 1;
yield 2;
yield 3;
}

生成器函数表达式

const generatorExpr = function* () {
yield 1;
yield 2;
yield 3;
};

作为对象方法的生成器

const obj = {
*generatorMethod() {
yield '方法中的生成器';
}
};

作为类方法的生成器

class MyClass {
*generatorMethod() {
yield '类方法中的生成器';
}
}

标识生成器函数的星号不受两边空格的影响,即:function* generatorFunction(){} 、function * generatorFunction(){} 、function *generatorFunction(){} ,这几种写法都是等价的。

yield 关键字

yield 关键字可以让生成器停止和继续执行,是生成器中最有用的地方。生成器在遇到 yield 关键字之前都会正常执行;遇到后,会停止执行,函数作用域的状态会被保留。停止执行的生成器函数,只能通过生成器对象调用 next() 来恢复执行。

生成器的执行流程

我们可以通过一个简单的示例,来观察生成器的执行流程:

function* simpleGenerator() {
console.log('开始执行');

const a = yield '第一次 yield';
console.log('a =', a);

const b = yield '第二次 yield';
console.log('b =', b);

const c = yield '第三次 yield';
console.log('c =', c);

return '结束';
}

// 创建生成器对象(不会立即执行)
const gen = simpleGenerator();
// 第一次调用 next(),执行到第一个 yield
let result = gen.next();
// 第二次调用 next(),传入参数给第一个 yield
result = gen.next('参数A');
// 第三次调用 next(),传入参数给第二个 yield
result = gen.next('参数B');
// 第四次调用 next(),传入参数给第三个 yield
result = gen.next('参数C');
// 再次调用 next()
result = gen.next();

生成器的双向通信

上述生成器的执行流程的例子中,我们可以通过 next() 方法给生成器函数传参;同时,我们也可以接收 yield 关键字后面的返回值:

function* simpleGenerator() {
console.log('开始执行');

const a = yield '第一次 yield';
console.log('a =', a);

const b = yield '第二次 yield';
console.log('b =', b);

const c = yield '第三次 yield';
console.log('c =', c);

return '结束';
}

// 创建生成器对象(不会立即执行)
const gen = simpleGenerator();
// 第一次调用 next(),执行到第一个 yield
let result = gen.next();
console.log('result =', result.value); // 输出: result = 第一次 yield

// 第二次调用 next(),传入参数给第一个 yield
result = gen.next('参数A');
console.log('result =', result.value); // 输出: result = 第二次 yield

// 第三次调用 next(),传入参数给第二个 yield
result = gen.next('参数B');
console.log('result =', result.value); // 输出: result = 第三次 yield

// 第四次调用 next(),传入参数给第三个 yield
result = gen.next('参数C');
console.log('result =', result.value); // 输出: result = 结束

// 再次调用 next()
result = gen.next();
console.log('result =', result.value); // 输出: result = undefined

提前终止生成器

与迭代器类似,生成器也支持提前终止/关闭的概念:return() 和 throw() 。

return()

但与迭代器不同,所有的生成器对象都有 return() 方法,只要通过它进入关闭状态,就无法恢复了。

const gen = simpleGenerator();
console.log(gen.return('终止了')); // { value: '终止了', done: true }
console.log(gen.next('第一次 yield')); // { value: undefined, done: true }

throw()

throw() 方法会在暂停的时候,将一个提供的错误注入到生成器对象中。如果错误没有被处理,生成器就会关闭:

const gen = simpleGenerator();
try {
gen.throw(new Error('第一次 throw')); // 直接抛出错误,进入 catch 块
} catch (e) {
console.log('捕获到错误:', e.message); // 输出: 捕获到错误: 第一次 throw
}
console.log('继续执行gen:', gen.next()); // 输出: 继续执行gen: { value: undefined, done: true }

生成器与迭代器的关系

生成器作为迭代器工厂,会自动实现迭代器协议,同时生成器对象本身也是可迭代的:

function* createIterator(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}

const iterator1 = createIterator([1, 2, 3]);
console.log(iterator1.next()); // { value: 1, done: false }
console.log(iterator1.next()); // { value: 2, done: false }
console.log(iterator1.next()); // { value: 3, done: false }
console.log(iterator1.next()); // { value: undefined, done: true }

// 生成器对象本身也是可迭代的
const iterableGen = createIterator(['a', 'b', 'c']);
for (const item of iterableGen) {
console.log(item); // a, b, c
}

用生成器实现 async/await 的 Polyfill

function asyncGenerator(generatorFn) {
return function (args) {
const generator = generatorFn.apply(this, args);

return new Promise((resolve, reject) => {
function step(key, arg) {
let result;

try {
result = generator[key](arg);
} catch (error) {
reject(error);
return;
}

const { value, done } = result;

if (done) {
resolve(value);
} else {
// 假设 value 总是一个 Promise
Promise.resolve(value).then(
val => step('next', val),
err => step('throw', err)
);
}
}

step('next');
});
};
}

结语

迭代器和生成器是 JavaScript 中强大而优雅的特性。它们不仅提供了更灵活的迭代方式,还开启了异步编程和惰性求值的新范式。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

赞(0)
未经允许不得转载:网硕互联帮助中心 » JavaScript 迭代器与生成器
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!