1. 理解 JavaScript 中的迭代协议
在 JavaScript 中,要让一个对象变得可迭代,我们需要先理解迭代协议(Iteration Protocols)。迭代协议并不是某种具体的 API,而是 ECMAScript 2015(ES6)引入的一组约定,包括可迭代协议(Iterable Protocol)和迭代器协议(Iterator Protocol)。
1.1 可迭代协议
可迭代协议要求对象必须实现 @@iterator 方法。在代码中,我们通过 Symbol.iterator 这个特殊属性来访问这个方法。当一个对象实现了 Symbol.iterator 方法时,我们就说这个对象是可迭代的。
javascript复制const iterableObject = {
[Symbol.iterator]() {
// 返回一个迭代器对象
}
}
1.2 迭代器协议
迭代器协议定义了如何从一个数据源中产生一系列值。一个迭代器对象必须实现 next() 方法,这个方法返回一个包含两个属性的对象:
value:当前迭代的值done:布尔值,表示迭代是否完成
javascript复制const iterator = {
next() {
return {
value: 'some value',
done: false
}
}
}
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实现自定义可迭代对象
2.1 基本实现
让我们从一个简单的例子开始,创建一个可迭代的范围对象:
javascript复制class Range {
constructor(start, end, step = 1) {
this.start = start
this.end = end
this.step = step
}
[Symbol.iterator]() {
let current = this.start
const end = this.end
const step = this.step
return {
next() {
if ((step > 0 && current <= end) || (step < 0 && current >= end)) {
const value = current
current += step
return { value, done: false }
}
return { done: true }
}
}
}
}
// 使用示例
const range = new Range(1, 5)
for (const num of range) {
console.log(num) // 输出 1, 2, 3, 4, 5
}
2.2 更复杂的例子:树形结构迭代
对于更复杂的数据结构,比如树,我们可以实现不同的迭代方式:
javascript复制class TreeNode {
constructor(value, children = []) {
this.value = value
this.children = children
}
*[Symbol.iterator]() {
yield this.value
for (const child of this.children) {
yield* child
}
}
}
// 使用示例
const tree = new TreeNode('root', [
new TreeNode('child1', [
new TreeNode('grandchild1'),
new TreeNode('grandchild2')
]),
new TreeNode('child2')
])
for (const value of tree) {
console.log(value)
// 输出: root, child1, grandchild1, grandchild2, child2
}
3. 生成器函数简化迭代器实现
ES6 的生成器函数(Generator Function)可以大大简化迭代器的实现。生成器函数返回一个生成器对象,这个对象同时符合可迭代协议和迭代器协议。
3.1 使用生成器重构 Range 类
javascript复制class Range {
constructor(start, end, step = 1) {
this.start = start
this.end = end
this.step = step
}
*[Symbol.iterator]() {
let current = this.start
while ((this.step > 0 && current <= this.end) ||
(this.step < 0 && current >= this.end)) {
yield current
current += this.step
}
}
}
3.2 无限序列生成器
生成器特别适合表示无限序列:
javascript复制function* fibonacci() {
let [prev, curr] = [0, 1]
while (true) {
yield curr
;[prev, curr] = [curr, prev + curr]
}
}
const fib = fibonacci()
console.log(fib.next().value) // 1
console.log(fib.next().value) // 1
console.log(fib.next().value) // 2
// 可以无限继续...
4. 内置可迭代对象与迭代器
JavaScript 中有许多内置的可迭代对象:
4.1 数组、字符串、Map、Set
这些内置对象都实现了 Symbol.iterator 方法:
javascript复制const arr = [1, 2, 3]
const arrIterator = arr[Symbol.iterator]()
console.log(arrIterator.next()) // { value: 1, done: false }
console.log(arrIterator.next()) // { value: 2, done: false }
console.log(arrIterator.next()) // { value: 3, done: false }
console.log(arrIterator.next()) // { done: true }
4.2 函数的 arguments 对象
即使在 ES5 中不可迭代的 arguments 对象,在 ES6+ 环境中也变得可迭代:
javascript复制function test() {
for (const arg of arguments) {
console.log(arg)
}
}
test(1, 2, 3) // 输出 1, 2, 3
5. 迭代器的实用技巧
5.1 提前终止迭代
有些迭代器可能实现 return() 方法,当迭代提前终止时(比如 break 或抛出错误),这个方法会被调用:
javascript复制const iterable = {
[Symbol.iterator]() {
let count = 0
return {
next() {
count++
return { value: count, done: false }
},
return() {
console.log('迭代提前终止')
return { done: true }
}
}
}
}
for (const num of iterable) {
console.log(num)
if (num > 3) break
}
// 输出: 1, 2, 3, 4, "迭代提前终止"
5.2 组合迭代器
我们可以创建组合迭代器来合并多个可迭代对象:
javascript复制function* chain(...iterables) {
for (const iterable of iterables) {
yield* iterable
}
}
const combined = chain([1, 2], new Set([3, 4]), 'hello')
console.log([...combined]) // [1, 2, 3, 4, 'h', 'e', 'l', 'l', 'o']
6. 异步迭代
ES2018 引入了异步迭代器和 for-await-of 循环:
6.1 异步迭代协议
异步迭代器与普通迭代器类似,但 next() 方法返回一个 Promise:
javascript复制const asyncIterable = {
[Symbol.asyncIterator]() {
let i = 0
return {
async next() {
await new Promise(resolve => setTimeout(resolve, 100))
if (i < 3) {
return { value: i++, done: false }
}
return { done: true }
}
}
}
}
;(async function() {
for await (const num of asyncIterable) {
console.log(num) // 0, 1, 2 (每隔100ms输出一个)
}
})()
6.2 实际应用:分页数据获取
异步迭代器非常适合处理分页 API:
javascript复制async function* paginatedFetch(url) {
let page = 1
let hasMore = true
while (hasMore) {
const response = await fetch(`${url}?page=${page}`)
const data = await response.json()
yield* data.items
hasMore = data.hasMore
page++
}
}
// 使用示例
;(async () => {
for await (const item of paginatedFetch('/api/data')) {
console.log(item)
}
})()
7. 常见问题与解决方案
7.1 对象不是可迭代的错误
当尝试对普通对象使用 for...of 时,会遇到错误:
javascript复制const obj = { a: 1, b: 2 }
for (const key of obj) {} // TypeError: obj is not iterable
解决方案1:使用 Object.keys()、Object.values() 或 Object.entries()
javascript复制for (const key of Object.keys(obj)) {
console.log(key, obj[key])
}
解决方案2:实现自定义迭代器
javascript复制obj[Symbol.iterator] = function* () {
for (const key in this) {
if (this.hasOwnProperty(key)) {
yield [key, this[key]]
}
}
}
for (const [key, value] of obj) {
console.log(key, value)
}
7.2 迭代器状态问题
迭代器是有状态的,一旦消耗就无法重用:
javascript复制const arr = [1, 2, 3]
const iterator = arr[Symbol.iterator]()
console.log([...iterator]) // [1, 2, 3]
console.log([...iterator]) // [] (已经消耗完了)
解决方案:每次需要迭代时获取新的迭代器
javascript复制function getFreshIterator(iterable) {
return iterable[Symbol.iterator]()
}
console.log([...getFreshIterator(arr)]) // [1, 2, 3]
console.log([...getFreshIterator(arr)]) // [1, 2, 3]
7.3 性能考虑
对于大型数据集,惰性求值的迭代器比预先计算所有值的数组更高效:
javascript复制function* bigDataGenerator() {
for (let i = 0; i < 1e6; i++) {
yield i
}
}
// 内存高效,因为一次只处理一个值
for (const num of bigDataGenerator()) {
if (num > 10) break
console.log(num)
}
8. 实际应用场景
8.1 自定义数据结构
实现链表、图等数据结构的迭代:
javascript复制class LinkedList {
constructor(value, next = null) {
this.value = value
this.next = next
}
*[Symbol.iterator]() {
let current = this
while (current) {
yield current.value
current = current.next
}
}
}
const list = new LinkedList(1, new LinkedList(2, new LinkedList(3)))
console.log([...list]) // [1, 2, 3]
8.2 状态机
用迭代器实现状态机:
javascript复制function* trafficLight() {
while (true) {
yield 'red'
yield 'yellow'
yield 'green'
yield 'yellow'
}
}
const light = trafficLight()
console.log(light.next().value) // 'red'
console.log(light.next().value) // 'yellow'
console.log(light.next().value) // 'green'
8.3 数据处理管道
组合多个迭代器创建数据处理管道:
javascript复制function* map(iterable, fn) {
for (const item of iterable) {
yield fn(item)
}
}
function* filter(iterable, predicate) {
for (const item of iterable) {
if (predicate(item)) {
yield item
}
}
}
const numbers = [1, 2, 3, 4, 5]
const result = filter(
map(numbers, x => x * 2),
x => x > 5
)
console.log([...result]) // [6, 8, 10]
9. 高级技巧
9.1 迭代器委托
使用 yield* 委托给另一个迭代器:
javascript复制function* flatten(iterable) {
for (const item of iterable) {
if (typeof item[Symbol.iterator] === 'function') {
yield* flatten(item)
} else {
yield item
}
}
}
const nested = [1, [2, [3, 4], 5]]
console.log([...flatten(nested)]) // [1, 2, 3, 4, 5]
9.2 迭代器适配器
创建可重用的迭代器适配器:
javascript复制function take(iterable, n) {
return {
[Symbol.iterator]() {
const iterator = iterable[Symbol.iterator]()
let count = 0
return {
next() {
if (count++ < n) {
return iterator.next()
}
return { done: true }
}
}
}
}
}
const numbers = [1, 2, 3, 4, 5]
console.log([...take(numbers, 3)]) // [1, 2, 3]
9.3 观察迭代过程
创建一个可以观察迭代过程的代理:
javascript复制function spyIterable(iterable, callback) {
return {
[Symbol.iterator]() {
const iterator = iterable[Symbol.iterator]()
return {
next() {
const result = iterator.next()
callback(result)
return result
}
}
}
}
}
const arr = [1, 2, 3]
const spied = spyIterable(arr, result => {
console.log('迭代步骤:', result)
})
console.log([...spied])
// 输出:
// 迭代步骤: { value: 1, done: false }
// 迭代步骤: { value: 2, done: false }
// 迭代步骤: { value: 3, done: false }
// 迭代步骤: { done: true }
// [1, 2, 3]
10. 测试与调试
10.1 验证对象是否可迭代
javascript复制function isIterable(obj) {
return obj != null && typeof obj[Symbol.iterator] === 'function'
}
console.log(isIterable([])) // true
console.log(isIterable({})) // false
console.log(isIterable('hello')) // true
10.2 自定义迭代器的单元测试
使用 Jest 测试自定义迭代器:
javascript复制describe('Range', () => {
test('应该正确迭代范围内的数字', () => {
const range = new Range(1, 3)
expect([...range]).toEqual([1, 2, 3])
})
test('应该处理反向范围', () => {
const range = new Range(3, 1, -1)
expect([...range]).toEqual([3, 2, 1])
})
test('应该处理空范围', () => {
const range = new Range(1, 0)
expect([...range]).toEqual([])
})
})
10.3 性能测试
比较不同迭代方式的性能:
javascript复制const bigArray = Array(1e6).fill().map((_, i) => i)
console.time('for loop')
for (let i = 0; i < bigArray.length; i++) {
const item = bigArray[i]
}
console.timeEnd('for loop')
console.time('for...of')
for (const item of bigArray) {
}
console.timeEnd('for...of')
console.time('forEach')
bigArray.forEach(item => {})
console.timeEnd('forEach')
11. 浏览器兼容性与转译
11.1 兼容性考虑
Symbol.iterator 和 for...of 在以下环境中支持:
- Chrome 38+
- Firefox 13+
- Edge 12+
- Safari 7+
- Node.js 0.12+
对于旧版浏览器,需要使用 Babel 等转译工具。
11.2 Babel 配置
在 .babelrc 中确保包含 @babel/plugin-transform-for-of:
json复制{
"plugins": [
"@babel/plugin-transform-for-of"
]
}
11.3 替代方案
在不支持 Symbol 的环境中,可以使用自定义方法名:
javascript复制function createLegacyIterable(obj, iteratorFactory) {
obj['@@iterator'] = iteratorFactory
return obj
}
const legacyIterable = createLegacyIterable({}, function() {
let i = 0
return {
next: function() {
return i < 3 ? { value: i++ } : { done: true }
}
}
})
// 使用自定义迭代辅助函数
function iterate(iterable) {
const iterator = iterable['@@iterator']()
let result
while (!(result = iterator.next()).done) {
console.log(result.value)
}
}
iterate(legacyIterable) // 0, 1, 2
12. 与其他语言特性的结合
12.1 解构赋值
可迭代对象可以直接用于解构:
javascript复制const [first, second] = new Range(1, 5)
console.log(first, second) // 1, 2
12.2 扩展运算符
可迭代对象可以使用扩展运算符:
javascript复制const arr = [...new Range(1, 3)]
console.log(arr) // [1, 2, 3]
12.3 Array.from()
Array.from() 可以接受任何可迭代对象:
javascript复制const set = new Set([1, 2, 3])
const arr = Array.from(set)
console.log(arr) // [1, 2, 3]
12.4 Promise.all()
当处理可迭代的 Promise 时:
javascript复制const promises = [
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
]
Promise.all(promises).then(values => {
console.log(values) // [1, 2, 3]
})
13. 设计模式中的应用
13.1 迭代器模式
经典的迭代器模式实现:
javascript复制class Collection {
constructor(items = []) {
this.items = items
}
createIterator() {
let index = 0
const items = this.items
return {
next() {
return index < items.length
? { value: items[index++], done: false }
: { done: true }
}
}
}
}
const collection = new Collection([1, 2, 3])
const iterator = collection.createIterator()
let result = iterator.next()
while (!result.done) {
console.log(result.value)
result = iterator.next()
}
13.2 观察者模式与迭代器结合
javascript复制class Observable {
constructor() {
this.observers = []
}
subscribe(observer) {
this.observers.push(observer)
return {
unsubscribe: () => {
this.observers = this.observers.filter(obs => obs !== observer)
}
}
}
*[Symbol.iterator]() {
for (const observer of this.observers) {
yield observer
}
}
}
const observable = new Observable()
const subscription1 = observable.subscribe(console.log)
const subscription2 = observable.subscribe(console.warn)
for (const observer of observable) {
observer('通知所有观察者')
}
14. 性能优化技巧
14.1 避免在热代码路径中创建迭代器
javascript复制// 不推荐 - 每次循环都创建新迭代器
function sumBad(iterable) {
let total = 0
for (const item of iterable) {
for (const innerItem of iterable) {
total += item + innerItem
}
}
return total
}
// 推荐 - 转换为数组一次
function sumGood(iterable) {
const arr = [...iterable]
let total = 0
for (const item of arr) {
for (const innerItem of arr) {
total += item + innerItem
}
}
return total
}
14.2 惰性求值的好处
javascript复制function* bigData() {
console.log('开始生成数据')
for (let i = 0; i < 1e6; i++) {
yield i
}
console.log('结束生成数据')
}
// 不会立即执行,只有在需要时才生成数据
const data = bigData()
// 只取前3个值
const firstThree = []
for (let i = 0; i < 3; i++) {
firstThree.push(data.next().value)
}
console.log(firstThree)
// 输出: "开始生成数据", [0, 1, 2]
// 不会输出 "结束生成数据"
14.3 内存效率比较
javascript复制// 预先计算所有值 - 高内存使用
function getAllNumbers(max) {
const result = []
for (let i = 0; i < max; i++) {
result.push(i)
}
return result
}
// 惰性生成 - 低内存使用
function* generateNumbers(max) {
for (let i = 0; i < max; i++) {
yield i
}
}
// 测试内存使用
const test = (creator, max) => {
console.time('memory')
const data = creator(max)
for (const num of data) {
if (num > 10) break
}
console.timeEnd('memory')
}
test(getAllNumbers, 1e6) // 较高内存使用
test(generateNumbers, 1e6) // 较低内存使用
15. 最佳实践总结
-
优先使用生成器函数:相比手动实现迭代器协议,生成器函数更简洁、更不易出错。
-
考虑惰性求值:对于大型数据集,惰性求值的迭代器比预先计算所有值更高效。
-
明确迭代语义:确保你的迭代器清晰地表达它迭代的是什么(键、值还是条目)。
-
处理清理逻辑:如果迭代器持有需要释放的资源,实现
return()方法。 -
保持不可变性:迭代通常不应该修改底层集合。
-
文档化迭代行为:在文档中明确说明你的对象是可迭代的,以及它迭代的内容和顺序。
-
考虑兼容性:如果需要支持旧环境,提供回退方案或使用转译工具。
-
性能敏感场景慎用:在性能关键的代码中,传统的
for循环可能比for...of更快。 -
利用组合:创建小的、可组合的迭代器工具函数,而不是庞大的迭代器实现。
-
测试迭代行为:确保你的迭代器在各种情况下表现正确,包括提前终止。
