🔁 JavaScript中的迭代全攻略 - for/while/迭代器/生成器/for await...of详解

105 阅读7分钟

🎯 学习目标:全面掌握 JavaScript 的迭代语句与迭代协议,能在不同数据结构与同步/异步场景下选择正确的遍历方式,并写出高性能、可维护的代码。

📊 难度等级:中级
🏷️ 技术标签:#JavaScript #迭代器 #生成器 #for-of #for-in #for await...of
⏱️ 阅读时间:约9分钟


🌟 引言

在日常 JavaScript 开发中,遍历数据“看起来都差不多”,但一旦涉及对象、Map/Set、类数组、异步数据流、性能优化,就容易踩坑:

  • 数组用 for...in 导致顺序和性能问题;
  • 把对象当可迭代结构用 for...of 直接报错;
  • 自定义迭代器和生成器不会用,错过优雅的惰性计算;
  • 大数据流用 for await...of 才是正确姿势,却不熟悉协议细节。

今天我用 7 个核心技巧,系统讲清“语句与协议”的边界与最佳实践,帮你写出更高效、可控的迭代代码。


💡 核心技巧详解

1. for vs while:基础循环的选择

🔍 应用场景

索引驱动的数组遍历、需要精细控制起止与步长、或在条件驱动下执行循环。

❌ 常见问题

while 忘记更新计数器,或数组越界导致死循环/错误。

// ❌ 计数器更新缺失可能导致死循环
let i = 0;
while (i < 3) {
  console.log('loop:', i);
  // 缺失 i++
}

✅ 推荐方案

/**
 * 使用 for 循环安全遍历数组
 * @param {any[]} list - 任意元素数组
 * @returns {any[]} 同步收集的结果
 */
const traverseWithFor = (list) => {
  const results = [];
  for (let i = 0; i < list.length; i++) {
    // 索引明确,性能稳定
    results.push(list[i]);
  }
  return results;
};

/**
 * 使用 while 循环在条件驱动下遍历
 * @param {number} start - 起始值
 * @param {number} end - 结束值(不含)
 * @returns {number[]} 生成的序列
 */
const traverseWithWhile = (start, end) => {
  const seq = [];
  let i = start;
  while (i < end) {
    seq.push(i);
    i += 1; // 确保条件推进
  }
  return seq;
};

💡 核心要点

  • for 适合索引明确的顺序遍历;
  • while 适合条件驱动的循环,注意推进条件;
  • 数据量大时优先 for,可微调步长与边界以获得稳定性能。

2. for...of vs for...in:遍历语义不要混用

🔍 应用场景

for...of 用于“可迭代对象”(数组、字符串、Map、Set、生成器等);for...in 用于对象的“可枚举属性键”。

❌ 常见问题

对数组使用 for...in 导致遍历到原型属性、顺序不稳定;对普通对象使用 for...of 直接报错。

// ❌ 对数组使用 for...in(遍历索引字符串,顺序可能受影响)
const arr = [10, 20, 30];
for (const k in arr) {
  console.log('index string:', k); // '0', '1', '2'
}

// ❌ 对对象使用 for...of(非可迭代,抛错)
// for (const v of { a: 1 }) {} // TypeError: {} is not iterable

✅ 推荐方案

/**
 * 遍历可迭代对象(数组/字符串/Map/Set)
 * @param {Iterable<any>} iterable - 可迭代对象
 * @returns {any[]} 收集到的值
 */
const collectIterableValues = (iterable) => {
  const values = [];
  for (const v of iterable) {
    values.push(v);
  }
  return values;
};

/**
 * 遍历对象自有可枚举属性键
 * @param {Record<string, any>} obj - 普通对象
 * @returns {string[]} 键列表
 */
const collectOwnKeys = (obj) => {
  const keys = [];
  for (const k in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, k)) keys.push(k);
  }
  return keys;
};

💡 核心要点

  • for...of 读取“值”;for...in 读取“键”;
  • 对象遍历更推荐 Object.keys/values/entries 保持可控与可读;
  • 避免对数组使用 for...in,不稳定且性能较差。

3. 迭代协议:可迭代与迭代器

🔍 应用场景

自定义数据结构的迭代行为,支持惰性求值与可控遍历。

✅ 推荐方案

/**
 * 自定义可迭代范围 [start, end)
 * @param {number} start - 起始
 * @param {number} end - 结束(不含)
 * @returns {Iterable<number>} 可迭代范围
 */
const createRange = (start, end) => ({
  [Symbol.iterator]: () => {
    let i = start;
    return {
      next: () => (i < end ? { value: i++, done: false } : { value: undefined, done: true })
    };
  }
});

💡 核心要点

  • 可迭代对象需实现 [Symbol.iterator] 返回迭代器;
  • 迭代器需实现 next() 返回 { value, done }
  • 惰性迭代避免一次性创建大量中间数据,利于性能与内存。

4. 生成器(Generator):更优雅的迭代器写法

🔍 应用场景

用更简洁的语法生成迭代序列,支持 yield 惰性输出与中断。

/**
 * 生成器创建斐波那契序列(前 n 个)
 * @param {number} n - 个数
 * @returns {Iterable<number>} 斐波那契序列
 */
const fibonacci = function* (n) {
  let a = 0, b = 1, i = 0;
  while (i < n) {
    yield a;
    [a, b] = [b, a + b];
    i += 1;
  }
};

💡 核心要点

  • 生成器本质是迭代器,语法更简洁;
  • yield 提供惰性产生值的能力;
  • 可搭配 for...of 直接遍历。

5. 异步迭代与 for await...of:数据流的正确遍历

🔍 应用场景

分页加载、流式文件读写、网络请求批次处理等异步数据源。

/**
 * 异步生成器:模拟批次拉取数据
 * @param {number} batches - 批次数
 * @returns {AsyncIterable<number>} 异步可迭代数据
 */
const fetchBatches = async function* (batches) {
  for (let i = 1; i <= batches; i++) {
    await new Promise((r) => setTimeout(r, 5));
    yield i; // 每批返回一个结果
  }
};

/**
 * 使用 for await...of 收集异步迭代结果
 * @param {AsyncIterable<any>} asyncIterable - 异步可迭代
 * @returns {Promise<any[]>} 收集到的值
 */
const collectAsync = async (asyncIterable) => {
  const out = [];
  for await (const chunk of asyncIterable) {
    out.push(chunk);
  }
  return out;
};

💡 核心要点

  • for await...of 遍历 AsyncIterable/AsyncGenerator
  • 避免将 Promise[] 直接用 for await...of,应先 Promise.all 或转为异步迭代;
  • 异步迭代是处理大数据流的内存友好方案。

6. 遍历不同数据结构:Map/Set/字符串/类数组

/**
 * 遍历 Map/Set/字符串/类数组的统一收集器
 * @param {any} target - 目标数据结构
 * @returns {any[]} 收集到的值
 */
const collectValues = (target) => {
  if (target instanceof Map) return Array.from(target.entries());
  if (target instanceof Set) return Array.from(target.values());
  if (typeof target === 'string') return Array.from(target);
  if (typeof target.length === 'number') return Array.from(target); // NodeList/HTMLCollection
  return [];
};

💡 核心要点

  • Map 默认遍历 [key, value]Set 遍历值;
  • 字符串是可迭代对象,可被 for...of 按字符遍历;
  • 类数组(如 NodeList)可用 Array.from 转正,避免 for...in

7. 性能与边界:选择正确的迭代策略

🎯 实战建议

  • 大数组性能更稳定的通常是索引 for
  • 需要可读性与语义清晰,优先 for...of
  • 对象遍历优先 Object.keys/entries,避免 for...in 的原型链干扰;
  • 大数据流与分页拉取用 for await...of,避免一次性内存爆炸;
  • 迭代中修改容器(增删元素)要谨慎,优先生成快照或用惰性策略。

📊 技巧对比总结

技巧使用场景优势注意事项
for索引驱动数组性能稳定,可控注意边界与步长
while条件驱动循环灵活防止死循环
for...of可迭代对象语义清晰不适用于普通对象
for...in对象键遍历简单避免用于数组,过滤原型链
迭代协议自定义结构惰性、可控正确实现 next()
生成器简洁迭代器语法优雅正确使用 yield
for await...of异步数据流内存友好仅用于异步可迭代

🎯 实战应用建议

最佳实践

  1. 使用 for...of 遍历集合类型,提升语义与可读性。
  2. 对象遍历使用 Object.entriesfor...of 组合处理键值对。
  3. 大数据流统一封装为 AsyncGenerator,用 for await...of 消费。
  4. 根据性能需求选择 forfor...of,避免对数组使用 for...in

性能考虑

  • 大数组在热路径中优先 for
  • 遍历中避免闭包捕获大对象,及时释放引用;
  • 生成器/异步生成器的惰性策略可显著降低内存峰值。

💡 总结

这 7 个迭代技巧覆盖了“语句与协议”的完整谱系:从 for/whilefor...of/for...in,再到迭代器/生成器与异步迭代。掌握它们后,你的代码将:

  1. 更语义化,遍历意图清晰;
  2. 更高性能,避免不必要的中间数据与错误用法;
  3. 更易维护,统一封装迭代行为并复用。

🔗 相关资源


💡 今日收获:理解迭代语句与协议的边界,选择合适的遍历方式,写出可读、可控、性能稳定的迭代代码。

如果这篇文章对你有帮助,欢迎点赞、收藏和分享!有任何问题也欢迎在评论区讨论。 🚀