news 2026/1/17 9:03:14

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/1/15 4:34:36

haxm is not installed排查步骤:Intel处理器兼容性深度剖析

HAXM 安装失败&#xff1f;一文彻底解决 “haxm is not installed” 顽疾 你有没有遇到过这样的场景&#xff1a;兴冲冲打开 Android Studio&#xff0c;准备调试刚写的代码&#xff0c;结果点击运行模拟器时弹出一句冰冷提示—— “HAXM is not installed” &#xff1f; 更…

作者头像 李华
网站建设 2025/12/26 20:42:52

VMware Unlocker终极指南:快速解锁macOS虚拟化功能

VMware Unlocker终极指南&#xff1a;快速解锁macOS虚拟化功能 【免费下载链接】unlocker 项目地址: https://gitcode.com/gh_mirrors/unlo/unlocker VMware Unlocker是一款功能强大的开源工具&#xff0c;专门用于解除VMware虚拟化平台对macOS系统的限制。通过智能化的…

作者头像 李华
网站建设 2025/12/27 2:15:35

双轨并行 智御未来:AI for Security与Security for AI协同战略深度解析

在人工智能技术狂飙突进的今天&#xff0c;网络安全领域正迎来一场颠覆性变革——AI for Security与Security for AI双轨战略&#xff0c;已从概念走向实战&#xff0c;成为构建数字时代纵深防御体系的核心骨架。前者以AI技术赋能安全能力升级&#xff0c;破解传统安全运营效率…

作者头像 李华
网站建设 2025/12/27 2:14:35

VMware Unlocker技术解析:完整实现macOS虚拟化实战指南

VMware Unlocker作为一款专业的macOS虚拟化工具&#xff0c;专门解决在非苹果硬件上运行苹果系统的技术难题。通过智能补丁技术&#xff0c;这款工具能够有效绕过macOS在虚拟化环境中的硬件限制&#xff0c;让Windows和Linux用户都能在普通PC上获得完整的苹果系统体验。 【免费…

作者头像 李华
网站建设 2026/1/8 2:51:54

LeRobot机器人控制:5分钟从零搭建你的第一个AI机械臂

LeRobot机器人控制&#xff1a;5分钟从零搭建你的第一个AI机械臂 【免费下载链接】lerobot &#x1f917; LeRobot: State-of-the-art Machine Learning for Real-World Robotics in Pytorch 项目地址: https://gitcode.com/GitHub_Trending/le/lerobot 还在为复杂的机器…

作者头像 李华
网站建设 2026/1/15 12:41:32

ImageToSTL:从平面到立体的智能转换革命

ImageToSTL&#xff1a;从平面到立体的智能转换革命 【免费下载链接】ImageToSTL This tool allows you to easily convert any image into a 3D print-ready STL model. The surface of the model will display the image when illuminated from the left side. 项目地址: h…

作者头像 李华