1. 题目与适用场景
日志聚合服务要按优先级依次读取内存数组、去重后的 Set 和分页生成器,消费者只需要前 100 条记录。请使用 Iterator.concat 合并这些 Iterable,保持惰性,说明无限数据源、异常和提前停止时如何释放底层迭代器。
2. 面试官考察点
- 能否区分 Iterable 与 Iterator,并知道
Iterator.concat要求参数实现 Iterable 协议。 - 能否说明返回值是新的惰性 Iterator,消费时才拉取各输入,且输入按顺序连接。
- 能否识别无限源会遮蔽其后的输入,以及参数列表必须有限且受引擎参数数量限制。
- 能否处理
return关闭、TypeError、兼容降级与不必要的数组物化。
3. 回答前需要澄清的问题
- 每个输入是可重复的 Iterable,还是已经推进过的单次 Iterator?
- 消费者需要流式接口,还是最终必须得到数组?
- 任一输入是否持有文件、网络或数据库游标,需要提前终止时关闭?
- 目标浏览器和 Node 版本是否原生提供
Iterator.concat,是否允许 polyfill?
4. 30 秒回答框架
我会先确认所有参数都实现 Symbol.iterator,再调用 Iterator.concat 返回一个惰性 Iterator。它按输入顺序逐个取得迭代器,只有消费者调用 next、take 或展开时才读取数据;take(100) 后停止拉取并触发底层关闭。无限输入放在末尾,否则后续输入永远不可达;旧运行时用生成器封装同样的惰性和关闭语义。
5. 分步骤深入解答
第一步:确认 Iterable 边界
Iterator.concat 接受的是 Iterable,而不只是拥有 next 的 Iterator。数组、Set、生成器对象都可传入;只有 Iterator 的对象需要先用 Iterator.from 包装。这个约束让 concat 可以取得并负责关闭当前输入的迭代器。
第二步:建立惰性合并
function* pages() {
yield { source: "page", id: 1 };
yield { source: "page", id: 2 };
}
const memory = [{ source: "memory", id: 1 }];
const unique = new Set([{ source: "set", id: 1 }, { source: "set", id: 2 }]);
const merged = Iterator.concat(memory, unique, pages());
const firstThree = merged.take(3).toArray();创建 merged 不会遍历任何输入;toArray 才开始消费。输出顺序是 memory、Set、pages,且不同输入中的值不会自动去重或转换。
第三步:理解消费和单次状态
返回的 Iterator 保存当前输入及其位置。一个消费者推进后,另一个消费者接手的是剩余值;需要重放时应重新取得各 Iterable 的 Iterator。若只想查看前 N 条,应在 concat 后使用 take,不要先展开成数组。
第四步:处理无限输入
任何输入可以是无限 Iterable,结果也会无限。若第一个输入无限,后续输入永远不会被读取;因此有限批次应排在前面,或在消费端设置 take、超时和取消边界。不要写 Iterator.concat(...infiniteIterables()),因为展开参数本身永远无法完成。
第五步:关闭、异常与兼容降级
当消费提前停止或抛出异常时,应让当前迭代器的 return 有机会执行,释放游标、文件句柄或网络连接。自定义迭代器可在 finally 中记录清理。缺少原生实现的运行时可用生成器实现按顺序 yield*,并补齐 return 传播;降级不能偷偷把所有输入复制到数组。
6. 高质量示范回答
我会校验参数是 Iterable,再用Iterator.concat创建惰性合并器,只有消费端调用take(100)或toArray时才读取输入。concat 按顺序取得各输入迭代器,输入中的值不自动去重;无限源若位于第一个位置会让后续源永远不可达。提前停止和异常路径要验证底层return已关闭资源。对旧运行时,我会用生成器保留惰性、顺序、单次消费和关闭语义,避免预先物化数组。
7. 常见错误
- 把只有
next的对象直接传入 → 抛出 TypeError → 用Iterator.from包装或实现Symbol.iterator。 - 误以为创建 concat 会立即执行 → 产生隐藏 I/O → 强调消费操作才拉取数据。
- 把无限 Iterable 放在第一个 → 后续输入永远不可达 → 调整顺序并设置
take或取消边界。 - 先用展开运算符合并 → 内存和等待时间失控 → 只在明确需要数组的边界调用
toArray。 - 忽略提前终止的
return→ 游标泄漏 → 用带计数的测试 Iterable 验证关闭。
8. 追问及应对
追问一:它和数组 concat 有什么区别?
数组 concat 立即处理已物化数组并返回数组;Iterator.concat 接受 Iterable,返回惰性 Iterator,可处理生成器和无限源。
追问二:为什么不能把所有 Iterator 都直接传入?
只有 Iterator 却不是 Iterable 时,concat 无法明确取得尚未访问输入的所有权和关闭责任。先用 Iterator.from 使其符合 Iterable 边界。
追问三:什么时候用 flatMap?
当输入本身是大量甚至无限的 Iterable 列表时,flatMap 可以逐项产生并展开,避免一次性展开参数列表;concat 更适合有限且已知的输入集合。
追问四:如何验证资源真的关闭?
写一个实现 next、return 的测试 Iterable,记录拉取次数和关闭次数;对 take、消费者中断及回调抛错分别断言停止拉取并执行 return。