在 JavaScript 中,数组是最常用的数据结构之一,而迭代器为数组的遍历提供了统一且可扩展的抽象。无论是 for...of 循环、展开语法还是解构赋值,背后都依赖数组默认实现的可迭代协议。理解如何从数组创建迭代器,不仅能帮助我们深入认识语言运行机制,也能在需要自定义遍历行为时提供更灵活的控制手段。

一、迭代器协议与数组的可迭代本质
要理解数组迭代器,必须先弄清楚 JavaScript 中的两个核心概念:可迭代协议和迭代器协议。可迭代协议要求一个对象实现 Symbol.iterator 方法,该方法被调用后必须返回一个迭代器对象。而迭代器协议要求该对象必须包含一个名为 next 的方法,每次调用 next 都会返回一个形如 { value: '元素值', done: false } 的结果对象。当 done 为 false 时表示仍有数据可取,当 done 变为 true 时表示遍历结束,此时 value 通常为 undefined。
数组的原型上天然实现了 Array.prototype[Symbol.iterator],因此每一个数组实例都具备可迭代能力。我们通常不会手动调用这个底层方法,因为 for...of、展开运算符等语法已经替我们完成了调用流程。不过,显式获取数组迭代器可以让我们更细致地控制遍历过程。例如下面的代码演示了如何手动驱动数组迭代器:
const arr = ['a', 'b', 'c'];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: 'c', done: false }
console.log(iterator.next()); // { value: undefined, done: true }从输出结果可以看到,迭代器对象的 next 方法每调用一次就前进一步,直到 done 变为 true。需要注意的是,迭代器是一次性消耗品,同一个迭代器实例被消费完毕后不会自动重置。如果还需要再次遍历,必须重新调用 Symbol.iterator 获取新的迭代器。数组迭代器还有一个容易被忽略的细节:对于稀疏数组,迭代器会在空位上返回 undefined,而不是像 map 方法那样跳过空位,这一点在实际处理中需要格外留意。
此外,数组迭代器对象本身也实现了 Symbol.iterator 方法,并且该方法返回迭代器自身。这意味着迭代器本身也是一个可迭代对象,它可以被 for...of 直接使用,也可以传给 Array.from 或其他接受可迭代对象的 API。这种设计保证了迭代器与可迭代对象之间的接口一致性。
二、数组内置的三种迭代器获取方式
数组提供了三个便捷方法用来获取不同类型的迭代器,分别是 values()、keys() 和 entries()。values() 返回一个只包含元素值的迭代器,它的行为与调用 arr[Symbol.iterator]() 完全一致,是数组默认遍历方式的显式版本。keys() 返回一个索引迭代器,它会按顺序产出数组下标。entries() 返回一个键值对迭代器,每个元素是一个包含 [索引, 值] 的二元数组。
const arr = ['x', 'y'];
for (const key of arr.keys()) {
console.log(key);
}
// 输出:0 1
for (const entry of arr.entries()) {
console.log(entry);
}
// 输出:[0, 'x'] 和 [1, 'y']
for (const value of arr.values()) {
console.log(value);
}
// 输出:x 和 y这三种方法都不会修改原数组,而是各自返回全新的迭代器对象。其中 entries() 尤其适合需要同时使用索引和值的场景,它消除了传统普通 for 循环中手动维护下标变量的负担。例如在处理用户列表渲染、表格数据映射或需要按位置更新的操作时,可以直接使用 for...of 配合解构来读取 [index, item],代码会清晰很多。
尽管这三个方法已经覆盖了大多数常规遍历需求,但它们的遍历方向固定为从前往后,步长固定为 1,并且无法在遍历过程中根据业务条件进行过滤或提前生成值。一旦遇到诸如反向遍历、每隔几个元素取一次、按条件惰性过滤等需求,就需要我们手动封装数组迭代器逻辑。
三、手动封装数组迭代器与生成器方案
手动创建数组迭代器的核心思路是维护一个索引状态,并在 next 方法中判断是否已经越过数组边界。通过闭包保存索引变量,可以实现任意自定义的遍历顺序。下面的示例封装了一个反向迭代器,它从数组末尾开始向前产出元素:
function createReverseIterator(arr) {
let index = arr.length - 1;
return {
next() {
if (index >= 0) {
return { value: arr[index--], done: false };
}
return { value: undefined, done: true };
},
[Symbol.iterator]() {
return this;
}
};
}
const items = ['a', 'b', 'c'];
const reverseIt = createReverseIterator(items);
console.log(reverseIt.next().value); // 'c'
console.log(reverseIt.next().value); // 'b'
console.log(reverseIt.next().value); // 'a'
console.log(reverseIt.next().done); // true在这个实现中,createReverseIterator 返回的对象同时包含了 next 方法和 Symbol.iterator 方法。Symbol.iterator 返回 this,这让该迭代器自身也具备可迭代能力,因此它也能被 for...of 使用。每次调用 next 时,索引会从末尾逐步递减,直到小于 0 时返回 done: true,从而结束遍历。
另一种更简洁的方式是使用生成器函数。生成器函数内部通过 yield 暂停执行,调用后会自动返回一个符合迭代器协议的对象。比如要实现一个步进迭代器,每隔指定步长取一个数组元素,生成器写法会比手动维护状态清晰不少:
function* stepIterator(arr, step) {
for (let i = 0; i < arr.length; i += step) {
yield arr[i];
}
}
const nums = [1, 2, 3, 4, 5, 6, 7];
const stepIt = stepIterator(nums, 2);
console.log(stepIt.next().value); // 1
console.log(stepIt.next().value); // 3
console.log(stepIt.next().value); // 5
console.log(stepIt.next().value); // 7
console.log(stepIt.next().done); // true生成器函数还支持 yield* 语法,可以把数组的默认迭代器委托给当前生成器,从而在自定义序列中插入额外的值。例如可以在遍历数组前后加入开始和结束标记:
function* enhancedIterator(arr) {
yield 'start';
yield* arr;
yield 'end';
}
const result = enhancedIterator([10, 20]);
for (const value of result) {
console.log(value);
}
// 依次输出:start、10、20、end对比手动实现和生成器实现,手动实现可以更细致地控制迭代器对象本身,例如管理内部状态、处理异常或实现一次性的特殊逻辑。生成器方案则更简洁,代码可读性更强,适合大多数自定义遍历场景。需要注意的是,生成器返回的迭代器在 done 变为 true 后再次调用 next 不会重新执行生成器,它仍然遵循一次性消费规则。
四、迭代器在实际开发中的应用场景与注意点
迭代器的最大价值在于惰性求值和按需获取。对于非常大的数组或外部数据源,一次性处理全部元素可能会带来内存和性能压力,而迭代器允许调用方在需要时才请求下一个值。这种模式在处理流式数据、分页加载、以及实现无限序列时非常有用。例如可以基于生成器创建一个无限递增的序列,再结合数组迭代器思想按需取用,从而避免数组长度限制。
迭代器也适合可中断遍历场景。使用 for...of 虽然可以借助 break 提前退出,但手动调用 next 能提供更细粒度的控制,例如在异步流程中逐步消费数据,或在满足特定条件后直接停止后续计算。递归处理树形结构时,也可以把子节点遍历封装成迭代器,让调用方用统一接口访问不同层级的数据。
不过在使用数组迭代器时有几个关键点需要牢记。第一,迭代器是一次性的,消费完毕后不会自动重置。如果业务中需要重复遍历,应保留原始数组并在每次遍历前重新创建迭代器,或者使用可重入的生成器函数。第二,不建议在遍历过程中修改原数组的长度,因为这种行为会导致迭代器跳过部分元素或产生重复元素,结果往往不符合预期。第三,数组迭代器返回的 value 是元素引用,对于对象类型的元素,通过迭代器获取到的仍是原对象,修改其属性会影响原数组;而对于原始值则是复制的值。第四,虽然可以通过改写 Array.prototype[Symbol.iterator] 改变所有数组的默认遍历行为,但这种操作会影响全局,容易引发不可预料的兼容问题,通常不建议在项目中使用。
掌握了数组迭代器的底层原理和常见用法之后,再面对 for...of、展开运算符、Array.from 等语法时,就能更清楚地理解它们的工作方式。无论是使用内置方法快速获取迭代器,还是手动封装自定义遍历逻辑,都能让代码在处理数组时更加灵活、可控。
JavaScript数组迭代器迭代器协议修改时间:2026-08-27 17:37:49