导读:本期聚焦于小伙伴创作的《JavaScript中的迭代器是什么?如何让普通对象变得可迭代》,敬请观看详情。遍历集合数据时如果只能靠下标和长度判断,代码会变得笨重且容易出错。JavaScript用迭代器模式把取值过程统一成next方法,返回包含value与done的对象。可迭代协议要求对象拥有Symbol.iterator方法,调用后产生迭代器。普通对象默认没有这个属性,因此无法用for...of直接循环。通过给对象添加Symbol.iterator并返回带有next函数的生成器,就能让自定义数据结构支持展开运算符与解构。理解这套机制有助于写出更通用的遍历逻辑,也能明白为何Map和Set天生可被迭代。

在JavaScript里,迭代器是一种按顺序访问数据集合的机制。它并不是一个具体的类,而是一套约定:只要一个对象实现了next方法,并且这个方法每次调用返回一个包含value和done属性的结果对象,那这个对象就可以被称为迭代器。与之相关的还有可迭代协议,指的是对象自身或原型链上部署了Symbol.iterator方法,调用该方法会返回一个迭代器。语言内建的数组、字符串、Map、Set都遵循这套协议,所以能直接使用for...of语法。

JavaScript中的迭代器是什么?如何让普通对象变得可迭代

一、迭代器与可迭代协议的基础原理

迭代器的核心在于把“如何取下一个元素”的逻辑封装起来。一个最简的迭代器可以写成普通对象,它的next方法内部维护一个指针,每次调用移动指针并返回当前值。done为false时表示还有数据,为true时表示遍历结束。这样的设计让使用者不必关心数据结构是数组、链表还是自定义对象,只要拿到迭代器就能统一处理。

可迭代协议则是更高一层的约定。如果一个对象可以被for...of循环,那它必须是可迭代的。判定标准非常简单:对象或者它的原型上存在一个键为Symbol.iterator的方法,且该方法执行后返回一个符合迭代器协议的对象。Symbol.iterator是ES6引入的知名符号,用来避免和普通属性名冲突。下面代码展示了一个手写的数字范围迭代器:

function createRangeIterator(start, end) {
  let current = start;
  return {
    next: function() {
      if (current <= end) {
        return { value: current++, done: false };
      } else {
        return { value: undefined, done: true };
      }
    }
  };
}

const it = createRangeIterator(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 }

上面的createRangeIterator返回的对象只实现了next,所以它是迭代器,但还不能直接用于for...of。要让它可迭代,需要再包一层,让对象拥有Symbol.iterator方法并返回自身或另一个迭代器。

二、为什么普通对象默认不可迭代

日常写代码时,我们习惯用for...of遍历数组,但同样的方式用在普通对象上会抛出TypeError,提示对象不是可迭代的。原因在于普通对象的原型链上没有Symbol.iterator。JavaScript设计者故意这样安排,因为对象的键是无序字符串或Symbol,直接迭代容易让开发者误以为有固定顺序,而数组和Map才强调序列。

不过这不代表对象不能迭代。很多时候我们希望遍历对象的某个内部集合,比如把所有属性值依次取出,或者把键值对当成数列处理。此时就需要手动让对象满足可迭代协议。注意,这里说的是对象“变得可迭代”,不是改变语言规则,而是在对象上显式添加Symbol.iterator方法。

三、使用Symbol.iterator让对象可迭代

最直接的做法是给目标对象定义一个Symbol.iterator属性,其值是一个函数,函数返回一个迭代器。最简单的返回方式是用生成器函数,因为生成器本身就实现了迭代器协议。下面例子让一个保存用户列表的对象支持for...of,依次产出每个用户名称:

const userGroup = {
  users: ['Alice', 'Bob', 'Charlie'],
  [Symbol.iterator]: function() {
    let index = 0;
    const self = this;
    return {
      next: function() {
        if (index < self.users.length) {
          return { value: self.users[index++], done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

for (const name of userGroup) {
  console.log(name);
}
// 输出 Alice Bob Charlie

这段代码中,userGroup通过计算属性名[Symbol.iterator]挂上了迭代方法。for...of在启动时调用这个方法拿到迭代器,然后反复调用next直到done为true。由于逻辑写在next里,外部完全不需要知道users数组的存在,这就是封装带来的好处。

如果觉得手写next太繁琐,可以利用生成器大幅简化。生成器函数内部用yield返回值,引擎会自动补全迭代器所需的结构。改写后代码如下:

const scoreMap = {
  math: 90,
  english: 85,
  science: 88,
  *[Symbol.iterator]() {
    for (const key of Object.keys(this)) {
      yield [key, this[key]];
    }
  }
};

for (const [subject, score] of scoreMap) {
  console.log(subject + ':' + score);
}

这里用到了对象方法简写加星号的生成器语法。Object.keys取出自身可枚举字符串键,每次yield抛出一个键值对数组。于是scoreMap可以直接解构成[key, value],也能被扩展运算符转为数组:const arr = [...scoreMap];。这种写法在需要处理配置对象、缓存对象时非常实用。

四、与内置语法的联动及注意事项

一旦对象可迭代,它不仅支持for...of,还能和大量语法糖配合。比如数组解构const [first, second] = obj,只要obj可迭代就能取前几个值;扩展运算符[..., obj]会把迭代结果铺平到新数组;甚至Promise.all在接收可迭代对象时也能正常工作。这让我们写的自定义数据结构可以无缝接入现有代码。

需要注意几个坑。第一,Symbol.iterator方法每次被调用都应返回全新的迭代器,否则多个循环共用同一指针会互相干扰。第二,如果对象已经部署了该方法,但返回的不是符合迭代器协议的对象,运行时照样报错。第三,用for...in遍历对象和用for...of遍历可迭代对象语义不同,前者拿键名且包含原型链,后者拿迭代值且只认协议。下面演示一个返回新迭代器避免状态污染的写法:

const counter = {
  limit: 3,
  [Symbol.iterator]() {
    let count = 0;
    const max = this.limit;
    return {
      next() {
        if (count < max) {
          return { value: count++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

const a = counter[Symbol.iterator]();
const b = counter[Symbol.iterator]();
console.log(a.next().value); // 0
console.log(b.next().value); // 0 互不影响

可以看到,每次访问counter[Symbol.iterator]都会重新执行函数,局部变量count独立存在,因此a和b两个迭代器各行其是。这样的实现才是安全的可迭代对象。

五、总结与实践建议

迭代器解决了“如何统一遍历”的问题,可迭代协议则让任意对象能接入语言级的循环与解构特性。普通对象虽默认不可迭代,但只需挂载Symbol.iterator并给出next或生成器,就能拥有和数组一样的遍历能力。在封装组件状态、树形结构、流式数据时,主动实现可迭代接口往往比暴露内部数组更优雅。

建议你在写工具库时,对暴露给外部的数据容器都考虑是否实现可迭代协议。不需要追求所有对象都可迭代,只针对那些确实有序列访问需求的场景,比如配置列表、队列、缓存条目。这样既能提升调用方代码的简洁度,也遵循了JavaScript现有的设计哲学。

iteratorSymbol_iteratoriterable_object修改时间:2026-08-02 08:12:32

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。