JavaScript学习笔记:15.迭代器与生成器

JavaScript学习笔记:15.迭代器与生成器

上一篇用类型数组搞定了二进制数据的"高效存储",这一篇咱们解锁JS遍历的"终极形态"------迭代器(Iterators)与生成器(Generators)。你肯定用过for循环遍历数组,用for...of遍历Set,但有没有想过:为什么数组能直接用for...of,普通对象却不行?为什么有些遍历能"暂停",比如异步请求依次执行?这些问题的答案,都藏在迭代器和生成器里。

简单说,迭代器是"遍历说明书"------告诉程序如何一步步取出数据;生成器是"智能导游"------不仅能按说明书带路,还能随时暂停、接收指令调整路线。今天咱们就用"旅游"的生活化比喻,把这对"遍历搭档"的原理、用法和实战价值彻底讲透,让你写出更灵活、更优雅的遍历代码。

一、先破案:为什么需要迭代器?普通循环不够用吗?

普通循环(forwhile)就像"自己开车逛景区"------路线得自己规划,停车点得自己记,遇到复杂数据结构(比如树、链表)就手忙脚乱。咱们先看普通循环的三大痛点:

  1. 遍历逻辑不统一 :遍历数组要记索引(i从0到length-1),遍历Set用forEach,遍历Map要forEachentries(),每种数据结构一套逻辑,记起来麻烦;
  2. 无法暂停与恢复:循环一旦启动就必须跑完,想在遍历中等待异步任务(比如遍历请求列表,前一个请求完成再发下一个)根本做不到;
  3. 普通对象不能直接遍历for...of能遍历数组/Set/Map,却不能直接遍历普通对象,得先转成Object.keys(obj),多此一举。

而迭代器和生成器的出现,就是为了解决这些问题:

  • 统一遍历逻辑:不管是数组、自定义数据结构,还是树/链表,都用for...of遍历,不用记不同语法;
  • 支持暂停恢复:遍历过程中能暂停,等待异步任务完成再继续,完美适配异步场景;
  • 让任意对象可遍历:给普通对象加个"遍历说明书",就能直接用for...of遍历。

二、迭代器:遍历的"基础协议"------像台"自动售货机"

迭代器的核心是"迭代器协议"------一个对象只要有next()方法,且返回{ value: 下一个值, done: 是否结束 },它就是迭代器。就像自动售货机:投币(调用next())→ 出商品(value)→ 售罄(done: true)。

1. 迭代器的核心规则

  • 必须有next()方法,无参数或一个参数;
  • next()返回对象必须包含done(布尔值),可选包含value
  • 迭代器是"一次性消耗"的:遍历到done: true后,再调用next(),永远返回{ done: true }

2. 手动实现迭代器:体验"售货机"的工作原理

咱们自定义一个"1~5的整数迭代器",手动实现迭代器协议,理解底层逻辑:

js 复制代码
// 自定义迭代器:生成1~5的整数
function createNumberIterator() {
  let current = 1;
  const max = 5;

  // 返回迭代器对象(符合迭代器协议)
  return {
    next() {
      if (current <= max) {
        return { value: current++, done: false };
      } else {
        return { done: true }; // 遍历结束,可省略value
      }
    }
  };
}

// 使用迭代器
const iterator = createNumberIterator();
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()); // { done: true }
console.log(iterator.next()); // { done: true }(已消耗,永远返回结束)

这个例子能直观看到:迭代器通过闭包维护current状态,每次next()推进状态,直到done: true

3. 迭代器的优势:支持无限序列

普通数组无法存储无限数据(比如自然数序列),但迭代器是"按需生成"的,能轻松实现无限序列:

js 复制代码
// 无限自然数迭代器
function createInfiniteIterator() {
  let current = 1;
  return {
    next() {
      return { value: current++, done: false }; // 永远不结束
    }
  };
}

const infiniteIt = createInfiniteIterator();
console.log(infiniteIt.next().value); // 1
console.log(infiniteIt.next().value); // 2
console.log(infiniteIt.next().value); // 3
// 想要多少要多少,不占额外内存

三、可迭代对象:能被for...of遍历的"合格数据"

迭代器是"售货机",但for...of不直接遍历迭代器,而是遍历"可迭代对象"------即拥有[Symbol.iterator]()方法的对象。这个方法调用后返回迭代器,相当于"售货机的说明书",for...of会自动按说明书获取迭代器,调用next()直到done: true

1. 内置可迭代对象

JS中数组、String、Set、Map、类型数组都是内置可迭代对象,因为它们的原型上有[Symbol.iterator]()方法:

js 复制代码
// 数组是可迭代对象
const arr = [1, 2, 3];
const arrIt = arr[Symbol.iterator](); // 获取迭代器
console.log(arrIt.next()); // { value: 1, done: false }

// for...of自动调用[Symbol.iterator](),遍历迭代器
for (const item of arr) {
  console.log(item); // 1、2、3
}

2. 让普通对象变成可迭代对象

普通对象没有[Symbol.iterator](),所以不能用for...of。咱们给它加个"说明书",让它变成可迭代对象:

js 复制代码
const user = {
  name: "张三",
  hobbies: ["篮球", "游戏", "美食"],
  // 实现[Symbol.iterator](),返回迭代器
  [Symbol.iterator]() {
    let index = 0;
    const hobbies = this.hobbies;
    return {
      next() {
        if (index < hobbies.length) {
          return { value: hobbies[index++], done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
};

// 现在user是可迭代对象,能被for...of遍历
for (const hobby of user) {
  console.log(hobby); // 篮球、游戏、美食
}

// 也能使用展开语法
const hobbyArr = [...user];
console.log(hobbyArr); // ["篮球", "游戏", "美食"]

3. 关键区别:迭代器 vs 可迭代对象

特性 迭代器 可迭代对象
核心标识 next()方法 [Symbol.iterator]()方法
作用 提供遍历的具体逻辑 提供迭代器的"创建说明书"
能否被for...of遍历 不能
例子 createNumberIterator()返回值 数组、Set、自定义user对象

四、生成器:简化迭代器的"智能导游"

手动实现迭代器需要维护状态(比如currentindex),麻烦且容易出错。生成器(Generator)是JS提供的"捷径"------用function*定义的函数,调用后返回生成器(同时是迭代器+可迭代对象),yield关键字实现暂停,自动维护状态,让迭代器创建变得超简单。

1. 生成器的核心语法

  • 函数定义:function* 函数名()(注意*);
  • 暂停标识:yield value(返回value给next(),暂停执行);
  • 调用生成器函数:返回生成器对象(不是执行函数体);
  • 生成器是迭代器:有next()方法,也有[Symbol.iterator]()(返回自身)。

2. 用生成器简化迭代器:一行顶十行

之前的"1~5整数迭代器",用生成器实现只要3行:

js 复制代码
// 生成器函数:生成1~5的整数
function* numberGenerator() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
}

// 调用生成器函数,返回生成器(迭代器)
const generator = numberGenerator();

// 生成器是迭代器,支持next()
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }

// 生成器是可迭代对象,支持for...of
for (const num of numberGenerator()) {
  console.log(num); // 1、2、3、4、5
}

更简洁的写法,用for...in或循环:

js 复制代码
// 生成1~max的整数生成器
function* rangeGenerator(start = 1, end, step = 1) {
  for (let i = start; i <= end; i += step) {
    yield i;
  }
}

// 遍历1~10,步长2
for (const num of rangeGenerator(1, 10, 2)) {
  console.log(num); // 1、3、5、7、9
}

3. 生成器的暂停与恢复:智能导游的"灵活路线"

yield的核心是"暂停执行",next()的核心是"恢复执行到下一个yield"。这个特性让生成器能实现"非连续执行",比如斐波那契数列:

js 复制代码
// 斐波那契数列生成器
function* fibGenerator(max = Infinity) {
  let a = 0, b = 1;
  while (b <= max) {
    yield b; // 暂停,返回b,下次从这里继续
    [a, b] = [b, a + b];
  }
}

// 遍历前5个斐波那契数
const fibIt = fibGenerator();
console.log(fibIt.next().value); // 1
console.log(fibIt.next().value); // 1
console.log(fibIt.next().value); // 2
console.log(fibIt.next().value); // 3
console.log(fibIt.next().value); // 5

五、高级用法:生成器的"进阶技能"

1. next()传参:暂停后调整状态

next()可以传参数,这个参数会成为上一个yield的返回值,实现"暂停后给生成器传指令":

js 复制代码
// 带参数的生成器:根据传入值调整步长
function* adjustGenerator(start = 1) {
  let step = 1;
  while (true) {
    // 接收next()传入的参数,作为yield的返回值
    const newStep = yield start;
    // 如果传了新步长,更新step
    if (newStep) step = newStep;
    start += step;
  }
}

const adjustIt = adjustGenerator(1);
console.log(adjustIt.next().value); // 1(第一次传参无效)
console.log(adjustIt.next(2).value); // 3(步长改为2:1+2)
console.log(adjustIt.next(3).value); // 6(步长改为3:3+3)
console.log(adjustIt.next(1).value); // 7(步长改为1:6+1)

2. throw():暂停时抛出异常

throw()方法给生成器抛出异常,异常会在当前暂停的yield处抛出,可在生成器内部捕获:

js 复制代码
function* errorGenerator() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } catch (err) {
    console.log("捕获异常:", err.message);
    yield "异常后继续执行";
  }
}

const errIt = errorGenerator();
console.log(errIt.next().value); // 1
errIt.throw(new Error("手动抛出异常")); // 捕获异常:手动抛出异常
console.log(errIt.next().value); // 异常后继续执行

3. return():提前终止生成器

return(value)让生成器立即返回{ value, done: true },后续next()都返回{ done: true }

js 复制代码
const gen = rangeGenerator(1, 5);
console.log(gen.next().value); // 1
console.log(gen.return("提前终止").value); // 提前终止
console.log(gen.next().done); // true

六、实战场景:迭代器与生成器的"用武之地"

1. 场景1:遍历自定义数据结构(树/链表)

迭代器适合遍历复杂数据结构,比如二叉树的中序遍历:

js 复制代码
// 二叉树节点
class TreeNode {
  constructor(val) {
    this.val = val;
    this.left = null;
    this.right = null;
  }
}

// 二叉树中序遍历生成器
function* inorderTraversal(root) {
  if (root) {
    yield* inorderTraversal(root.left); // 递归遍历左子树
    yield root.val; // 返回当前节点值
    yield* inorderTraversal(root.right); // 递归遍历右子树
  }
}

// 构建二叉树
const root = new TreeNode(1);
root.right = new TreeNode(2);
root.right.left = new TreeNode(3);

// 遍历二叉树
for (const val of inorderTraversal(root)) {
  console.log(val); // 1、3、2(中序遍历结果)
}

2. 场景2:异步迭代(依次执行异步任务)

生成器的暂停特性适合处理异步流程,比如依次请求多个接口,前一个成功再请求下一个:

js 复制代码
// 模拟异步请求
function fetchData(url) {
  return new Promise(resolve => {
    setTimeout(() => resolve(`数据:${url}`), 1000);
  });
}

// 异步生成器:依次请求接口
function* asyncGenerator(urls) {
  for (const url of urls) {
    const data = yield fetchData(url); // 暂停,等待Promise完成
    yield data; // 返回数据
  }
}

// 执行异步生成器
async function runAsyncGenerator() {
  const urls = ["url1", "url2", "url3"];
  const gen = asyncGenerator(urls);
  
  let result = gen.next();
  while (!result.done) {
    // 如果是Promise,等待其完成
    const value = await result.value;
    console.log(value);
    result = gen.next();
  }
}

runAsyncGenerator(); // 每隔1秒输出一个数据

3. 场景3:无限序列(按需生成,不占内存)

处理大数据量时,生成器按需生成数据,避免一次性加载所有数据导致内存溢出:

js 复制代码
// 生成100万以内的偶数(按需生成,不占内存)
function* evenGenerator(max = 1000000) {
  for (let i = 2; i <= max; i += 2) {
    yield i;
  }
}

// 遍历前10个偶数,后面的不生成
const evenIt = evenGenerator();
for (let i = 0; i < 10; i++) {
  console.log(evenIt.next().value); // 2、4、6...20
}

七、避坑指南:这些坑千万别踩

  1. 迭代器是一次性的 :遍历到done: true后,再调用next()也不会重置,需重新创建迭代器;
  2. 生成器不能重复迭代:一个生成器对象只能遍历一次,再次遍历需重新调用生成器函数;
  3. next()第一次传参无效 :第一次调用next()时,生成器还没执行到任何yield,传参不会被接收;
  4. 普通对象不是可迭代对象 :别直接用for...of遍历普通对象,需手动实现[Symbol.iterator]()
  5. yield只能在生成器函数内使用 :普通函数不能用yield,会报错。

八、总结:迭代器与生成器的核心价值

迭代器定义了"统一的遍历协议",让不同数据结构的遍历逻辑标准化;生成器简化了迭代器的创建,提供了"暂停/恢复"的强大特性,两者结合让JS的遍历能力从"手动开车"升级为"智能导游"。

核心价值总结:

  1. 统一遍历逻辑:for...of通吃所有可迭代对象,不用记多种遍历语法;
  2. 支持复杂场景:无限序列、异步迭代、自定义数据结构遍历,普通循环做不到;
  3. 优化性能:按需生成数据,避免一次性加载大数据导致的内存压力。

掌握它们,你就能从容应对复杂的遍历需求,写出更优雅、更高效的代码。

相关推荐
来两个炸鸡腿4 小时前
DW动手学大模型应用全栈开发 - (1)大模型应用开发应知必会
python·深度学习·学习·nlp
JS_GGbond4 小时前
当JS拷贝玩起了“俄罗斯套娃”:深拷贝与浅拷贝的趣味对决
前端·javascript
小徐不会敲代码~4 小时前
Vue3 学习2
前端·javascript·学习
我命由我123454 小时前
Python Flask 开发 - Flask 快速上手(Flask 最简单的案例、Flask 处理跨域、Flask 基础接口)
服务器·开发语言·后端·python·学习·flask·学习方法
m0_740043734 小时前
Vue2 语法糖简洁指南
前端·javascript·vue.js
深蓝海拓4 小时前
PySide6从0开始学习的笔记(二) 控件(Widget)之容器类控件
笔记·qt·学习·pyqt
_李小白4 小时前
【Android GLSurfaceView源码学习】第二天:GLSurfaceView深度分析
android·学习
摇滚侠5 小时前
Redis 零基础到进阶,Spring Boot 整合 Redis,笔记93-99
spring boot·redis·笔记
zhougl9965 小时前
区分__proto__和prototype
开发语言·javascript·原型模式