各位前端同行、刚入行的小朋友这篇是来聊 JavaScript 里一组非常实用、却长期被低估的数据结构Map 和 Set。如果你平时习惯了用 Object 当字典、用 Array 处理去重和集合判断我建议你把这篇看完它解决的问题不是我拍脑袋想出来的而是真实业务里每天都会遇到的对象键的类型限制、遍历顺序不稳定、频繁增删时的性能损耗、数组去重还要写一堆 filter 逻辑。这篇内容适合所有阶段的 JavaScript 开发者无论你是刚学完基础语法还是已经写了几年业务代码都能从中拿回一些可以直接抄作业的东西。关于 Map 和 Set 的知识点网上教程很多但大多只告诉你“有哪些方法”很少解释“为什么用这个以及它的边界在哪里”。我这些年做项目踩过不少坑也积累了一些实测数据所以想用一篇完整篇幅把概念、用法、对比、实战、排错一次性讲透。内容会保持一贯的直接风格不绕弯子能用的代码直接贴能踩的坑提前告诉你。1. 为什么需要 Map 和 Set从 Object 和 Array 的痛点说起1.1 把 Object 当字典用为什么总有点别扭我在刚写 JavaScript 的时候和大多数人一样需要存键值对就直接上一个对象字面量const dict {}; dict[name] 张三; dict[age] 30;这套用法在简单场景下没问题但只要业务复杂起来你就会逐渐感觉到几个难以忍受的点。第一个痛点是键只能是字符串或者 Symbol。对象在底层实现里key 最终都会被转成字符串。这意味着如果你拿一个数字 1 和一个字符串 1 去存同一个对象它们实际上是同一个键。更麻烦的是如果你想用另一个对象来做 key比如缓存某个组件实例对应的数据Object 根本做不到因为它会把对象强制转成[object Object]这种字符串导致所有对象键全部冲突覆盖。这个问题在业务代码里不是没遇到过多少人写缓存结构时就是因为 Object 的 key 限制最后被迫手动加 id 字段或者用数组去循环查找性能又差又丑。第二个痛点是遍历顺序在某些情况下不稳定。虽然在现代引擎里整数类型的键会按升序排列字符串键按插入顺序排列但这两者混合时顺序就会产生让人困惑的结果。比如插入了b、10、a、2这几个键Object.keys 返回的数组顺序往往并不是你添加的顺序。对于依赖插入顺序做业务处理的场景比如实现一个先进先出的缓存队列Object 就会给你埋一颗雷。第三个痛点是原型链污染和安全性问题。当你使用普通对象存储用户输入数据时如果键名恰好是constructor、toString、__proto__之类就很容易踩到原型链相关的坑。虽然现代开发者一般不会直接赋值到__proto__上但只要你做过对象合并、数据清洗对这个风险一定不陌生。为了规避这个问题你被迫使用Object.create(null)或者每次调用hasOwnProperty去兜底写得又累又容易漏。Map 的出现就是冲着解决这三类痛点来的。它的 key 可以是任意数据类型包括对象、函数、NaN而且记住插入顺序。更重要的是Map 的结构天然就是为“字典”设计的get、set、has、delete这些方法语义明确不需要一次一次地Object.prototype.hasOwnProperty.call。我自己在重构旧代码时凡是把普通对象当纯数据字典使用的地方几乎都替换成了 Map代码简洁度提升非常明显。1.2 用数组处理集合类业务麻烦不是一点点数组去重是很多前端每天都在写的逻辑。用 Set 之前的常见写法是const arr [1, 2, 2, 3, 4, 4]; const unique []; for (const item of arr) { if (!unique.includes(item)) { unique.push(item); } }这段代码在数据量小的时候没问题但你要知道includes是 O(n) 的查找外层再套一层循环整体就是 O(n²)。数据量从几百涨到几万的时候页面卡顿就会变得肉眼可见。类似的场景还有“判断某个值是否存在”以前写arr.indexOf(x) ! -1或者arr.includes(x)都逃不掉线性查找。集合类业务还有更复杂的场景并集、交集、差集。用数组做这些运算代码会写得很长而且可读性差。比如求两个数组的交集先要去重还要嵌套循环加includes判断最后得到的结果还可能包含重复值处理起来非常烦人。另外数组还缺少一种“集合语义”。比如一个列表数据你想要的是“某个值是否在里面”这个信息本身而并不关心它的位置、数量数组却把所有的功能混在一起。这就导致代码里到处都是filter includes之类组合式逻辑看久了会头疼。Set 就是专为集合语义设计的数据结构。它天然保证元素唯一has方法在底层是哈希查找平均复杂度 O(1)比数组的线性查找快一个量级。更重要的是配合展开运算符Set 可以在几行代码内完成交集、并集、差集运算这个我会在后面的章节里展开先记住结论凡是涉及“唯一性”、“成员关系判断”、“集合运算”这三个关键词的业务Set 都是比数组更正确的选择。2. Map 的核心知识拆解从声明方法到遍历技巧2.1 Map 的声明与三种初始化方式各有什么讲究Map 的创建方式非常简单最常见的就是new Map()然后通过set方法往里填数据。还有一个非常实用的特性是它可以直接接收一个可迭代对象作为构造参数最典型的就是二维数组。// 方式一空 Map逐步添加 const map new Map(); map.set(name, 李四); map.set(age, 25); // 方式二从二维数组初始化 const map2 new Map([ [name, 王五], [age, 28], [job, 前端工程师], ]); // 方式三从另一个 Map 复制 const map3 new Map(map2);这里我想多说一句使用细节当你从数组初始化 Map 时内层的每个成员必须是“键值对数组”也就是一个长度为 2 的可迭代对象。如果你写错成了多层的对象数组Map 会先把对象转换成键再把 undefined 作为值很容易埋下隐患。我在实际开发里见过一些同事这么写当时排查半天才发现是这个原因。还有一个常见的痛点疑问Map能不能用对象字面量那样的方式直接声明答案是不能Map 没有直接的字面量语法必须通过new Map()构造。这一点在 JS 社区里经常被吐槽但使用习惯之后你会觉得这样反而清晰因为它明确区分了“纯数据容器”和“对象”。有一个建议给到大家如果是接口返回的数据一般还是保留对象结构不要强行转 Map一旦你明确要利用 Map 的特性做频繁增删、键类型不固定、顺序敏感的操作时再转换成 Map 不迟。我自己习惯在工具函数里做一个类似的转换封装function objectToMap(obj) { return new Map(Object.entries(obj)); } function mapToObject(map) { return Object.fromEntries(map); }这样在两种结构之间切换就很自然了而且Object.fromEntries是原生方法不需要额外依赖。2.2 核心方法与遍历方式和 Object 对比后你就明白差异Map 的方法体系非常完整核心的几个分别是set、get、has、delete、clear、size。其中size是属性而不是方法这点和数组的length类似和 Object 需要用Object.keys(obj).length才能拿到键数量形成鲜明对比。每次看到新同事用Object.keys(obj).length统计键数量时我都想推荐他直接换成 Map。set方法返回的是 Map 自身所以支持链式调用这是一些框架源码里常见的写法const map new Map() .set(a, 1) .set(b, 2) .set(c, 3);get方法在键不存在时返回undefined这一点有点坑因为可能你的业务里某个值本身就是undefined所以一般建议配合has方法先做判断或者使用默认值兜底const value map.has(key) ? map.get(key) : default;Map 的遍历方式非常灵活。你可以直接用for...of遍历整个 Map每次迭代拿到的就是[key, value]这样的数组这是 Map 和 Object 一个非常显著的差异Object 默认是不可迭代的Map 默认就是可迭代的。for (const [key, value] of map) { console.log(key, value); } map.forEach((value, key) { console.log(key, value); });还可以单独遍历键或值for (const key of map.keys()) {} for (const value of map.values()) {} for (const [key, value] of map.entries()) {}这里请大家注意一个重要细节Map 的forEach回调参数是(value, key)值在前键在后。这和数组的forEach (item, index)的直觉不一致我第一次写时就被坑过。习惯性写上(key, value)之后发现打印的是反的当时定位这个问题浪费了几分钟所以在文章里先帮你排掉这个雷。遍历顺序方面Map 严格按照插入顺序来这一点在实现“先进先出缓存”、“按添加顺序渲染列表”等业务时非常友好。另外Map 结构内部的键比较用的是SameValueZero算法简单理解就是和严格相等基本一致但有一个例外NaN在 Map 里被认为是和自身相等的。所以你完全可以这么写map.set(NaN, not a number); map.get(NaN); // not a number这在 Object 里是做不到的因为obj[NaN]会被转成字符串键。如果面试时被问到“Map 和 Object 的键有什么本质区别”记得把这个点说进去。2.3 判断数据类型怎么区分 Map 和 Object热词里一直有人问 JavaScript 判断数据类型的问题。当你面对一个变量不确定它是不是 Map 时最稳妥的方式是Object.prototype.toStringconst m new Map(); const obj {}; Object.prototype.toString.call(m); // [object Map] Object.prototype.toString.call(obj); // [object Object]使用typeof对 Map 判断只会得到object根本区分不了。使用instanceof Map在跨 iframe 或者跨模块复制场景下偶尔会失效因为不同全局环境有各自的 Map 构造函数。所以我更推荐上面的toString方案这同样适用于 Set、WeakMap、WeakSet 这些类型。还有一个非常容易被忽略的问题JSON.stringify(new Map())的结果是{}。因为 Map 没有普通对象的可枚举属性序列化时默认什么都不会输出。这导致你如果要把 Map 传给后端直接 JSON 序列化会得到空对象。解决办法要么是把 Map 转换成数组或者普通对象要么自定义toJSONconst map new Map([[a, 1]]); JSON.stringify([...map]); // {a:1}下发时转数组 JSON.stringify(Object.fromEntries(map)); // {a:1}我实际项目里通常用[...map]的方式传递给后端因为后端拿到的是一个二维数组每个键值对顺序是明确的语义也更清晰。当然和团队成员沟通好、定好接口约定就行但一定要避免直接把 Map 抛给JSON.stringify。3. Set 的核心知识拆解从去重到集合运算3.1 Set 的基础用法add、has、delete 这些方法够用吗Set 的基础 API 比 Map 更简单一些主要包括add、has、delete、clear、size。有一点要注意Set 没有get方法因为 Set 里的“值”本身就是它唯一的标识你想取某个值只要它存在你自然已经有了不需要通过键去取。const s new Set(); s.add(1); s.add(2); s.add(2); // 重复添加无效 s.size; // 2 s.has(2); // true s.delete(2); // trueadd方法同样支持链式调用因为返回的是 Set 自身const s new Set().add(a).add(b).add(c);Set 的遍历也是按照插入顺序来的。你可以用for...of直接遍历成员也可以用forEach。需要注意这里有个奇怪的对称特性Set 的forEach回调参数是(value, key)但 Set 里没有 key所以它特意让 key 和 value 取相同的值。这本质上是为了让 Set 的遍历接口和 Map 保持一致。如果你写代码时忽略了这个设计只用到第一个参数就没有问题。Set 里的值去重规则和 Map 的键一致使用SameValueZero。这意味着const s new Set([NaN, NaN, NaN]); s.size; // 1 const obj { a: 1 }; const s2 new Set([obj, obj, { a: 1 }]); s2.size; // 2两个 obj 是同一个引用所以去重{ a: 1 } 是新对象所以不去重最后这点非常关键Set 去重针对的是引用地址不是对象的结构。很多同学以为new Set([{a:1}, {a:1}])会去重成一项实际不会两个对象各自独立。所以如果你想按照对象某个字段去重需要先做一次映射这个在后面的业务场景我会再提。3.2 一行代码实现数组去重和集合运算数组去重是 Set 最出圈的应用核心代码一行const uniqueArr [...new Set(arr)];或者使用Array.from(new Set(arr))结果一样。两者的区别不大Array.from的可读性稍微好一点而展开运算符写起来更简洁。如果你要兼容非常旧的环境用的是 Babel 之类的方案这两个都没有问题。既然可以快速去重那么集合运算自然也很容易实现了。假设有两个 Set分别保存了集合 A 和集合 Bconst a new Set([1, 2, 3, 4]); const b new Set([3, 4, 5, 6]); // 并集 const union new Set([...a, ...b]); // 交集 const intersection new Set([...a].filter(x b.has(x))); // 差集在 A 中但不在 B 中 const difference new Set([...a].filter(x !b.has(x)));这套逻辑在实际开发中用途非常广泛。比如你有一批“全部用户 ID”又有一批“已禁用用户 ID”要生成“可用用户 ID”直接用差集三行代码就出来了。标签系统、权限系统、角色对比、关键词过滤几乎都是这个套路。额外提醒一个小坑filter返回的是数组再包一层new Set是为了保证唯一性。如果你的初始数组本身没重复也可以不包。但为了防止上游数据不可控我建议始终用new Set包一层这个习惯可以帮你省掉很多线上才暴露的 bug。3.3 WeakSet 是什么它和 Set 有什么区别这套内容如果不提 WeakSet 总感觉不完整。WeakSet 和 Set 一样都是不重复的集合但它有两条重要限制第一成员只能是对象第二它内部持有的是对象的弱引用不会影响垃圾回收机制。什么意思呢打个比方如果你有一个 Set里面存了一个对象即使这个对象在外层的所有引用都被清掉了Set 本身还维持着引用导致对象无法被回收内存就会一直被占用。而 WeakSet 不持有强引用当对象外部引用消失后它在 WeakSet 里也会随之被回收掉。const ws new WeakSet(); let node document.querySelector(.box); ws.add(node); node null; // 这个 DOM 节点后续可以被垃圾回收了这种特性适合做“标记类”业务。比如你要记录哪些用户已经注册过事件处理函数哪些按钮被初始化过用 WeakSet 存对象引用就不用担心删除时忘记从集合里移除造成内存泄漏。不过与之对应的限制是WeakSet 不能遍历也没有size属性因为它内部成员状态不稳定随时可能被回收。所以它只能做存在性判断不能做过程式遍历。理解了这个取舍你就知道什么时候用 Set、什么时候用 WeakSet 了。4. Map 和 Set 的实战对比从性能到场景选择4.1 性能实测Map 和 Object 到底谁更快我先把结论放在前面在频繁增删键值的场景下Map 的性能明显优于 Object在只读、静态配置的场景下两者差距不大。这里我基于自己本地 Node.js 18 环境做过的粗略测试给一个参考数据。我构造了一个包含 10 万条数据的场景分别使用 Object 和 Map测试“写入 10 万个键”和“读取 10 万个键”的耗时取多次测试的平均值后Map 的写入大概比 Object 快 20% 到 50%读取快 10% 到 30% 左右。在删除键的场景下差距更明显因为delete obj.key对 V8 引擎对象隐藏类的影响比较大可能让整个对象变成慢模式而 Map 的delete操作没有这个负担。下面是我测试用的大致代码const count 100000; const obj {}; const map new Map(); console.time(object-set); for (let i 0; i count; i) { obj[key_${i}] i; } console.timeEnd(object-set); console.time(map-set); for (let i 0; i count; i) { map.set(key_${i}, i); } console.timeEnd(map-set);有人会问为什么 Object 在某些场景下写入慢这和 V8 的隐藏类优化机制有关。当你不断往对象上添加新属性时引擎需要频繁迁移隐藏类或者在属性过多时落到字典模式这个转换本身是有性能开销的。而 Map 从设计上就是一个哈希表容器不需要经历这种“对象形状推断”过程所以在大规模动态增删时更稳定。做项目时我的建议是如果是静态配置项、接口返回的数据结构用 Object 就好语义清晰写起来也方便。如果是一份运行时不断变化的数据映射比如在线用户表、缓存 LRU、上报事件聚合优先 Map。如果是算法题里频繁读取节点的邻接表这种场景Map 基本是标配。另外不要忽略for...in遍历 Object 和for...of遍历 Map 的性能差异。for...in本身需要枚举属性而且要遍历原型链通常比直接迭代 Map 慢一个量级。如果你在处理大数据集合这个差异可能直接决定了接口响应时间。4.2 业务场景串讲复选框选中、订单去重、缓存管理理论堆一堆不如直接看场景。第一个场景管理表格复选框选中态。以前常见写法是这样const selected {}; selected[rowId] true; // 取消选中 delete selected[rowId];这个写法的问题是 rowId 会被转成字符串而且如果 rowId 有特殊字符收发数据时容易出问题。换成 Set 后const selected new Set(); // 选中 selected.add(rowId); // 取消选中 selected.delete(rowId); // 判断是否选中 selected.has(rowId); // 获取选中数量 selected.size; // 批量清空 selected.clear();这一个例子几乎覆盖了 Set 的所有核心方法而且代码读起来就是自解释的不需要额外的注释。我在实际后台管理项目里就是这样做的后来同事接手代码时反馈说“这套看着太舒服了”。第二个场景订单数据去重。比如 WebSocket 持续推送订单状态你需要在本地维护一个“待处理订单列表”。直接拿数组push会让重复订单越积越多使用 Set 封装一层管理代码干净而且天然防止重复const orderSet new Set(); function handleIncomingOrders(orders) { for (const order of orders) { if (!orderSet.has(order.id)) { orderSet.add(order.id); processOrder(order); } } }当你需要把当前订单 ID 列表提交给后端时直接[...orderSet]就行。这里顺带提一句如果订单 ID 要从Set转成数组并传给接口不要忘了Array.isArray是判断不了 Set 的输出前确认你用的是展开运算符或者Array.from。第三个场景缓存管理器。用 Map 做缓存天然支持任意类型 key配合对象键可以很方便地实现“组件实例级缓存”。比如要根据某个组件实例缓存对应配置const cache new Map(); function getComponentConfig(instance) { if (cache.has(instance)) { return cache.get(instance); } const config loadConfig(instance); cache.set(instance, config); return config; }这个用 Object 要怎么实现你会发现你没法拿实例本身当 key只能手动给实例加 id或序列化成字符串麻烦且容易出错。Map 的存在直接改变了这类代码的复杂度。4.3 Map 和 Set 的组合玩法注意别写出隐性问题Map 和 Set 不是两个孤立的数据结构它们经常组合在一起用。最常见的玩法是“Map 的 value 放 Set”比如维护一个“分类到标签列表”的映射const tagMap new Map(); function addTag(category, tag) { if (!tagMap.has(category)) { tagMap.set(category, new Set()); } tagMap.get(category).add(tag); }这样分类下的标签自动去重。删除分类时直接tagMap.delete(category)整个 Set 也会一起被清掉内存回收是自洽的不需要额外清理内部成员这是组合式容器很好用的一点。再比如利用 Set 的集合特性和 Map 的属性做“多重分组统计”现在有一批订单需要按状态分组同时每个组内订单不能重复。你可以用一个 Mapkey 是状态value 是 Set 存订单 ID。前端做仪表盘统计时每来一条新消息只需要做两次哈希判断一次判断组是否存在一次判断订单是否已存在整体耗时可控代码结构清晰。这里提醒一个组合时的“隐性问题”如果你从 Map 里取出来的 Set 被外部变量持有了那么外部对 Set 的修改会直接影响到 Map 中的值。这在有些情况下是你要的效果但在某些场景下会成为 bug 来源。比如你只想读取某个分类下的标签数量却无意中往里 add 了一个新标签就会污染原始数据。如果你不希望外部修改可以考虑返回副本function getTags(category) { return tagMap.get(category) ? new Set(tagMap.get(category)) : new Set(); }这种“防御性副本”思想在接口开发和多人协作时特别有用成本不高但可以避免很多莫名其妙的数据污染问题。5. 常见问题与排查技巧实录5.1 遍历时删除成员为什么经常出问题在遍历 Set 或者 Map 时直接删除当前成员是高频踩坑点。刚开始使用 Set 时我写过类似代码const s new Set([1, 2, 3, 4]); s.forEach(item { if (item % 2 0) { s.delete(item); } });这段代码在多数情况下看起来没问题因为 Set 的 forEach 本身对删除当前项是安全的。但如果你在遍历过程中删除的是“未来还没遍历到的元素”结果就可能出乎意料。比如const s new Set([1, 2, 3]); for (const item of s) { if (item 1) { s.delete(3); } }虽然不会直接报错但遍历结果会跳过 3 还是继续访问 3取决于引擎内部实现行为不确定。更稳妥的做法是先把要删除的成员收集到临时数组遍历结束后统一删除const toDelete []; for (const item of s) { if (item % 2 0) { toDelete.push(item); } } toDelete.forEach(item s.delete(item));Map 也是同理不要在遍历时直接修改键集合的大小。如果你使用的是clear清空整个 Map也要知道迭代器会直接终止后续代码逻辑可能受影响。经验法则是遍历容器的同时只读不写要写就到遍历之后再写。5.2 序列化和深拷贝Map 与 Set 的隐藏陷阱开篇我提到了JSON.stringify(new Map())得到{}。这个问题在配合 axios 发送请求参数时特别常见你满心以为传了一个对象结果接口收到的是一个空结构排查到头才发现是 Map 根本不能直接被 JSON 序列化。解决方案上文给了用展开运算符或Object.fromEntries转换即可。Set 的序列化同样不能用 JSON 完成。如果要用JSON.stringify传输 Set你要么先[...set]转成数组要么写一个replacer自定义序列化逻辑。不过做前端存储的时候我更推荐直接利用浏览器原生支持的方式localStorage.setItem(tags, JSON.stringify([...tagSet])); // 读取时 const tagSet new Set(JSON.parse(localStorage.getItem(tags)));这个模式我用了很久非常稳定。因为Set构造器可以直接接收数组转回 Set 只需要一行。还有一个容易踩的点发生在深拷贝库上。很多流行的深拷贝实现并不天然支持 Map 和 Set或者支持度各有差异。比如JSON.parse(JSON.stringify(obj))遇到 Map 直接变空对象遇到 Set 直接变空数组。使用 Lodash 的cloneDeep是可以支持 Map 和 Set 的但如果你在团队里换了一个轻量级拷贝函数可能就会踩坑。所以当项目里出现自定义深拷贝工具时记得确认它对这两个数据结构的处理逻辑别等线上才暴露。5.3 引用类型 key 的坑为什么存进去取不到Map 的 key 可以是对象这是优点但很多初学者会在这上面翻车。const map new Map(); function setCache(info) { map.set({ id: info.id }, info); } function getCache(id) { return map.get({ id }); }上面这段代码你永远取不到值因为两次创建的对象引用地址完全不同。Map 判断键是否相等使用SameValueZero对于对象来说比较的是引用地址不是结构内容。很多人上段代码一跑发现get是undefined以为是 Map 有问题其实是用错了。正确做法是使用同一个引用或者把对象的某个唯一属性拆出来作为键const map new Map(); function setCache(info) { map.set(info.id, info); } function getCache(id) { return map.get(id); }还有一个关联坑如果你把一个对象存进 Set然后修改了这个对象的属性Set 里的成员仍是同一个引用不会自动替换成新对象。如果你依赖对象内容做唯一性判断一定要在一个不可变的引用上操作或者设计专门的 key 字段否则会出现“内容变了但集合里还是旧数据”的诡异 bug。另外不要把NaN的键和-0、0搞混。Map 和 Set 使用SameValueZero所以-0和0被认为是相同键NaN和NaN相同。如果你在处理数值去重时同时出现了-0和0它们会被合并成一项。这个设计跟Object.is(-0, 0) false不一样是容易混淆的点。5.4 迭代器操作频繁报错先检查调用对象对不对最后整理一下和迭代器相关的报错。最常见的报错信息是map[Symbol.iterator] is not a function或者set is not iterable。这个其实不是你代码逻辑写错了多半是你把普通对象当成了类数组在用。比如有些人会对Object使用for...of结果直接报obj is not iterable。这不是 Object 的错因为普通对象没有实现迭代器协议。如果你需要遍历对象键值请用Object.entries之后再遍历或者先转成 Map。另一个相关问题是你定义了一个 Map但在传给函数时函数内部用...map展开这完全没问题因为 Map 默认迭代器返回的就是[key, value]数组。但如果你在一个自定义的类里定义了[Symbol.iterator]方法方法内部返回了另一个迭代器这时忘记return下一句代码也会造成iterator is not a function这类错误。每次遇到迭代器相关报错时可以按三步排查第一确认遍历对象是不是 Map/Set/数组第二确认迭代器有没有正确实现第三确认是不是在for...of里使用了break或提前return导致生成器状态错乱。我在真实项目里还遇到过一种情况后端返回数据时传过来一些特殊对象前端用第三方库包装之后试图for...of结果报格外奇怪的错误。后来发现是第三方库的某些属性在某些版本下不保证可迭代。这类问题没有通用解但检查顺序里一定包含“确认当前对象是否真的是 Map 或 Set”。最后分享一点我的个人习惯写了这么多代码最后说点实在的。Map 和 Set 上手门槛不高但真正能在项目里用好养成两个习惯很重要。一个习惯是之后你写字典类数据结构时先停下来想一下这里用 Object 到底合不合适如果键类型不确定、需要频繁增删、顺序敏感就大胆用 Map。第二个习惯是涉及到“是否包含某个值”的地方别再下意识地indexOf或includes了先看场景是否适合用 Set。把这两个习惯坚持半年你写出的代码在可读性、性能方面都会上一个大台阶。有人说这些都是 ES6 的老知识了但老知识能用的多才算真的掌握了希望这篇能帮你把基础补扎实后续在上层框架和复杂业务里少踩一些坑。
阅读完成 · 觉得有帮助?