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

前言

家人们,咱写 JS 的时候是不是总被 "数组里的重复元素" 烦到挠头?是不是吐槽过 "对象的 key 只能是字符串,太死板了" ?今天这俩 JS 界的 "宝藏工具人" ------SetMap,直接给你把这些痛点按在地上摩擦!还有它们的 "低调兄弟" 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 本身的内容,真正的成员就是12,这也是 Set 和数组在控制台展示的核心区别~

在一起看看deletehasclear

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返回也没用!⚠️

当我们把 SetforEach 的特性结合起来时,还能发现更多有趣的细节 ------ 比如用 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 内存的 "隐形清洁工",弱引用太香了

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

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

WeakSetSet"轻量版",核心规则先划重点:

  • 只能存储对象类型 (数字、字符串等原始类型一概不收,存了也白存);
  • 对存储的对象是弱引用 :如果外部没有其他引用指向这个对象 ,垃圾回收机制会自动把 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 的 "内存友好版",键仅对象 + 自动回收

WeakMapMap"专属内存管家" ,核心规则和 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...

相关推荐
xuedaobian2 小时前
2025年我是怎么用AI写代码的
前端·程序员·ai编程
saberxyL2 小时前
前端登录加密与Token管理实践
前端
凛_Lin~~2 小时前
安卓 面试八股文整理(原理与性能篇)
android·java·面试·安卓
3秒一个大2 小时前
React 中 Context 的作用与用法:从主题切换案例说起
前端·react.js
2501_944446002 小时前
Flutter&OpenHarmony文本输入组件开发
前端·javascript·flutter
AI前端老薛2 小时前
你了解react合成事件吗
前端·react.js·前端框架
WebRuntime2 小时前
所有64位WinForm应用都是Chromium浏览器(2)
javascript·c#·.net·web
贺今宵2 小时前
2025.electron-vue3-sqlite3使用
前端·javascript·electron
王同学_1163 小时前
爬虫辅助技术(css选择器、xpath、正则基础语法)
前端·css·爬虫