news 2026/3/19 10:09:52

ES6入门实战:Set与Map数据结构从零实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ES6入门实战:Set与Map数据结构从零实现

从手写实现到工程实战:深入理解 ES6 中的 Set 与 Map

你有没有遇到过这样的场景?

  • 想给一个数组去重,写了好几行filter+indexOf,结果发现对象还去不掉;
  • 想用某个 DOM 节点当“键”来存一些临时数据,却发现 JavaScript 对象的键只能是字符串;
  • 在做函数缓存时,为了处理多个参数绞尽脑汁拼接 key,最后还得担心命名冲突……

这些问题,在 ES6 出现之前,几乎是每个前端开发者都踩过的坑。而今天我们要聊的主角 ——SetMap,正是为了解决这些痛点而生。

它们不是花架子,而是真正改变了我们组织和操作数据的方式。更重要的是,理解它们的工作原理,不仅能让你写出更高效的代码,还能在面试中从容应对“手写一个 Set”这类高频题。


为什么我们需要 Set?数组去重真的那么简单吗?

我们先来看一个看似简单的问题:如何对数组去重?

const arr = [1, 2, 3, 2, 1]; // 方法一:使用 filter + indexOf const unique1 = arr.filter((item, index) => arr.indexOf(item) === index); // 方法二:使用 Set(一行解决) const unique2 = [...new Set(arr)];

两种写法都能得到[1, 2, 3],但性能差距巨大:

方法时间复杂度
filter + indexOfO(n²)
new Set()O(n)

别小看这个差异。当数组长度达到 10000 级别时,前者可能卡顿几百毫秒,后者几乎无感。

但这还不是全部问题。如果数组里存的是对象呢?

const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 1, name: 'Alice' } // 重复项 ]; // 你会发现,无论 indexOf 还是 includes 都无法识别这两个对象相等 users.includes(users[0]); // true users.includes({ id: 1, name: 'Alice' }); // false!

因为对象比较的是引用地址,内容相同也没用。

这个时候,Set依然束手无策 —— 它也只能基于引用去重。但我们可以通过封装逻辑配合Map来解决这类需求(后文会讲)。

那 Set 到底是怎么工作的?

你可以把Set想象成一个“智能盒子”,它只允许每个值进去一次。

它的底层通常基于哈希表实现,插入、查找、删除平均时间复杂度都是O(1)。虽然 V8 引擎内部实现细节很复杂,但从行为上我们可以这样抽象:

  • 添加元素时,先检查是否已存在;
  • 如果不存在,则添加并记录顺序;
  • 所有操作保持插入顺序可遍历。

而且,SetNaN友好 —— 尽管NaN !== NaN,但在Set中只会保留一个NaN


动手实现一个简易版 Set

光说不练假把式。下面我们来手写一个简化但功能完整的MySet类,帮助你彻底搞懂它的运行机制。

class MySet { constructor(iterable) { this._items = []; if (iterable) { for (const item of iterable) { this.add(item); } } } add(value) { if (!this.has(value)) { this._items.push(value); } return this; // 支持链式调用 } has(value) { // 特殊处理 NaN:利用 NaN !== NaN 的特性 if (value !== value) { return this._items.some(v => v !== v); } return this._items.includes(value); } delete(value) { const index = this._items.findIndex(v => { // 同样处理 NaN if (v !== v && value !== v) return true; return v === value; }); if (index > -1) { this._items.splice(index, 1); return true; } return false; } clear() { this._items = []; } get size() { return this._items.length; } [Symbol.iterator]() { let index = 0; return { next: () => { if (index < this._items.length) { return { value: this._items[index++], done: false }; } return { done: true }; } }; } }

关键设计点解析

  1. has中的value !== value是判断NaN的经典技巧
    因为只有NaN满足x !== x,所以可以用它来做特殊匹配。

  2. 迭代器协议[Symbol.iterator]让实例支持for...of
    这是 ES6 可迭代协议的核心,让自定义集合也能被原生语法消费。

  3. 返回this实现链式调用
    set.add(1).add(2),提升 API 使用体验。

  4. 使用数组存储_items是为了教学清晰
    实际引擎不会这么做(includes是 O(n)),生产环境多用哈希结构优化。

尽管这个版本性能不如原生Set,但它完整还原了语义逻辑,非常适合学习和调试。


Map:JavaScript 终于有了真正的字典

如果说Set解决了“唯一性”的问题,那Map解决的就是“灵活映射”的问题。

在 ES6 之前,我们只能用普通对象{}来模拟键值对:

const cache = {}; const key = someDOMNode; cache[key] = 'data'; // ❌ 出问题了!

为什么出问题?因为所有非字符串键都会被自动转成字符串!

{}.toString(); // "[object Object]" [] + []; // ""

于是两个不同的对象可能变成同一个键,造成数据覆盖。

更糟的是,原型链上的属性也可能被误读:

'toString' in {} // true,即使你没定义

这就是所谓的“属性名污染”。

Map彻底打破了这些限制。


Map 的核心优势

对比维度普通对象 ({})Map
键类型仅字符串/Symbol任意类型(包括对象、函数)
插入顺序不保证(早期 JS 行为不一)保证按插入顺序
获取大小Object.keys().length直接.size
是否受原型影响
动态增删性能可能触发隐藏类失效更稳定

正因为这些优点,Map成为现代 JS 中管理动态映射关系的首选工具。


手写实现 MyMap:揭开哈希映射的本质

同样地,我们通过手写MyMap来理解其内部机制。

class MyMap { constructor(iterable) { this._items = []; if (iterable) { for (const [key, value] of iterable) { this.set(key, value); } } } set(key, value) { const entry = this._findEntry(key); if (entry) { entry.value = value; } else { this._items.push({ key, value }); } return this; } get(key) { const entry = this._findEntry(key); return entry ? entry.value : undefined; } has(key) { return !!this._findEntry(key); } delete(key) { const index = this._items.findIndex(entry => this._isKeyEqual(entry.key, key) ); if (index > -1) { this._items.splice(index, 1); return true; } return false; } clear() { this._items = []; } get size() { return this._items.length; } _findEntry(key) { return this._items.find(entry => this._isKeyEqual(entry.key, key)); } _isKeyEqual(a, b) { // 处理 NaN === NaN if (a !== a && b !== b) return true; return a === b; } [Symbol.iterator]() { let index = 0; return { next: () => { if (index < this._items.length) { const { key, value } = this._items[index++]; return { value: [key, value], done: false }; } return { done: true }; } }; } }

设计亮点说明

  • _findEntry抽离查找逻辑,避免重复代码;
  • _isKeyEqual正确处理NaN比较,确保语义一致性;
  • 迭代器返回[key, value]数组,符合规范,支持解构:

js for (const [k, v] of myMap) { console.log(k, v); }

  • 支持链式调用,API 更友好。

虽然查找仍是 O(n),但对于小型缓存或配置映射完全够用。更重要的是,它揭示了Map的本质:一个支持任意键的有序键值对列表


工程实战中的典型应用场景

理论懂了,怎么用才是关键。以下是我在真实项目中总结的高价值使用模式。

场景一:用户选中状态管理(Set)

const selectedIds = new Set(); function toggleSelect(id) { selectedIds.has(id) ? selectedIds.delete(id) : selectedIds.add(id); } toggleSelect(1); // 选中 toggleSelect(2); // 再选中 toggleSelect(1); // 取消(自动去重生效) console.log([...selectedIds]); // [2]

✅ 优势:无需手动维护索引,天然防重复。

场景二:DOM 节点元数据绑定(Map)

const nodeStyles = new Map(); function attachStyle(node, style) { nodeStyles.set(node, style); } function applyStyles() { for (const [node, style] of nodeStyles) { Object.assign(node.style, style); } } // 即使节点被复用,也能精准恢复样式 attachStyle(document.getElementById('btn'), { color: 'red' });

✅ 优势:安全附加数据,不影响 DOM 结构,也不会泄漏全局变量。

场景三:函数记忆化(Memoization)优化性能

function memoize(fn) { const cache = new Map(); return function(...args) { const key = args.length === 1 ? args[0] : JSON.stringify(args); if (cache.has(key)) { return cache.get(key); } const result = fn.apply(this, args); cache.set(key, result); return result; }; } const fib = memoize(n => n <= 1 ? n : fib(n - 1) + fib(n - 2)); fib(40); // 原本递归爆炸,现在瞬间完成

⚠️ 注意:对于对象参数,建议升级为WeakMap避免内存泄漏。


高阶技巧与最佳实践

1. 什么时候该用SetvsArray

  • 元素需唯一 →Set
  • 需频繁判断是否存在 →Set.has()arr.includes()快得多
  • 不关心顺序或需要排序 → 仍可用Array

2.Mapvs 普通对象怎么选?

使用场景推荐选择
键是字符串且结构固定{}
键是变量/对象/函数Map
频繁增删键值对Map
需要.size或迭代顺序Map

3. 内存安全:善用WeakSetWeakMap

如果你用对象作为键,并希望在对象被回收时自动释放关联数据,请使用弱引用版本:

const seenNodes = new WeakSet(); function processNode(node) { if (seenNodes.has(node)) return; seenNodes.add(node); // 处理逻辑... }

🔐WeakSetWeakMap不会阻止垃圾回收,适合做标记、缓存、监听等场景。

4. 不要试图JSON.stringify(Set)

直接序列化会失败:

JSON.stringify(new Set([1,2,3])); // "{}"

正确做法:

JSON.stringify([...new Set([1,2,3])]); // "[1,2,3]"

或者转换为对象形式存储。


写在最后:掌握 Set 与 Map,才算真正迈入现代 JS 开发

SetMap看似只是两个新数据结构,实则代表了一种思维方式的转变:

从“将就用对象模拟”到“选用合适的数据模型”

当你开始思考:“这个场景该用Set还是Map?”、“要不要考虑内存回收?”——你就已经脱离了初级编码阶段。

它们也是许多高级特性的基石。比如:

  • React中用Set管理依赖列表;
  • Vue的响应式系统用WeakMap存储观测关系;
  • 缓存池、事件总线、权限校验等通用模块广泛采用Map

未来 ECMAScript 提案中的HashSetsHashTables、甚至TupleRecord,也都延续了这一理念。

所以,下次你在写去重逻辑时,不要再写filter + indexOf了。打开控制台,敲下:

[...new Set(array)]

那一瞬间的清爽感,就是技术进步带来的最小却最真实的快乐。

如果你在实际项目中有有趣的Set/Map用法,欢迎在评论区分享交流!

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/15 5:40:40

如何为React-PDF文档添加防篡改的时间戳签名?

在现代数字化办公环境中&#xff0c;PDF文档的安全性越来越受到重视。React-PDF作为一个强大的React库&#xff0c;不仅能够创建精美的PDF文档&#xff0c;还能为文档添加专业级的安全保障。今天&#xff0c;我们就来聊聊如何利用React-PDF为你的重要文档添加防篡改的时间戳签名…

作者头像 李华
网站建设 2026/3/17 6:06:35

华为云能源行业云边协同:构筑新能源电站智能运维新基座

在新能源产业规模化发展进程中&#xff0c;光伏、风电场等设施普遍面临分布分散、环境恶劣、数据繁杂等运维难题&#xff0c;传统人工巡检模式效率低下、响应滞后&#xff0c;难以适配高质量运营需求。华为云依托云边协同架构&#xff0c;打造新能源电站数据采集与智能运维平台…

作者头像 李华
网站建设 2026/3/16 2:44:22

XJTU-thesis终极指南:西安交大学位论文排版快速上手教程

对于西安交通大学的硕博研究生来说&#xff0c;XJTU-thesis LaTeX模板是撰写学位论文的得力助手。这个官方模板能够自动处理复杂的格式要求&#xff0c;确保生成的PDF完全符合学校规范&#xff0c;让你专注于内容创作而非格式调整。 【免费下载链接】XJTU-thesis 西安交通大学学…

作者头像 李华
网站建设 2026/3/16 4:51:18

当攻击进入工业化时代:彻底告别人力驱动的传统模式,全面迈入机器速度的智能防御新纪元

工业化的浪潮曾重塑人类生产方式&#xff0c;而如今&#xff0c;网络攻击的“工业化” 正成为全球安全领域的最大威胁——自动化攻击工具批量生成、僵尸网络规模化部署、漏洞利用流程标准化&#xff0c;传统依赖人工响应的防御体系早已不堪一击。在攻防对抗的赛道上&#xff0c…

作者头像 李华
网站建设 2026/3/16 4:51:19

Editor.md:打造高效在线Markdown编辑体验

Editor.md&#xff1a;打造高效在线Markdown编辑体验 【免费下载链接】editor.md The open source embeddable online markdown editor (component). 项目地址: https://gitcode.com/gh_mirrors/ed/editor.md 在当今内容创作和技术文档编写中&#xff0c;Markdown已经成…

作者头像 李华
网站建设 2026/3/16 4:51:21

Windows安卓应用安装器终极指南:一键实现跨平台应用体验

Windows安卓应用安装器终极指南&#xff1a;一键实现跨平台应用体验 【免费下载链接】APK-Installer An Android Application Installer for Windows 项目地址: https://gitcode.com/GitHub_Trending/ap/APK-Installer 还在为在Windows电脑上无法直接安装安卓应用而烦恼…

作者头像 李华