Set/Map+Weak三剑客的骚操作:JS 界的 “去重王者” ,“万能钥匙”和“隐形清洁工”

前言

家人们,咱写 JS 的时候是不是总被 "数组里的重复元素" 烦到挠头?是不是吐槽过 "对象的 key 只能是字符串,太死板了" ?今天这俩 JS 界的 "宝藏工具人" ------Set和Map,直接给你把这些痛点按在地上摩擦!还有它们的 "低调兄弟" WeakSet/WeakMap,偷偷帮你解决内存泄漏,这波骚操作直接把 JS 玩明白了~

篇幅有点长,但是干货拉满!没搞懂你找我😄

一、Set:数组的 "洁癖管家",重复元素一键劝退

先给 Set 下个性感定义 :长得像数组,却容不下任何重复成员,主打一个 "宁缺毋滥"。不管你往里面塞多少个一样的,它都只留一个!就是这么洁癖,你不服也得服!

1. 基础操作:add/delete/has/clear/size,一套组合拳

先来看add(往里面添加元素):

JavaScript 复制代码
// 初始化一个空的Set实例
let s = new Set();
s.add(1); // 向 Set中添加数字1
s.add(2); // 向 Set中添加数字2
console.log(s);  

你会发现,咦?为啥打印出的结果是这个奇怪样子?前面还有个Set(2)是个什么玩意,其实这是控制台对 Set 实例 的 "友好提示" ------Set(2)表示这是一个包含 2 个成员的 Set 集合,后面跟着的{1, 2}才是 Set 里的具体成员,并不是打印结果 "奇怪",而是控制台为了让你直观看到 Set 的类型和长度,特意做的格式展示~,这也印证了Set不属于数组。

所以这里我们如果用解构的方法就不会有前面的东西:

JavaScript 复制代码
console.log(...s);   // 直接输出成员:1 2(解构为独立参数)
console.log([...s]); // 输出数组:[1, 2](转换为普通数组)

简单说,Set(2)只是控制台的 "类型标签",不是 Set 本身的内容,真正的成员就是1和2,这也是 Set 和数组在控制台展示的核心区别~

在一起看看delete、has、clear:

JavaScript 复制代码
let s = new Set([1, 2, 3, 4, 5]);
s.delete(2);  // 删除 Set中的元素2
console.log(s);  // 输出 Set(4) { 1, 3, 4, 5 },没有 2
console.log(s.has(3));  // 判断 Set中是否存在元素 3,ture
s.clear();  // 清空 Set中的所有元素,Set(0) {}
console.log(s);

❗️⭐当然这里有一个要注意的点:如果用has判断[]:

JavaScript 复制代码
let s = new Set([1, 2, 3, 4, 5]);
s.add([]);  // 增加一个[]
console.log(s.has([]));  // false,引用地址不一样

任何涉及到引用地址的,都会判断为false,核心原因 就是引用类型的 "地址唯一性" ,数组是引用类型,每一次 [] 都会创建一个全新的、内存地址不同的数组对象。

Set 的 has 方法判断元素是否存在时,对于引用类型(数组、对象等),是通过 "内存地址是否一致" 来判断,而非值是否相同。因此 has([]) 找不到之前添加的那个数组,最终返回 false。

最后就是用size获得set的长度(不要把数组的length搞混哦⚠️):

JavaScript 复制代码
let s = new Set([1, 2, 3, 4, 5]);
console.log(s.size);

2. 最实用技能:数组去重!

这绝对是 Set 的 "成名作" ,一行代码解决数组重复问题,我们大部分时候用Set目的就是为了去重,好用的飞起,不需要再用for一个一个遍历啦!

JavaScript 复制代码
const arr = [1, 2, 3, 2, 1];
let arr2 = [...new Set(arr)];  // Set 里面是允许存放数组的!
console.log(arr2);  // 解构的结果为 [ 1, 2, 3 ]

不只是数组,字符串也能去重:

JavaScript 复制代码
const str = 'abcba';
console.log(new Set(str));

3. 遍历 Set:keys/values/entries/forEach,其实都差不多😝

Set 里的 "键" 和 "值" 是同一个东西(毕竟它是单值集合 ),所以keys()和values()遍历出来的结果一毛一样。看似花里胡哨,实则逻辑超简单:

JavaScript 复制代码
let set = new Set(['a','b','c']);

// 1. keys():获取Set的"键"(Set的键和值是同一个)
for(let key of set.keys()){
    console.log(key); // 依次输出a、b、c
}

// 2. values():获取Set的值,和keys()结果完全一致
for(let val of set.values()){
    console.log(val); // 依次输出a、b、c
}

// 3. entries():返回[key, value]形式的迭代器,键值相同
for(let item of set.entries()){
    console.log(item); // 依次输出['a','a']、['b','b']、['c','c']
}

// 4. forEach遍历:和数组forEach用法一致
set.forEach((val, key) => {
    console.log(key + ':' + val); // 依次输出a:a、b:b、c:c
});

4. ⚠ 遍历不改变原数组!return返回也没用!⚠️

当我们把 Set 和 forEach 的特性结合起来时,还能发现更多有趣的细节 ------ 比如用 Set 对数组去重后,再通过 forEach 修改数组元素,依然要遵循 "直接改 item 无效、需通过索引修改原数组" 的规则:

JavaScript 复制代码
const arr = [1, 2, 3];
arr.forEach((item, i, array) => {
    item *= 10;  // 直接修改是没用滴!
})
console.log(arr); // 还是输出 [1, 2, 3]
js 复制代码
const arr = [1, 2, 3];
arr.forEach((item, i, array) => {
    arr[i] = item * 10;  // 必须通过索引!
})
console.log(arr); // 输出 [10, 20, 30]

且 forEach 里的 return 也依旧无法终止遍历:

JavaScript 复制代码
const arr = [1, 2, 3];
arr.forEach((item, i, array) => {
    if(i < 2) {
        console.log(item);
        return;  // 正常打印完 1就退出,但是结果为 1,2
    }
})

这段代码既体现 Set "成员唯一" 的核心特性,又完整复现了 forEach 修改数组的关键规则 ------ 直接操作 item 无法改变原数组、return 仅终止当前循环而非整个遍历,把 Set 和 forEach 的核心逻辑紧密串联了起来。

5.🌈判断能否遍历的小技巧

假设你不知道Set可以遍历,那怎么判断呢?一招搞定,那就是直接去浏览器上打印出一个Set对象,看看里面有没有iterator这个方法,如果有,那就👌(^o^)/~,大胆放心遍历!

二、Map:传统对象的 "超级进化版",key 想放啥就放啥

传统 JS 对象的痛点:key 只能是字符串或 Symbol ,想拿对象当 key?门都没有!但 Map 直接打破这个限制 ------ 数字、数组、对象、甚至 null 和 undefined ! 啥都能当 key,堪称 "万能键值对容器"。

1. 基础操作:set/get/has/size/delete/clear(跟Map差不多!)

JavaScript 复制代码
const m = new Map();
// 各种奇奇怪怪的 key 都能放
m.set('hello', 'world'); // key是字符串
m.set([], 1); // key是数组
m.set(null, 2); // key是null
console.log(m.size); // Map 的长度,输出 3

console.log(m.get(null)); // 输出 2,精准取到 null对应的值
console.log(m.has([])); // 输出 false!注意:数组是引用类型,这里的[]和set的[]不是同一个对象!
m.delete(null); // 删除 key为 null的项
m.clear(); // 清空 Map

2. 遍历 Map:比对象遍历爽多了

Map 天生支持遍历,不用像对象那样 "转数组再遍历"。好我现在假设不知道可以遍历,大声告诉我怎么办?😮看来你会了:

JavaScript 复制代码
const m = new Map([['name', 'henry'], ['age', 18]]);
// 直接用for...of遍历,拿到[key, value]
for (let [key, val] of m) {
    console.log(key, val); // 输出name henry、age 18
}

这里依旧跟Map一样的问题 (引用地址不同):

JavaScript 复制代码
const arrKey = [];
const m = new Map();
m.set(arrKey, '我是数组键的值');
console.log(m.get(arrKey)); // 输出"我是数组键的值"(引用地址一致)
console.log(m.get([])); // 输出undefined(新数组,地址不同)

三、WeakSet/WeakMap:JS 内存的 "隐形清洁工",弱引用太香了

聊完 Set 和 Map,必须提它们的 "低调兄弟" ------WeakSet 和 WeakMap,这俩主打一个 "弱引用" ,堪称内存泄漏的 "克星",一个守护 Set 体系,一个守护 Map 体系,分工明确又超实用!甚至有些前端开发者都不知道有这俩玩意!必须补充上⬆️!

1. WeakSet:Set 的 "内存友好版",只存对象 + 自动回收

WeakSet 是 Set 的 "轻量版",核心规则先划重点:

  • 只能存储对象类型 (数字、字符串等原始类型一概不收,存了也白存);
  • 对存储的对象是弱引用 :如果外部没有其他引用指向这个对象 ,垃圾回收机制会自动把 WeakSet 里的这个对象清理掉,绝不占内存;
  • 不可遍历 (没有 keys ()/values ()/forEach 等遍历方法),也没有 size 属性,主打一个 "默默干活不露面"。

错误示例:向WeakSet添加原始类型(会直接报错):

JavaScript 复制代码
const wsError = new WeakSet();
try {
    // 尝试添加数字(原始类型),会抛出TypeError
    wsError.add(123); 
} catch (err) {
    console.log('报错信息:', err.message); // 输出:Invalid value used in weak set
}

正确示例:WeakSet仅存储对象+弱引用特性:

JavaScript 复制代码
// 1. 初始化WeakSet
const ws = new WeakSet();

// 2. 定义对象(只有对象能存入WeakSet)
let obj1 = { name: 'JS玩家1' };
let obj2 = { name: 'JS玩家2' };

// 3. 向WeakSet添加对象(正常生效,无报错)
ws.add(obj1);
ws.add(obj2);

// 4. 判断对象是否存在(返回布尔值)
console.log('obj1是否在WeakSet中:', ws.has(obj1)); // 输出:true
console.log('obj2是否在WeakSet中:', ws.has(obj2)); // 输出:true

// 5. 删除指定对象(返回布尔值,存在则删除并返回true)
ws.delete(obj2);
console.log('删除obj2后,obj2是否存在:', ws.has(obj2)); // 输出:false

// 6. 弱引用核心演示:外部销毁obj1的引用
console.log('销毁obj1前,obj1是否存在:', ws.has(obj1)); // 输出:true
obj1 = null; // 外部不再引用obj1
// 此时JS垃圾回收器(GC)会在合适时机自动清理WeakSet中obj1的引用
// 注意:无法通过代码直接验证回收结果(WeakSet不可遍历、无size属性),但原理是确定的

补充:WeakSet不支持的操作(避免踩坑):

JavaScript 复制代码
try {
    // WeakSet无size属性,访问会报错
    console.log(ws.size); 
} catch (err) {
    console.log('访问size报错:', err.message); // 输出:ws.size is undefined
}

try {
    // WeakSet不可遍历,forEach会报错
    ws.forEach(item => console.log(item)); 
} catch (err) {
    console.log('forEach遍历报错:', err.message); // 输出:ws.forEach is not a function
}

2. WeakMap:Map 的 "内存友好版",键仅对象 + 自动回收

WeakMap 是 Map 的 "专属内存管家" ,核心规则和 WeakSet 呼应,更贴合键值对场景:

  • 键只能是对象类型(原始类型当键直接无效);
  • 对键的引用是弱引用:如果外部没有其他引用指向这个键对象,垃圾回收机制会自动回收这个键值对,彻底杜绝内存泄漏;
  • 不可遍历(没有 keys ()/values ()/entries () 等方法),也没有 clear () 方法,主打 "用完即走不拖沓"。
JavaScript 复制代码
// 1. 初始化WeakMap
let wm = new WeakMap();

// 2. 定义对象作为键(符合 WeakMap的键要求)
let obj = {name: 'JS玩家'};

// 3. 添加键值对(键是对象,正常生效)
wm.set(obj, '这是WeakMap的值');

// 4. 查看值:成功获取
console.log(wm.get(obj)); // 输出:这是WeakMap的值

// 5. 外部销毁obj的引用
obj = null;
// 此时WeakMap中obj对应的键值对会被垃圾回收器自动清理(无法通过代码直接验证,是内存层面的行为)

// 6. 尝试用原始类型(字符串)当键:直接报错!
try {
    wm.set('hello', 'world'); // WeakMap不允许原始类型作为键,执行到这行就会抛错
    console.log(wm.get('hello')); // 这行代码永远不会执行
} catch (err) {
    console.log('错误原因:', err.message); // 输出:错误原因: Invalid value used as weak map key
}

3. 为啥需要 WeakSet/WeakMap?为啥有些开发者甚至不知道它俩?

比如做 DOM 元素的状态管理,用 WeakMap 存 DOM 元素对应的状态:

JavaScript 复制代码
// 假设页面有个按钮元素
const btn = document.querySelector('#myBtn');

// 用 WeakMap存按钮的点击次数
const btnClickCount = new WeakMap();
btnClickCount.set(btn, 0);

// 按钮点击时更新次数
btn.addEventListener('click', () => {
    let count = btnClickCount.get(btn);
    btnClickCount.set(btn, count + 1);
    console.log('点击次数:', count + 1);
});

// 如果后续按钮被移除(比如btn = null),WeakMap里的键值对会自动回收,不会内存泄漏!
// 要是用普通Map,即使btn被移除,Map依然持有强引用,内存会一直被占用,这就是差距~

四、总结

1. 最后唠两句(核心点):

  • Set 核心是唯一值集合,主打数组去重,支持 add/delete/has/clear 等操作,无法通过索引取值;

  • Map 核心是万能键值对,键可以是任意类型,弥补传统对象短板,支持 set/get/delete/has/clear;

  • WeakSet/WeakMap 主打弱引用 + 自动回收,仅存 / 仅以对象为键,不可遍历,是解决内存泄漏的绝佳方案。

2. 一张表理清 Set/Map/WeakSet/WeakMap 核心区别:

特性 Set Map WeakSet WeakMap
存储形式 单值集合(无键值) 键值对集合 单值集合(仅对象) 键值对集合(键仅对象)
成员 / 键类型 任意类型 键:任意类型; 值:任意 仅对象 键:仅对象;值:任意
引用类型 强引用 强引用 弱引用 弱引用(仅键)
遍历性 可遍历 可遍历 不可遍历 不可遍历
内存回收 手动清空 手动清空 自动回收无引用对象 自动回收无引用键对象
特殊属性 有 size 有 size 无 size 无 size

结语

Set 就像 "去重神器" ,解决数组重复问题手到擒来;Map 是 "万能键值对" ,弥补了传统对象的短板;WeakSet/WeakMap 则是 "内存管家",默默帮你清理无用内存,杜绝泄漏。

记住核心用法:

  • 去重、存唯一值 → 用 Set;
  • 非字符串键的键值对存储 → 用 Map;
  • 存对象且怕内存泄漏 → 存单值用 WeakSet,存键值对用 WeakMap。

把这四个玩明白,JS 数据存储的坑能少踩一大半,效率直接拉满!赶快用起来!

需要了解其他数据类型的读者可以看我的文章:栈与堆的精妙舞剧:JavaScript 数据类型深度解析

附上ES6的原文资料:es6.ruanyifeng.com/#docs/set-m...

相关推荐
zach31 分钟前
前端落地实战:Next.js 项目 Nginx + PM2 生产部署全流程(附踩坑总结)
前端·pm2·next.js
overmind40 分钟前
oeasy教h5前端001 修改的乐趣_直接修改在线网页
前端
只睡四小时1 小时前
JS 手写 D-pad 空间导航:电视端焦点引擎实战
android·开发语言·前端·javascript·ecmascript·hls·空间导航
恋猫de小郭1 小时前
Android 原生的 Compose A2UI 也来了,你还抱着 XML 养老吗?
android·前端·flutter
庄园特聘拆椅狂魔1 小时前
从连连看到粒子消散——前端小游戏与视觉特效的技术选型
前端
JudithHuang1 小时前
React 常用 Hooks
前端·react.js·前端框架
sycmancia2 小时前
Qml——Window元素使用
前端
每天都好困2 小时前
03 AR 投影与误差修正:把地图上的点贴到视频画面上
前端
每天都好困2 小时前
04 地图图层封装:像管理 Photoshop 图层一样管理地图元素
前端
去伪存真3 小时前
构建你的第一个 DevOps AI Agent:自动化捕获、分析并修复 CI 故障
前端·agent