【迭代器】js 迭代器与可迭代对象终极详解

目标:不仅会"用",还能"设计、调试、扩展、优化"。文内包含从零手写、生成器、惰性管道、异步流、资源管理、常见坑、性能建议、练习清单等。


1. 核心协议

  • 可迭代协议 (Iterable) :对象实现 obj[Symbol.iterator](),返回一个迭代器。
  • 迭代器协议 (Iterator) :返回值具备 next() 方法,每次 next() 返回 { value, done }
  • 消费方for...of、展开 ...、数组/对象解构、Promise.allnew Map(iterable)new Set(iterable)Array.from 等。
  • 原生可迭代ArrayStringMapSetTypedArrayargumentsNodeList 等。
js 复制代码
const arr = [10, 20];
const it = arr[Symbol.iterator](); // 拿到迭代器
console.log(it.next()); // { value: 10, done: false }
console.log(it.next()); // { value: 20, done: false }
console.log(it.next()); // { value: undefined, done: true }

2. for...of / for...in / for await...of 对比

  • for...of:遍历"值",依赖可迭代协议,顺序稳定。
  • for...in:遍历"可枚举属性键",含原型链可枚举属性,不需要可迭代。
  • for await...of:遍历"异步可迭代"或"值为 Promise 的可迭代",逐个 await
js 复制代码
const arr = [3, 6, 9];
for (const v of arr) console.log('of =>', v); // 3 6 9
for (const k in arr) console.log('in =>', k); // 0 1 2

3. 从零手写同步迭代器(含 return/throw)

场景:为自定义对象提供可迭代能力,并处理提前终止。

js 复制代码
const counter = {
  current: 1,
  max: 3,
  [Symbol.iterator]() {
    const self = this;
    return {
      next() {
        if (self.current <= self.max) {
          return { value: self.current++, done: false };
        }
        return { value: undefined, done: true };
      },
      return() {
        console.log('迭代被提前终止,执行清理逻辑');
        return { value: undefined, done: true };
      },
      throw(err) {
        console.log('外部抛错被捕获:', err.message);
        return { value: undefined, done: true };
      },
    };
  },
};

for (const n of counter) {
  console.log(n);
  if (n === 2) break; // 触发 return()
}

要点:

  • Symbol.iterator 返回的对象必须实现 next()
  • done: true 视为终止;value 可省略。
  • return() 可用于 break/return/throw 时的清理;throw() 让外部异常传入迭代器。

4. 生成器 (Generator) 深潜:function* / yield / yield*

生成器函数(function* / async function*)执行后返回一个"生成器对象",它同时是迭代器和可迭代对象。生成器以"暂停/恢复"的方式运行,内部编译成状态机。

4.1 生成器函数 vs 生成器对象

  • 生成器函数 :写法 function* foo() { ... }const foo = function* () { ... };箭头函数不能写成生成器。
  • 生成器对象 :调用生成器函数得到,如 const it = foo();它拥有 next/return/throw 并实现 Symbol.iterator
js 复制代码
function* range(start, end, step = 1) {
  for (let i = start; i <= end; i += step) yield i; // yield 产出,并"暂停"
}
const it = range(1, 3);
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }

4.2 yield 的双向通信与状态机

next(value) 会把 value 作为"上一个 yield 表达式的结果"传回生成器内部。

js 复制代码
function* dialog() {
  const name = yield '你是谁?';
  const lang = yield `你好,${name},你用什么语言?`;
  return `${name} 使用 ${lang}`;
}
const g = dialog();
console.log(g.next());           // { value: '你是谁?', done: false }
console.log(g.next('Alice'));    // { value: '你好,Alice,你用什么语言?', done: false }
console.log(g.next('JavaScript'));// { value: 'Alice 使用 JavaScript', done: true }

4.3 return / throw:主动收尾与异常注入

  • iter.return(value):立即终止,返回 { value, done: true },触发生成器内的 finally
  • iter.throw(err):将错误注入生成器,在内部可被 try/catch 捕获;若未捕获则向外抛出。
js 复制代码
function* work() {
  try {
    yield 1;
    yield 2;
  } finally {
    console.log('清理资源');
  }
}
const it2 = work();
console.log(it2.next());      // { value:1, done:false }
console.log(it2.return(99));  // 清理资源 -> { value:99, done:true }

4.4 yield*:委托/扁平化子迭代器,并可接收子迭代器的 return

yield* otherIterable 把"迭代控制权"交给子迭代器,等价于逐个 for...of 产出其值。yield* 的结果是子迭代器的 return 值。

js 复制代码
function* sub() {
  yield 1;
  yield 2;
  return 9; // 会被 yield* 捕获
}
function* parent() {
  const ret = yield* sub();   // 产出 1、2,并获得 ret=9
  yield ret;                  // 再产出 return 值
}
console.log([...parent()]); // [1, 2, 9]
yield* 应用:递归/扁平化/管道组合
js 复制代码
function* flatten(tree) {
  for (const node of tree) {
    if (Array.isArray(node)) yield* flatten(node); // 递归委托
    else yield node;
  }
}
console.log([...flatten([1, [2, [3, 4]], 5])]); // [1,2,3,4,5]

4.5 生成器的执行特性与调试要点

  • 惰性 :直到调用 next() 才会继续运行;适合大/无限序列。
  • 单次消费:同一个生成器对象不可复位,需重新创建。
  • 清理 :在生成器内部用 try/finally;外部可以 return() 触发。
  • 不可用箭头函数 :箭头语法不支持 function*,需常规函数写法。
  • 与 for...offor...of 自动反复 next() 直到 done:truebreak/throw/return 会触发迭代器的 return()

5. 可迭代工具箱与常见 API

  • 展开/解构:[...iterable]const [a, ...rest] = iterable
  • 集合转换:Array.from(iterable)new Map(iterable)new Set(iterable)
  • Promise 组合:Promise.all(iterable)Promise.allSettledPromise.race(需可迭代)
js 复制代码
const set = new Set([1, 2, 3]);
const arr = [...set]; // [1,2,3]
const [first, ...rest] = set; // first=1, rest=[2,3]

6. 自定义数据结构:可迭代的 Deque(类 + 私有字段)

js 复制代码
class Deque {
  #data = [];
  pushFront(x) { this.#data.unshift(x); }
  pushBack(x) { this.#data.push(x); }
  popFront() { return this.#data.shift(); }
  popBack() { return this.#data.pop(); }
  get size() { return this.#data.length; }

  [Symbol.iterator]() {
    let idx = 0;
    return {
      next: () =>
        idx < this.#data.length
          ? { value: this.#data[idx++], done: false }
          : { value: undefined, done: true },
      return() { return { done: true }; },
    };
  }
}

const dq = new Deque();
dq.pushBack(10); dq.pushFront(5); dq.pushBack(20);
for (const v of dq) console.log(v); // 5 10 20

设计建议:

  • 迭代期间若会修改内部存储,需明确顺序定义与终止条件(如记录快照或用生成器惰性遍历)。
  • 大数据/潜在无限序列优先用生成器,避免一次性展开耗内存。

7. 惰性管道:map / filter / take / drop

用生成器实现"按需取值"的流式组合。

js 复制代码
function* map(iterable, fn) {
  for (const x of iterable) yield fn(x);
}
function* filter(iterable, pred) {
  for (const x of iterable) if (pred(x)) yield x;
}
function* take(iterable, n) {
  if (n <= 0) return;
  let i = 0;
  for (const x of iterable) {
    yield x;
    if (++i >= n) break;
  }
}
function* drop(iterable, n) {
  let i = 0;
  for (const x of iterable) if (i++ >= n) yield x;
}

const src = [1, 2, 3, 4, 5, 6];
const pipeline = take(filter(map(src, x => x * 3), x => x % 2 === 0), 2);
console.log([...pipeline]); // [6, 12]

优势:逐元素计算,适合大数据、IO 流;可轻松扩展更多算子(zip、flatMap、chunk、uniq 等)。


8. 异步迭代器与 for await...of

异步可迭代实现 Symbol.asyncIteratornext() 返回 Promise,或用 async function*

js 复制代码
const asyncCounter = {
  current: 1,
  max: 3,
  async *[Symbol.asyncIterator]() {
    while (this.current <= this.max) {
      await new Promise(r => setTimeout(r, 100));
      yield this.current++;
    }
  },
};

(async () => {
  for await (const n of asyncCounter) console.log(n);
})();

典型场景:分页 API、网络流(ReadableStream)、文件流、数据库游标、消息队列。

同步可迭代 + Promise 元素

for await...of 也能遍历"同步可迭代且元素为 Promise"的情况:

js 复制代码
const xs = [1, 2, 3].map(v => Promise.resolve(v * 10));
(async () => {
  for await (const v of xs) console.log(v); // 10 20 30
})();

9. 资源管理与提前终止

在生成器中用 try/finally + return() 保障资源释放。

js 复制代码
function* readChunks(reader) {
  try {
    while (true) {
      const chunk = reader.read();
      if (!chunk) break;
      yield chunk;
    }
  } finally {
    reader.close(); // 即便 break/throw 也会执行
  }
}

在异步生成器中同理使用 try/finally

js 复制代码
async function* streamLines(stream) {
  try {
    for await (const line of stream) yield line;
  } finally {
    stream.destroy?.();
  }
}

10. 常见坑排查表(含错误示例)

  • TypeError: object is not iterable:缺少 Symbol.iterator 或拼写错误。
  • 迭代器复用:多数迭代器是一次性的,复用要重新获取 obj[Symbol.iterator]()
  • for...ofbreak/throw 却未清理资源:实现 return() 或在生成器用 finally
  • for...of 误用在异步迭代器:应改 for await...of
  • 隐式耗尽:[...iter] 会一次性拉平,若是大数据/无限序列会卡死或 OOM;改用惰性消费。
  • 顺序期待:Set/Map 保持插入顺序;普通对象属性遍历顺序有规则但不属"可迭代"。

调试技巧:

js 复制代码
const it = someIterable[Symbol.iterator]();
console.log(it.next(), it.next()); // 手动探查序列

11. 性能与工程化建议

  • 惰性优先:未知大小或可能无限的来源用生成器/异步生成器。
  • 避免重复遍历 :对昂贵来源(IO/计算)避免多次消费,可缓存结果或暴露 toArray()
  • 批量/背压 :异步流中可结合 takechunkthrottle 控制节奏。
  • 类型提示 :在 TS 中为迭代器声明泛型,避免 any 扩散。
  • 组合优先:map/filter/take/drop/flatMap/zip 等算子小而精,利于单测和重用。
  • 清理保证 :生成器里用 try/finally;显式实现 return() 以防资源泄漏。

12. 典型模式示例

12.1 管道式数据流

js 复制代码
function* flatMap(iterable, fn) {
  for (const x of iterable) {
    const res = fn(x);
    if (Symbol.iterator in Object(res)) yield* res;
    else yield res;
  }
}

const words = ['hi', 'js'];
const chars = flatMap(words, w => w.split(''));
console.log([...chars]); // ['h','i','j','s']

12.2 无限序列 + take 限流

js 复制代码
function* naturals() { let i = 1; while (true) yield i++; }
console.log([...take(naturals(), 5)]); // [1,2,3,4,5]

12.3 异步分页封装

js 复制代码
async function* fetchPages(fetchPage) {
  let page = 1;
  while (true) {
    const data = await fetchPage(page);
    if (!data.length) break;
    yield data;
    page += 1;
  }
}

(async () => {
  for await (const page of fetchPages(p => api.list({ page: p }))) {
    console.log('page size', page.length);
  }
})();

12.4 具备回收的文件读取(Node)

js 复制代码
const fs = require('fs');
async function* readLines(path) {
  const stream = fs.createReadStream(path, 'utf8');
  try {
    for await (const chunk of stream) yield chunk;
  } finally {
    stream.close();
  }
}

13. FAQ 精要

  • 何时用生成器 vs 普通函数? 需要"逐步产出/惰性/可中断/可组合"时用生成器。
  • 迭代器能重置吗? 原生多数不可;若需可重复遍历,应在 Symbol.iterator 中返回"新的迭代器实例"。
  • 如何判断对象可迭代? obj != null && typeof obj[Symbol.iterator] === 'function'
  • async 迭代器如何并行? 迭代本身是串行消费;并行可在内部批量启动 Promise,再逐个 yield 结果(注意背压)。
  • 能否在生成器里用 await? 不能,改用 async function* 或在外层 for await...of

15. 结语

迭代器与可迭代协议为 JS 提供统一、可组合的访问抽象;生成器/异步生成器进一步让"惰性、流式、可中断"变得自然。工程落地时,请同时关注资源释放、背压、可测试性与性能可观测性,把迭代封装成可靠的基础设施。

相关推荐
Fantastic_sj2 小时前
[代码例题] var 和 let 在循环中的作用域差异,以及闭包和事件循环的影响
开发语言·前端·javascript
HashTang2 小时前
【AI 编程实战】第 3 篇:后端小白也能写 API:AI 带我 1 小时搭完 Next.js 服务
前端·后端·ai编程
三年三月2 小时前
React 中 CSS Modules 详解
前端·css
JANG10242 小时前
【Linux】常用指令
linux·服务器·javascript
粉末的沉淀2 小时前
tauri:关闭窗口后最小化到托盘
前端·javascript·vue.js
赵庆明老师2 小时前
NET 使用SmtpClient 发送邮件
java·服务器·前端
绝世唐门三哥3 小时前
使用Intersection Observer js实现超出视口固定底部按钮
开发语言·前端·javascript
南山安3 小时前
Vue学习:ref响应式数据、v-指令、computed
javascript·vue.js·面试
思茂信息3 小时前
CST电动车EMC仿真——电机控制器MCU滤波仿真
javascript·单片机·嵌入式硬件·cst·电磁仿真