资讯动态

Map和Set在前端项目中的实战价值:告别数组走天下的低效代码

发布时间:2026/9/20 3:45:50 来源:尧图企业网站定制
上个月我做了一个表格批量操作功能要保存用户勾选的所有行。第一版很自然地用了数组存 id结果连续点几十次之后去重、查找、删除全开始变得难写最后翻来覆去全是indexOf和splice代码又臭又多还老出边界问题。后来我把数据容器换成了Set代码量直接少掉一半逻辑也直白了。那之后我意识到一个事实很多人写了三五年前端日常数据结构还是只有数组和对象这两个选项Map和Set在认知里只是面试八股进不了项目。这篇就是写给这些老铁看的把Map和Set在真实项目里到底香在哪、什么时候该用、什么时候不该用讲清楚。1. “万物皆数组”的代价对象哈希表隐藏的小陷阱1.1 对象的键只有一种归宿字符串先用一个最经典的例子热热身。很多前端做配置映射时下意识的写法是const obj {}; const key { id: 1 }; obj[key] hello; console.log(obj); // { [object Object]: hello }你以为你在用一个对象当键其实 JavaScript 早就把它调教成了字符串[object Object]。于是第二个对象键会把第一个覆盖掉数据悄悄丢失还不报错。这是对象作为哈希表最坑的地方——键的类型会被强制转换。数字键也有类似问题。很多人不知道Object.keys对数字键的排序规则和字符串键不一样const obj {}; obj[b] 1; obj[2] 2; obj[1] 3; console.log(Object.keys(obj)); // [1, 2, b]数字键按升序排在前面如果你写了一个依赖遍历顺序的逻辑对象会在某些场景下给你“惊喜”。Map就不存在这个问题它严格按插入顺序遍历键是什么类型就保留什么类型这也是我第一个切换场景的原因。1.2 原型链、隐藏类与 delete 的低效对象还有一层历史包袱——原型链。用普通对象做映射时key in object会把原型链上的属性也算进去比如最常见的就是toString、constructor这种名字你根本没法防止用户配置里出现这类键。万一有人塞了一个__proto__那行为更是让人摸不着头脑。要安全判断对象里有没有某个自有键必须写Object.prototype.hasOwnProperty.call(obj, key)又长又绕。性能上普通对象也不是稳的。V8 对对象做了“隐藏类”优化当你给对象增加、删除属性隐藏类会变化甚至退化进入字典模式后存取效率更差。尤其在频繁delete的场景里对象的代价明显比Map高而Map是专门给这种动态增删键值对设计的引擎对它有独立的优化路径。这些细节在小型页面里感受不出来但只要你做一个数据量稍微大一点的管理后台差距就来了。提示如果你的对象只是一个固定结构、只有几个字段比如{ name, age, email }那就继续用对象没必要为了用 Map 而用 Map。Map 的优势在于“键值对会动态变化”和“键的类型不受限”。2. Map 上手从“键值对”到“真正的哈希表”的思维切换2.1 基本操作set 可以链式has 是灵魂Map的基本 API 大家应该都背过但实际开发时的体验完全不同。我最喜欢的一点是set返回 map 本身可以链式初始化const userStatus new Map() .set(alice, { online: true }) .set(bob, { online: false });取值、判断、删除是标配三件套userStatus.get(alice); // { online: true } userStatus.has(bob); // true userStatus.delete(bob); // true userStatus.size; // 1注意size是 Map 的属性不像数组用length也不像对象需要Object.keys(obj).length。多了这个内置属性判断“是否有数据”直接if (map.size)比对象优雅太多。判断键是否存在这点在实际项目里的体验差异非常明显。对象时代你总要纠结是obj.key、obj[key]还是hasOwnPropertyMap 时代一个has全解决不需要担心键重名冲突也不怕原型链上的脏东西。2.2 复杂键函数、NaN、DOM 节点都能当 key这是 Map 碾压对象的决定性场景。对象能当键吗不能因为会被转成字符串。但 Map 可以而且很多业务里就是需要这种能力。我举一个很现实的例子给一组 DOM 节点存元数据。以前大家会用>const nodeMeta new Map(); function handleClick(btn) { if (!nodeMeta.has(btn)) { nodeMeta.set(btn, { clickCount: 0, startTime: Date.now() }); } const meta nodeMeta.get(btn); meta.clickCount 1; }函数当 key 也很有用尤其是事件总线或者观察者模式里。你需要为每个回调函数存一份独立的注册信息对象的字符串转换直接让你崩溃Map 则可以精确对应到函数本身。还有个细节Map的键比较用的是SameValueZero规则NaN和自己相等。这意味着你可以把NaN当键也可以正常查到不用担心NaN ! NaN的问题。数组、正则这些引用类型只要引用地址一致也能正确命中同一个键。2.3 遍历顺序、解构与对象互转的实用姿势Map天生就是可迭代对象配合for...of遍历是体验最好的方式之一for (const [key, value] of userStatus) { console.log(key, value.online); }也可以根据需要取键序列或值序列[...userStatus.keys()]; // [alice] [...userStatus.values()]; // [{ online: true }]和对象互转几乎是无缝的// 对象转 Map const config { theme: dark, lang: zh-CN }; const configMap new Map(Object.entries(config)); // Map 转对象 const plainObj Object.fromEntries(configMap);这个Object.fromEntries真的香我经常用它把 Map 还原成普通对象传给接口因为 JSON 序列化不认 Map。做接口对接时前端内部用 Map 管理状态提交前Object.fromEntries一下就能转成后端要的 JSON相当丝滑。我自己的缓存套路也依赖 Map一个典型的memoize函数const cache new Map(); function getConfig(id) { if (cache.has(id)) return cache.get(id); const data fetchConfigFromServer(id); // 模拟异步取数 cache.set(id, data); return data; }缓存这种场景对键值对动态增删、频繁has判断要求极高Map 天然匹配。当然缓存不用删除时 Map 就够了如果希望缓存项失效后被垃圾回收那要用后面的 WeakMap这里先按下不表。3. Set 的价值不止去重存在性判断与集合运算的正确打开方式3.1 去重只是最表面的能力提到Set很多人的第一反应是数组去重。对这是最直观的用途const ids [1, 2, 2, 3, 3, 3]; const uniqueIds [...new Set(ids)]; // [1, 2, 3]但如果你只把 Set 当去重工具那就亏大了。Set 的真正价值在于它代表了一个数学意义上的“集合”元素唯一、无序、可以快速判断是否包含某个元素。has方法是 O(1) 级别的判断而数组的includes是 O(n) 级别数据一多两者差距就摆在眼前。举一个做活动页常见的场景用户可选择的标签过滤const activeTags new Set(); function toggleTag(tag) { if (activeTags.has(tag)) { activeTags.delete(tag); } else { activeTags.add(tag); } renderFilteredList(); }用数组写还要考虑indexOf、splice这些操作可读性差不说过滤数据时还得再includes一遍。Set 的has写出来语义就是“这个标签在不在当前选中集合里”一眼就懂。注意一点Set的去重遵循SameValueZero规则NaN可以正确去重但两个内容相同的对象依然会被当成两个不同元素因为它们的引用地址不同。如果你要按对象某个字段去重还是得自己用 Map 实现const arr [{ id: 1 }, { id: 1 }, { id: 2 }]; const seen new Map(); arr.forEach(item seen.set(item.id, item)); const uniqueByField [...seen.values()];3.2 集合运算并集、交集、差集一步到位前端业务里其实经常遇到“集合运算”的需求只是很多人没意识到。比如“我有 A 组权限和 B 组权限要找出两边共有的部分”。以前写数组时要用filter套includes现在用 Set 语义更清晰const userPerms new Set([read, write]); const requiredPerms new Set([write, delete]); // 并集 const union new Set([...userPerms, ...requiredPerms]); // { read, write, delete } // 交集 const intersection new Set( [...userPerms].filter(item requiredPerms.has(item)) ); // { write } // 差集 const difference new Set( [...userPerms].filter(item !requiredPerms.has(item)) ); // { read }这里虽然还是要filter但因为内部用的是requiredPerms.has(item)而不是includes整体复杂度从 O(n*m) 降到了 O(nm)数据量大了以后差距很可观。我做过一个设备筛选面板左侧是“在线状态”过滤条件右侧是“设备类型”过滤条件最终要找出同时满足两个条件的设备。把设备 id 分别存成两个 Set然后取交集就是结果集。配合记忆化状态一变化就重新算一次交集UI 实时刷新性能毫无压力。3.3 实践案例多选表格的 selected 集合管理具体落到我开头的那个场景。表格多选时我用 Set 存选中行的 idconst selectedIds new Set(); function toggleSelect(row) { if (selectedIds.has(row.id)) { selectedIds.delete(row.id); } else { selectedIds.add(row.id); } updateFooter(); } function isSelected(row) { return selectedIds.has(row.id); }全选、反选这些操作也一下子变简单function selectAll(rows) { rows.forEach(row selectedIds.add(row.id)); } function clearSelection() { selectedIds.clear(); }然后需要按选中顺序展示时直接遍历 Set 就是按加入顺序输出天然稳定。如果当时用数组存全选时要考虑去重反选时要splice或重建数组还要担心重复 push 导致数据错乱这些边界问题一个晚上都调不完。Set 把这一切从根上消灭了。4. WeakMap 和 WeakSet写给内存管理的一份“放手声明”4.1 弱引用到底弱在哪WeakMap和WeakSet名字里这个 “Weak” 经常把人吓退其实理解起来不复杂。它们的键对目标对象是“弱引用”意味着如果除了 WeakMap/WeakSet 之外没有任何其他变量再引用这个对象垃圾回收就会直接把它收走不需要你手动delete。与此对应这两个家伙都不支持遍历操作也没有size、keys、values、clear。因为一旦支持遍历引擎就得时刻跟踪所有条目弱引用就没有意义了。它们就是纯粹给“对象关联数据”用的。两者的键都必须是对象不能传原始类型。WeakMap的经典定位是“给对象附一份隐形的数据同时不污染对象本身也不阻止对象被回收”。4.2 实战场景DOM 生命周期与定时器句柄最常见的场景就是 DOM 节点。比如你给几十个按钮绑定了各自的点击次数和定时器句柄如果用 Map 存储每次手动移除 DOM 节点时都要记得把 Map 里对应的条目也删掉很容易漏。漏掉就会形成内存泄漏页面越跑越卡。用 WeakMap 之后不需要成对清理了。DOM 节点从页面移除后只要没有别的变量引用它对应条目就会被垃圾回收自动清理const pendingTimers new WeakMap(); function setupButton(btn) { const timerId setInterval(() { // 处理按钮状态 }, 1000); pendingTimers.set(btn, timerId); } function teardownButton(btn) { const timerId pendingTimers.get(btn); if (timerId ! undefined) { clearInterval(timerId); } }这个模式在处理长列表、动态渲染组件时特别重要。Vue、React 这些框架内部也大量使用 WeakMap 来管理组件实例与响应式依赖之间的关系本质上都是在解决“对象销毁后附加数据也要跟着销毁”的难题。4.3 私有变量的轻量实现还有一个很实用的玩法用 WeakMap 模拟类的私有属性。传统写法是用下划线约定_secret或者用 Symbol 加闭包但前者本质还是公开的后者写法绕。用 WeakMap 可以直接把私有数据挂在实例对象上外部拿不到const _secret new WeakMap(); class Counter { constructor(start 0) { _secret.set(this, { count: start }); } increment() { const data _secret.get(this); data.count 1; return data.count; } } const c new Counter(); console.log(c.increment()); // 1 console.log(c._secret); // undefined外部拿不到内部数据WeakMap 的键是this这个实例实例被销毁后私有数据也跟着被回收不会拖累全局。这种写法在做组件封装、工具库的时候非常舒适既保证了封装性又不用额外维护销毁逻辑。提示WeakSet 的“弱”和 WeakMap 同理但has仍然是它的核心操作。在一个消息中心系统里我曾经用 WeakSet 记录“已经初始化过的事件 emitter”避免对同一个对象重复初始化对象销毁后记录自动消失不需要手动兜底清理。5. 真实项目里的选型判断什么时候继续用对象和数组5.1 一张表看清 Map 与 Object 的边界说到底我们不是要用 Map 和 Set 取代一切而是要给数据选对容器。我的选择标准大致如下使用场景推荐容器主要原因固定字段的普通对象表单、配置、DTOObject结构简单序列化方便引擎访问属性快需要动态增删键、键类型多样的映射关系Map键可以是任意类型增删性能稳定遍历有序需要存 DOM 节点、业务对象的元数据WeakMap自动跟随目标回收不泄漏内存元素唯一、判断是否存在、集合运算SetO(1) 的 has 判断天然去重按钮、事件回调等唯一逻辑的去重注册WeakSet不阻止回收自动去重有序列表、需要排序/过滤/分页展示Array数组的方法链是处理有序数据最强的工具按照出现顺序或数值大小存键值对Map插入顺序保留没有整数字段的自动排序我见过一些“为了用 Map 而用 Map”的代码表单提交时还要先转成对象多此一举。做工程最怕“只会一把锤子”或者“把所有东西都当钉子”。固定结构的数据就该用对象Map 是给动态键值对用的两者定位本来就不同。5.2 典型方案多状态表格的存储设计再回到我开头说的表格场景综合看一篇完整的状态管理方案。除了用 Set 存选中的行我还用 Map 存每一行的额外状态比如是否禁用、选中时间、校验失败信息const selectedIds new Set(); const rowMeta new Map(); function handleSelect(row) { if (selectedIds.has(row.id)) { selectedIds.delete(row.id); rowMeta.delete(row.id); } else { selectedIds.add(row.id); rowMeta.set(row.id, { selectedAt: Date.now(), isInvalid: checkInvalid(row), }); } updateFooter(); }如果有“批量删除选中行”的需求直接遍历 Set 拿 id 去请求后端然后清空async function batchDelete() { await api.deleteRows([...selectedIds]); selectedIds.clear(); rowMeta.clear(); refreshTable(); }如果需求变了要求“选中顺序按用户点击顺序回显”Set 的遍历就是插入顺序直接输出即可。用数组做这个状态管理每一步都在小心处理去重、查找、顺序容易出错。Set 加 Map 的组合可以说是表格状态管理的低配解。5.3 我现在的默认选择经过这段时间的实践我形成了习惯凡是“键值对映射”且在运行时会变化的越过对象直接用 Map凡是“判断一件东西在不在一个集合里”的越过数组直接用 Set凡是数据要脱离引用才不会被误删的大胆用 WeakMap / WeakSet。代码可读性反而是提升的。因为 Map 的has/get/set方法能让阅读者明确知道你在做哈希表操作而obj.key value这个语法更像是在“修改一个对象”语义上少了一层“容器在动态管理键值”的暗示。团队 code review 时看到map.has()比看到arr.indexOf() -1直观得多也少了很多不必要的注释。最后说点实际操作中的体会如果这篇你只记住一句话我希望是别再一个数组走天下了。下次遇到“要判断在不在集合里”“要用复杂类型做键映射”时先想想 Map 和 Set。前端工程化走到今天数据结构早就不再是“会用数组和对象就行”的阶段Map和Set也不是面试时才拿出来背的 API它们是日常开发里真正能帮你省时间、省性能、少出 bug 的基础工具。我自己把表格选中状态从数组改成 Set 的那天顺手删掉了二十多行 patch 逻辑这种感觉是真的香。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价