怎么在JavaScript中手动实现自定义迭代器

作者:青柠时间:2026-11-03 13:29:33浏览:417

深入解析如何在JavaScript中从零手动实现自定义迭代器。系统拆解Iterable与Iterator双层协议契约,提供范围迭代器、链表遍历、生命周期清理return方法与自引用迭代器的完整代码示例和排坑指南。

在 JavaScript 中,手动实现自定义迭代器的本质是遵循 ECMAScript 规范定义的迭代器协议(Iterator Protocol)与可迭代协议(Iterable Protocol):为目标对象挂载一个名为 [Symbol.iterator] 的工厂函数,该函数返回一个包含 next() 方法的对象;每次执行 next() 时计算并递增内部游标指针,返回包含 value 和 done 两个属性的迭代结果对象。开发者无需依赖生成器语法糖(function*),即可为任意自定义集合、复杂数据结构或流式序列提供完全兼容 for...of、解构赋值与展开运算符的原生遍历能力。

怎么在JavaScript中手动实现自定义迭代器
图 0:JavaScript 可迭代对象、迭代器与规范产物三元架构全景

一、协议本质:解构可迭代协议与迭代器协议

在 ECMAScript 6 之后,JavaScript 遍历体系并不是针对数组单独定制的语法糖,而是一套严格基于鸭子类型(Duck Typing)的接口规范。很多开发者经常混淆“可迭代对象”与“迭代器”的概念,理解两者的分工是手动编码的前提:

  • 可迭代对象(Iterable):充当数据容器。只要一个对象拥有以 Symbol.iterator 为键的方法,且该方法调用后会返回一个迭代器实例,这个对象就是可迭代对象。常见原生对象如 Array、Set、Map、String 都默认实现了该协议。
  • 迭代器对象(Iterator):充当状态游标机。迭代器本身是一个包含 next() 方法的普通对象。每次调用 next(),它都会按既定规则移动内部指针,并返回一个符合 IteratorResult 接口的普通对象。
  • 迭代结果(IteratorResult):必须包含两个字段——value 表示当前项的值(可以是任意类型数据);done 为布尔值,当遍历仍在进行时为 false,当数据全部耗尽或终止时为 true。
怎么在JavaScript中手动实现自定义迭代器
图 1:ECMAScript 可迭代协议与迭代器协议的分层职责及消费生态

可迭代协议的核心价值在于工厂化与独立性。可迭代对象只负责在每次被消费时(例如每次遇到新的 for...of 循环)调用 [Symbol.iterator]() 生成一个崭新的迭代器。这样即使有两个并发循环遍历同一个数组或容器,它们各自手持独立的迭代器实例,内部游标互不干扰。

二、从零编写基础范围迭代器(Range Iterator)

我们先脱离复杂对象,通过最简闭包结构手动构建一个基础的 createRangeIterator 函数。该函数接收起始值 start、终止值 end 和步长 step,返回一个纯粹的迭代器对象。

// 基础范围迭代器工厂函数
function createRangeIterator(start = 0, end = Infinity, step = 1) {
  // 步骤 1:在闭包内维护私有游标状态
  let current = start;

  // 返回符合迭代器协议(Iterator Protocol)的对象
  return {
    // 步骤 2:必须实现无参或接收参数的 next() 方法
    next() {
      if (current <= end) {
        const resultValue = current;
        current += step; // 步骤 3:向后推进游标指针
        // 返回进行中的迭代结果
        return { value: resultValue, done: false };
      }

      // 步骤 4:当游标越界时,返回终止结果并保持终态幂等
      return { value: undefined, done: true };
    }
  };
}

这个闭包内部的 current 变量充当了状态机的记忆指针。外部代码通过连续调用 next() 方法逐步消费数据:

// 手动消费测试代码
const iterator = createRangeIterator(1, 5, 2);

console.log(iterator.next()); // 第一次调用
console.log(iterator.next()); // 第二次调用
console.log(iterator.next()); // 第三次调用
console.log(iterator.next()); // 第四次调用(触达边界)
console.log(iterator.next()); // 第五次调用(终止后继续调用)
控制台示例输出
{ value: 1, done: false }
{ value: 3, done: false }
{ value: 5, done: false }
{ value: undefined, done: true }
{ value: undefined, done: true }
怎么在JavaScript中手动实现自定义迭代器
图 2:手动实现自定义迭代器的 4 步标准工程流程
规范提醒:终态幂等性

ECMAScript 规范要求,当迭代器首次返回 done: true 之后,后续对该迭代器进行的任意多次 next() 调用,都必须持续稳定地返回 done: true。切勿在游标越界后发生状态回滚或抛出未捕获异常。

三、赋予对象原生遍历能力:实现 [Symbol.iterator] 属性

上述纯迭代器虽然实现了 next(),但如果直接将它放入 for (const n of iterator),在未做进一步处理时会抛出 TypeError: iterator is not iterable 错误。这是因为 JavaScript 的原生循环语法只接受可迭代对象(Iterable)。

要让一个普通对象或自定义类支持原生遍历,必须通过全局内建的 Symbol.iterator 符号为其挂载迭代器生成方法。我们以一个实际的数字区间集合类 NumberRange 为例:

class NumberRange {
  constructor(start, end, step = 1) {
    this.start = start;
    this.end = end;
    this.step = step;
  }

  // 实现可迭代协议:以 Symbol.iterator 作为方法名
  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    const step = this.step;

    // 每次遍历返回崭新的独立迭代器实例
    return {
      next() {
        if (current <= end) {
          const value = current;
          current += step;
          return { value, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

通过实现 [Symbol.iterator],该类的实例就可以直接融入现代 JavaScript 所有的可迭代消费生态中:

const range = new NumberRange(1, 5, 2);

// 1. 原生 for...of 循环
for (const num of range) {
  console.log("for...of 节点:", num);
}

// 2. 数组展开运算符(Spread Operator)
const arraySpread = [...range];
console.log("展开后数组:", arraySpread);

// 3. 解构赋值(Destructuring)
const [first, second] = range;
console.log("解构项:", first, second);

// 4. Array.from 转换
const arrayFrom = Array.from(range, x => x * 10);
console.log("Array.from 映射结果:", arrayFrom);
控制台示例输出
for...of 节点: 1
for...of 节点: 3
for...of 节点: 5
展开后数组: [ 1, 3, 5 ]
解构项: 1 3
Array.from 映射结果: [ 10, 30, 50 ]
怎么在JavaScript中手动实现自定义迭代器
图 3:运行时游标指针推进与状态机状态转移跟踪图
架构防错:切勿将游标存放在 this 实例上

常见的错误写法是在 NumberRange 构造函数中声明 this.current = start,然后在 next() 中使用 this.current += this.step。这种做法破坏了实例的状态隔离,导致该对象只能被完整遍历一次,第二次执行 for...of 时将直接退出。正确的做法始终是将游标放在 [Symbol.iterator] 生成的局部作用域或单独的迭代器类中。

四、进阶特性:提前终止处理与自引用迭代器模式

在实际生产环境中,除了标准的连续读取,还需要应对遍历过程被提前中断的场景,以及让迭代器本身能够直接作为可迭代对象复用的需求。

4.1 可选的 return() 方法:捕获中断与资源清理

当外部使用 break、return、抛出异常,或者在解构赋值只读取前几项就退出时,迭代并未自然耗尽。如果该迭代器打开了底层文件句柄、网络通道、数据库游标或启动了内部计时器,就需要一个钩子进行资源释放。

ECMAScript 迭代器协议为此提供了可选的 return(value) 方法。JavaScript 引擎在检测到循环提前退出时,会自动检查迭代器是否存在 return 方法并执行它:

function createSafeResourceIterator(resourceName) {
  let index = 0;
  const maxItems = 10;
  let isCleanedUp = false;

  console.log(`[资源初始化] 打开数据源: ${resourceName}`);

  const cleanUp = () => {
    if (!isCleanedUp) {
      console.log(`[资源清理] 释放连接与游标: ${resourceName}`);
      isCleanedUp = true;
    }
  };

  return {
    next() {
      if (index < maxItems) {
        return { value: `item_${++index}`, done: false };
      }
      cleanUp(); // 正常耗尽时清理
      return { value: undefined, done: true };
    },

    // 引擎在遇到 break / return / throw / 提前解构时自动调用
    return(value) {
      cleanUp(); // 提前终止时清理
      return { value, done: true }; // 必须返回包含 done: true 的对象
    }
  };
}

// 模拟提前跳出
const resource = {
  [Symbol.iterator]() {
    return createSafeResourceIterator("Database_Cursor_01");
  }
};

for (const data of resource) {
  console.log("消费数据:", data);
  if (data === "item_2") {
    console.log("满足特定条件,主动 break 退出!");
    break; // 触发 return() 清理机制
  }
}
控制台示例输出
[资源初始化] 打开数据源: Database_Cursor_01
消费数据: item_1
消费数据: item_2
满足特定条件,主动 break 退出!
[资源清理] 释放连接与游标: Database_Cursor_01

4.2 自引用可迭代迭代器(Iterable Iterator)

JavaScript 原生数组的 arr.values() 或 arr.keys() 返回的对象既是迭代器(有 next()),同时又具备可迭代能力(能直接放入 for...of)。这种特性被称为自引用迭代器。

要实现这一标准行为,只需在迭代器对象自身上挂载 [Symbol.iterator]() { return this; } 即可:

function makeIterableIterator(start, end) {
  let curr = start;

  const iterator = {
    next() {
      if (curr <= end) {
        return { value: curr++, done: false };
      }
      return { value: undefined, done: true };
    },
    // 将自身声明为可迭代对象
    [Symbol.iterator]() {
      return this;
    }
  };

  return iterator;
}

const singleUseIter = makeIterableIterator(10, 12);
// 此时迭代器自身可直接被 for...of 消费,无需通过外部容器包装
for (const val of singleUseIter) {
  console.log("自引用消费项:", val);
}
怎么在JavaScript中手动实现自定义迭代器
图 4:迭代器生命周期闭环流程与提前终止清理机制

五、数据结构实战:单向链表的自定义迭代器

为了加深对自定义迭代器的理解,我们来看一个非线性的典型数据结构——单向链表(LinkedList)。很多初学者在需要遍历链表时,会先通过数组暂存节点,这不仅消耗了多余的内存空间,而且破坏了链表动态流式的特性。通过实现自定义迭代器,可以直接在链表节点之间按需流转:

// 链表节点类
class ListNode {
  constructor(value, next = null) {
    this.value = value;
    this.next = next;
  }
}

// 链表容器类
class LinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
  }

  append(value) {
    const newNode = new ListNode(value);
    if (!this.head) {
      this.head = newNode;
      this.tail = newNode;
      return this;
    }
    this.tail.next = newNode;
    this.tail = newNode;
    return this;
  }

  // 实现可迭代协议
  [Symbol.iterator]() {
    let currentNode = this.head; // 游标指向链表头节点

    return {
      next() {
        if (currentNode !== null) {
          const nodeValue = currentNode.value;
          currentNode = currentNode.next; // 推进指针至下一节点
          return { value: nodeValue, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

// 使用与验证
const list = new LinkedList();
list.append("Node A").append("Node B").append("Node C");

for (const text of list) {
  console.log("链表节点值:", text);
}
控制台示例输出
链表节点值: Node A
链表节点值: Node B
链表节点值: Node C

六、高频陷阱与开发避坑准则

在手动实现迭代器时,由于 JavaScript 不会在编译期强制做接口类型校验,容易发生隐蔽的运行时逻辑缺陷。以下是四项最关键的避坑准则:

常见陷阱 缺陷表现与后果 正确规范与解决方案
done: true 时携带有效数据 在最后一个元素返回 { value: last, done: true },导致 for...of 直接忽略并丢失最后一项。 所有有效数据必须在 done: false 时返回;当 done: true 时,value 应为 undefined。
游标共享引发的状态竞争 将遍历索引定义在主实例 this.index 上,导致嵌套循环或多次遍历时直接死锁或跳出。 每次调用 [Symbol.iterator]() 时都在函数作用域内创建全新的独立闭包指针变量。
非法步长导致死循环 未对 step <= 0 或 NaN 做校验,导致游标无法推进,造成主线程卡死崩溃。 在迭代器初始化时增加断言校验:if (step <= 0) throw new RangeError("step 必须为正数")。
遗漏 return() 返回值 在 return() 中执行了清理但没有返回合法对象,导致 TypeError: Iterator result undefined is not an object。 return() 钩子必须显式返回符合协议的对象,标准形式为 return { done: true }。

七、总结与标准实现脚手架

手动实现 JavaScript 自定义迭代器并不复杂,核心归结为三条铁律:

  1. 定接口:让容器具备 [Symbol.iterator]() 工厂方法,确保其能够产出迭代器。
  2. 管状态:在闭包或迭代器实例中隔离 current 指针,严禁污染容器实例。
  3. 守边界:严格遵循 { value, done } 契约,做到自然耗尽有终态、异常退出有 return() 清理、终态之后具备幂等性。

当需要为自定义对象编写生产级迭代器时,可以直接参考以下标准脚手架代码:

// 生产级标准可迭代容器模板
class StandardCollection {
  constructor(items = []) {
    this.items = items;
  }

  [Symbol.iterator]() {
    let pointer = 0;
    const source = this.items;
    const total = source.length;

    const iterator = {
      next() {
        if (pointer < total) {
          return { value: source[pointer++], done: false };
        }
        return { value: undefined, done: true };
      },

      return(value) {
        // 资源回收与状态重置逻辑
        pointer = total;
        return { value, done: true };
      },

      // 实现自引用,兼具迭代器与可迭代能力
      [Symbol.iterator]() {
        return this;
      }
    };

    return iterator;
  }
}
作者最新文章
相关文章
更多