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的整数迭代器”,手动实现迭代器协议,理解底层逻辑:

// 自定义迭代器:生成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. 迭代器的优势:支持无限序列

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

// 无限自然数迭代器
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]()方法:

// 数组是可迭代对象
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。咱们给它加个“说明书”,让它变成可迭代对象:

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行:

// 生成器函数:生成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或循环:

// 生成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”。这个特性让生成器能实现“非连续执行”,比如斐波那契数列:

// 斐波那契数列生成器
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的返回值,实现“暂停后给生成器传指令”:

// 带参数的生成器:根据传入值调整步长
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处抛出,可在生成器内部捕获:

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 }

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

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

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

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

// 二叉树节点
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:异步迭代(依次执行异步任务)

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

// 模拟异步请求
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:无限序列(按需生成,不占内存)

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

// 生成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. 优化性能:按需生成数据,避免一次性加载大数据导致的内存压力。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿蒙Armon

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值