JS Set 遍历方法详解:forEach 与 for...of 的使用

作者:半夏时间:2026-11-06 10:14:48浏览:321

深入详解 JavaScript 中 Set 集合的两种核心遍历方式:forEach 方法与 for...of 循环,剖析其语法结构、底层参数映射、流程控制差异与异步处理场景。

JavaScript / ES6+ 数据结构与算法 核心遍历控制流
JavaScript 中的 Set 是一种用于存储唯一值的集合结构。在遍历 Set 时,最常用的两种核心方式是 forEach 方法与 for...of 循环:forEach 适合对每个元素执行回调函数并支持传递自定义上下文,但无法中途跳出循环;for...of 则基于集合的迭代器协议,不仅能直接访问元素,还能自由使用 break、continue 及 await 异步控制。本文将分别解析这两种遍历方式的语法、底层传参原理、流程控制差异以及在实际开发中的选型建议。
JS Set 遍历方法详解:forEach 与 for...of 的使用
图 0:JavaScript Set 集合架构与两种遍历方式流转概览

一、JavaScript Set 的基本特性与遍历基础

在深入掌握遍历之前,需要明确 Set 与传统数组(Array)的核心区别:Set 内部的成员是唯一的,重复添加相同的值会被自动忽略。同时,ECMAScript 规范保证了 Set 是有序集合,其遍历顺序严格按照元素的插入顺序(Insertion Order)展开。

Set 没有像普通数组那样的数值索引(Index),因此开发者不能通过 set[0] 的下标语法来读取具体元素。要访问集合内部的所有数据,必须通过“顺序遍历”的方式逐个提取。在现代 JavaScript 中,遍历 Set 主要分为两套体系:

  • 方法回调机制:即集合原型上的 Set.prototype.forEach(),借由高阶函数向每一项分发回调。
  • 迭代器机制:由于 Set.prototype[Symbol.iterator] 的内置实现,可以使用 for...of 语法直接消费该可迭代对象。

二、使用 forEach 遍历 Set:语法与参数特征

Set.prototype.forEach() 允许开发者为集合中的每一个值执行一次提供的回调函数。

1. 基本使用示例

// 环境要求:ES6 (Node.js 6+ 或所有主流浏览器)
const skills = new Set(['JavaScript', 'TypeScript', 'Rust']);

skills.forEach((value, key, ownerSet) => {
  console.log(`元素值: ${value}, 键名: ${key}`);
});

/* 示例输出:
元素值: JavaScript, 键名: JavaScript
元素值: TypeScript, 键名: TypeScript
元素值: Rust, 键名: Rust
*/

2. 为什么 forEach 的 key 与 value 完全相同?

初次使用 Set.prototype.forEach 的开发者往往会感到困惑:为什么回调函数有三个参数 (value, key, set),而且前两个参数打印出来的值完全一样?

这是 JavaScript 语言规范为了保持 API 一致性与多态性 的精心设计。在 ECMAScript 6 中,Map 和 Set 是同时被引入的,且 Array.prototype.forEach 早已存在。在 Map.prototype.forEach(value, key, map) 中,键和值分别对应真正的映射关系。

为了让一个通用的回调函数(形如 (val, key) => { ... })可以无缝兼容 Array、Map 与 Set,规范制定委员会决定不在 Set 上减少参数,而是让 key 返回与 value 相同的值。这样在编写高阶库或工具函数时,调用方无需针对集合类型编写额外的适配分支。

JS Set 遍历方法详解:forEach 与 for...of 的使用
图 1:Set forEach 回调参数映射与 Map 接口对齐原理

3. 使用 thisArg 绑定上下文

Set.prototype.forEach 接受第二个可选参数 thisArg。当传入普通函数(非箭头函数)时,该参数将作为回调函数内部的 this 引用:

const formatter = {
    prefix: '[Tech]',
    format(item) {
        console.log(`${this.prefix} ${item}`);
    }
};

const tags = new Set(['Node.js', 'Browser']);

// 传递 formatter 作为 thisArg
tags.forEach(function(val) {
    this.format(val);
}, formatter);

/* 示例输出:
[Tech] Node.js
[Tech] Browser
*/

三、使用 for...of 遍历 Set:基于迭代器的原生控制

for...of 是 ES6 引入的语言级循环语句,专门用于消费实现了可迭代协议(Iterable Protocol)的对象。Set 原型对象上默认部署了 [Symbol.iterator] 方法,因此可以直接搭配 for...of 使用。

1. 基本使用示例

const frameworks = new Set(['Vue', 'React', 'Svelte']);

for (const item of frameworks) {
    console.log('框架名称:', item);
}

/* 示例输出:
框架名称: Vue
框架名称: React
框架名称: Svelte
*/

2. 配合 Set 迭代器辅助方法:values()、keys() 与 entries()

除了直接写 for (const item of set) 外,Set 还提供了三个内置方法生成专门的迭代器:

  • set.values():返回一个包含了集合中所有值的迭代器(与 set[Symbol.iterator]() 完全等价)。
  • set.keys():在 Set 中,为了与 Map 保持 API 结构一致,set.keys() 的行为和 set.values() 相同,产出的仍是元素值。
  • set.entries():返回一个迭代器,每一项产生形如 [value, value] 的二元数组,非常适合搭配数组解构语法。
const ports = new Set([8080, 3000, 9000]);

// 使用 entries() 与数组解构
for (const [key, val] of ports.entries()) {
    console.log(`Key: ${key}, Value: ${val}`);
}

/* 示例输出:
Key: 8080, Value: 8080
Key: 3000, Value: 3000
Key: 9000, Value: 9000
*/
JS Set 遍历方法详解:forEach 与 for...of 的使用
图 2:for...of 驱动 Set 迭代器底层的流转与控制中断步骤

四、核心差异深入对比:控制流与异步处理

虽然 forEach 与 for...of 都能实现对 Set 成员的逐项遍历,但它们在流程控制与异步等待两个维度存在根本性的差异。

1. 差异一:流程中断能力(break / continue / return)

在开发中,若在找到匹配项后需要立即终止遍历以节省性能,两者的表现截然不同:

  • forEach 无法终止遍历:在 forEach 的回调函数中直接写 break 或 continue 会引发语法错误(SyntaxError: Illegal break statement);而在回调中使用 return,仅仅是结束了“当前这一项回调函数”的执行,进入下一个元素的调用,效果只相当于 continue,无法跳出整个循环。
  • for...of 原生支持完全控制流:可以随时使用 break 终止整个循环,使用 continue 跳过当前项,或者在包含函数中使用 return 直接返回。
const numbers = new Set([10, 25, 30, 45, 50]);

// 场景:找到第一个大于 20 的数字即停止遍历
for (const num of numbers) {
    if (num > 20) {
        console.log('命中目标并终止:', num);
        break; // 成功退出循环,后续 30, 45, 50 不会被执行
    }
}

/* 示例输出:
命中目标并终止: 25
*/

2. 差异二:异步处理与 async/await 的执行顺序

当遍历的每一项需要进行异步操作(如向服务端发送网络请求或查询数据库)时:

  • forEach 的异步隐患:forEach 并不等待其回调函数返回的 Promise。它会以近似并发的方式一次性触发所有项的异步调用,无法做到“处理完前一项再处理后一项”,且外层无法直接通过 await set.forEach(...) 等待所有异步任务完成。
  • for...of 的串行等待:for...of 会在上一个 await 的 Promise 决议(resolve)之后才推进迭代器并执行下一轮,从而保证严格的串行顺序。
// 模拟异步任务
const delayTask = (name, ms) => new Promise(resolve => {
    setTimeout(() => {
        console.log(`任务完成: ${name}`);
        resolve();
    }, ms);
});

async function runSequence() {
    const taskQueue = new Set(['任务A', '任务B', '任务C']);

    console.log('--- 开始顺序执行 ---');
    for (const task of taskQueue) {
        await delayTask(task, 100); // 严格每隔 100ms 依次输出
    }
    console.log('--- 全部串行执行完毕 ---');
}

runSequence();

/* 示例输出:
--- 开始顺序执行 ---
任务完成: 任务A
任务完成: 任务B
任务完成: 任务C
--- 全部串行执行完毕 ---
*/
JS Set 遍历方法详解:forEach 与 for...of 的使用
图 3:forEach 与 for...of 核心能力与流程机制多维对比

五、场景选型建议与常见避坑指南

1. 选型建议

场景需求 推荐使用方式 核心原因说明
普通全量遍历,无副作用输出 for...of 或 forEach 两者均可,现代 JavaScript 风格更推荐直接简洁的 for...of
需要中途跳出循环(如搜索满足条件的单项) for...of 原生支持 break 语法,无需遍历多余成员
涉及 await 串行执行异步流程 for...of 能够阻塞循环体,按插入顺序依次等待完成
必须传递并锁定特定 this 上下文 forEach(callback, thisArg) 无需手动使用 bind 或闭包保存指针
需要同时消费键与值进行数组模式解构 for (const [k, v] of set.entries()) 解构语法直观清晰,表达力强

2. 常见避坑指南

避坑点一:误认为 return false 可以跳出 forEach

在部分第三方工具库(如 jQuery)中,return false 可以终止遍历,但在原生 JavaScript 的 Set.prototype.forEach 中无效。回调函数中的任何返回值都会被原生引擎静默抛弃。若需提前跳出,请统一重构为 for...of 搭配 break。

避坑点二:在遍历过程中添加或删除元素

ECMAScript 规范明确规定:如果在遍历 Set 的过程中调用 set.add(x) 添加了新项,且该项在当前指针之后,该新项将在本次遍历的后续阶段被访问到。如果无限制地在循环体内添加尚未存在的新值,会导致无限循环。反之,如果在遍历过程中 set.delete(x) 删除了后续项,该项将不会被访问。

JS Set 遍历方法详解:forEach 与 for...of 的使用
图 4:JS Set 遍历方式选型决策树与实践建议

六、总结

在 JavaScript 中遍历 Set 集合时,forEach 与 for...of 并非互斥的替代品,而是针对不同编程范式和控制流场景的具体工具:

forEach 沿袭了数组与字典高阶方法的统一函数式调用规范,三参数机制(value, key, set)保证了跨结构处理逻辑的兼容性,在无需中断、需要利用 thisArg 的纯函数回调场景中十分便捷;而 for...of 深度依托语言内置的迭代器协议,不仅语法简洁,且天然拥有对 break、continue、return 以及 await 的完整控制权。在日常开发中,建议优先将 for...of 作为现代 JavaScript 的标准遍历方式,在明确需要函数式参数分发时再选用 forEach。

作者最新文章
相关文章
更多