JavaScript学习笔记:10.集合

JavaScript学习笔记:10.集合

上一篇吃透了数组这个"万能收纳箱",但开发中总会遇到数组和对象搞不定的场景:想快速给数组去重却要写循环判断,用对象存键值对却发现键只能是字符串/符号,缓存DOM元素时不小心造成内存泄漏......这时候,JS的"高级收纳工具"------集合(Keyed Collections)就该登场了。

集合家族主要有四位核心成员:Set(无重复值的集合)、Map(键值对集合,键可任意类型)、WeakSet(弱引用版Set)、WeakMap(弱引用版Map)。它们就像为特定场景定制的收纳盒:Set是"不允许放重复物品的盒子",Map是"能用任意标签贴物品的柜子",Weak系列则是"会自动清理过期物品的智能收纳"。

今天就用"收纳工具对比"的思路,把这四位成员的用法、优势、坑点和实战场景讲透,让你再也不用为"去重""非字符串键"这些问题头疼。

一、先搞懂:为什么需要集合?对象和数组不够用吗?

在Set和Map出现之前(ES6之前),JS开发者只能用数组存列表、用对象存键值对,但这俩"老工具"有明显的短板,就像用普通收纳箱装特殊物品,怎么都别扭。

场景需求 数组/对象的尴尬 集合的解决方案
存储无重复值的列表 去重需手动遍历+判断(如arr.indexOf(item) === -1),效率低 Set自动去重,添加重复值直接忽略,无需额外代码
用非字符串/符号做键存数据 对象键会自动转为字符串(如{ [{}]: 1 }的键会变成"[object Object]" Map的键可以是任意类型(对象、数组、函数等),直接用原类型作为键
获取集合的长度/大小 对象需手动计算Object.keys(obj).length,数组虽有length但空槽会干扰 Set和Map直接用size属性获取,精准无误差
存储临时缓存(如DOM元素) 对象/数组会强引用数据,即使数据无用也不会被垃圾回收,导致内存泄漏 WeakSet/WeakMap弱引用数据,数据无其他引用时自动回收,避免内存泄漏

简单说:数组和对象是"通用工具",集合是"专用工具",针对"去重""任意键类型""内存优化"这些场景,集合能让代码更简洁、高效、安全。

二、Set:"去重收纳盒"------ 自动过滤重复值

Set的核心作用是"存储唯一值",不管你往里面加多少重复数据,它只会保留一份,就像一个有"自动筛选功能"的收纳盒,重复物品进不来。

1. 基本用法:简单到离谱

Set的API特别简洁,核心就4个方法+1个属性,一看就会:

js 复制代码
// 1. 创建Set(可传数组初始化,自动去重)
const mySet = new Set([1, 2, 2, 3]);
console.log(mySet); // Set(3) {1, 2, 3}(重复的2被自动过滤)

// 2. 添加元素(add方法,重复添加无效)
mySet.add(4);
mySet.add(2); // 重复值,添加失败
console.log(mySet.size); // 4(长度正确)

// 3. 查找元素(has方法,返回布尔值)
console.log(mySet.has(3)); // true
console.log(mySet.has(5)); // false

// 4. 删除元素(delete方法,返回是否删除成功)
mySet.delete(4);
console.log(mySet); // Set(3) {1, 2, 3}

// 5. 清空集合(clear方法)
mySet.clear();
console.log(mySet.size); // 0

2. 核心优势:比数组强在哪?

和数组相比,Set的优势集中在"去重""查找""删除"三个场景,效率更高、代码更简洁:

  • 去重 :数组去重需[...new Set(arr)],一行搞定,无需循环判断;
  • 查找 :数组用indexOf()(时间复杂度O(n)),Set用has()(时间复杂度O(1)),数据量越大差距越明显;
  • 删除 :数组需先找索引再用splice()(O(n)),Set直接用delete()(O(1));
  • 识别NaN :数组indexOf(NaN)返回-1(找不到),Set能正确识别NaN,视为同一个值。
js 复制代码
// 1. 数组去重(Set一行搞定)
const arr = [1, 2, 2, 3, NaN, NaN];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // [1, 2, 3, NaN](NaN去重成功)

// 2. 查找NaN(Set正确识别)
const set = new Set([NaN]);
console.log(set.has(NaN)); // true(数组做不到)

3. 遍历Set:三种常用方式

Set是可迭代对象,遍历顺序和插入顺序一致,支持三种遍历方式:

js 复制代码
const mySet = new Set(["苹果", "香蕉", "橙子"]);

// 方式1:for...of遍历(最常用)
for (const item of mySet) {
  console.log(item); // 苹果、香蕉、橙子
}

// 方式2:forEach遍历
mySet.forEach((item) => {
  console.log(item); // 苹果、香蕉、橙子
});

// 方式3:遍历keys()/values()(Set的key和value相同)
for (const key of mySet.keys()) {
  console.log(key); // 苹果、香蕉、橙子
}

4. 避坑点:Set不是数组,没有索引

Set不能通过索引访问元素(如mySet[0]),如果需要按索引取值,必须先转数组([...mySet][0])。

三、Map:"万能标签柜"------ 键可以是任意类型

Map的核心作用是"键值对映射",但比对象强大的是:键可以是任意类型(对象、数组、函数、Symbol等),就像一个万能标签柜,不管什么类型的标签,都能贴在物品上。

1. 基本用法:和对象类似,但更灵活

Map的API和Set对应,核心是"增删改查"键值对:

js 复制代码
// 1. 创建Map(可传二维数组初始化)
const myMap = new Map([
  ["name", "张三"],
  [18, "年龄"],
  [{ id: 1 }, "用户对象"] // 键是对象,合法!
]);
console.log(myMap.size); // 3

// 2. 添加/修改键值对(set方法,已存在则修改)
myMap.set(Symbol("key"), "符号键");
myMap.set(18, "修改后的年龄"); // 已存在的键,修改值

// 3. 查找值(get方法,传对应键)
console.log(myMap.get("name")); // "张三"
console.log(myMap.get({ id: 1 })); // undefined(注意:对象是引用类型,必须是同一个对象)

// 4. 检测键是否存在(has方法)
console.log(myMap.has(Symbol("key"))); // true

// 5. 删除键值对(delete方法)
myMap.delete(18);
console.log(myMap.has(18)); // false

// 6. 清空Map(clear方法)
myMap.clear();
console.log(myMap.size); // 0

2. Map vs 对象:该选谁?

很多人会纠结"什么时候用Map,什么时候用对象",记住三个核心判断标准:

  • 用Map的场景:
    1. 键在运行时才能确定(如动态生成的键);
    2. 键不是字符串/符号(如对象、数组作为键);
    3. 需要频繁添加/删除键值对,或需要获取集合大小;
    4. 需要按插入顺序遍历键值对。
  • 用对象的场景:
    1. 键是固定的字符串,且需要访问属性(如obj.name);
    2. 需要继承原型上的方法(如Object.prototype的方法);
    3. 对个别属性有特殊逻辑处理(如getter/setter)。

3. 遍历Map:按插入顺序来

Map的遍历顺序严格遵循插入顺序,比对象的遍历顺序(随机)更可靠,常用两种方式:

js 复制代码
const myMap = new Map([["a", 1], ["b", 2], ["c", 3]]);

// 方式1:for...of遍历(返回[key, value]数组)
for (const [key, value] of myMap) {
  console.log(`${key}: ${value}`); // a:1、b:2、c:3
}

// 方式2:forEach遍历(回调参数:value, key, map)
myMap.forEach((value, key) => {
  console.log(`${key}: ${value}`); // 顺序和插入一致
});

4. 避坑点:引用类型键的"坑"

Map的键是"按引用比较"的(除了原始类型和Symbol),两个看起来一样的对象,只要不是同一个引用,就会被视为两个不同的键:

js 复制代码
const map = new Map();
const obj1 = { id: 1 };
const obj2 = { id: 1 };

map.set(obj1, "value1");
map.set(obj2, "value2");

console.log(map.size); // 2(视为两个不同的键)
console.log(map.get(obj1)); // "value1"
console.log(map.get(obj2)); // "value2"

四、WeakSet & WeakMap:"智能收纳"------ 自动清理过期数据

WeakSet和WeakMap是Set和Map的"弱引用版本",核心优势是"弱引用"------不会阻止垃圾回收机制回收数据,就像智能收纳盒,当里面的物品没有其他地方引用时,会自动清理,避免内存泄漏。

1. 核心区别:Weak系列 vs 普通系列

特性 Set/Map WeakSet/WeakMap
引用类型 强引用(数据即使无其他引用,也不会被回收) 弱引用(数据无其他引用时,自动被回收)
存储内容 Set存任意类型值,Map键可任意类型 WeakSet只能存对象/Symbol,WeakMap键只能是对象/Symbol
可枚举性 可遍历(for...of、forEach) 不可遍历(无size属性,无keys()/values()方法)
内存泄漏风险 有(如缓存DOM元素后未手动删除) 无(自动回收无引用数据)

2. WeakSet:弱引用版Set

WeakSet的用法和Set类似,但只能存对象或Symbol,且不可遍历:

js 复制代码
const weakSet = new WeakSet();
const obj = { id: 1 };

// 添加元素(只能是对象/Symbol)
weakSet.add(obj);
weakSet.has(obj); // true

// 删除元素
weakSet.delete(obj);
weakSet.has(obj); // false

// 错误:存原始类型会报错
weakSet.add(123); // TypeError: Invalid value used in weak set
适用场景:
  • 存储DOM元素(如标记已处理的DOM节点,节点被删除后自动回收);
  • 存储临时关联的对象(如避免循环引用导致的内存泄漏)。

3. WeakMap:弱引用版Map

WeakMap的用法和Map类似,但键只能是对象或Symbol,不可遍历,是"存储对象私有数据"的神器:

js 复制代码
// 经典场景:用WeakMap存储对象私有数据
const privateData = new WeakMap();

class User {
  constructor(name) {
    // 私有数据存在WeakMap中,外界无法访问
    privateData.set(this, { name, password: "123456" });
  }

  getName() {
    return privateData.get(this).name;
  }
}

const user = new User("张三");
console.log(user.getName()); // "张三"
console.log(privateData.get(user)); // 只能在类内部访问,外界无法获取
适用场景:
  • 存储对象私有数据(避免污染全局,且不影响垃圾回收);
  • 缓存对象相关数据(如API请求缓存,对象销毁后缓存自动清理);
  • 关联DOM元素和数据(如给DOM节点绑定数据,节点删除后自动回收)。

4. 避坑点:Weak系列不可遍历

WeakSet和WeakMap没有size属性,也没有keys()/values()/forEach()方法,无法遍历,只能通过has()/get()/delete()操作已知的键/值。

五、集合的键值相等规则:SameValueZero算法

Set和Map的键/值相等判断遵循"SameValueZero"算法,和严格相等(===)类似,但有两个区别,这也是容易踩坑的点:

  1. NaN 与自身相等(===NaN !== NaN,但Set/Map中视为同一个值);
  2. -0+0 相等(和===一致,但需注意和Object.is()的区别)。
js 复制代码
// 1. NaN视为相等
const set = new Set([NaN, NaN]);
console.log(set.size); // 1(只保留一个NaN)

// 2. -0和+0视为相等
const map = new Map([[-0, "a"], [+0, "b"]]);
console.log(map.get(-0)); // "b"(后面的覆盖前面的)

六、实战场景:集合的高频用法

1. Set的实战用法

  • 数组去重:const uniqueArr = [...new Set(arr)]
  • 字符串去重:const uniqueStr = [...new Set(str)].join("")
  • 存储唯一标签/状态:如用户的权限标签(避免重复);
  • 快速判断元素是否存在:如判断用户是否已添加到收藏列表。

2. Map的实战用法

  • 存储多类型键数据:如Map([[userObj, userInfo], [Symbol("config"), config]])
  • 动态键值对缓存:如API请求缓存(键是请求参数对象,值是响应数据);
  • 按插入顺序遍历:如需要保留顺序的配置项。

3. WeakMap的实战用法

  • 对象私有数据:如类的私有属性(避免用#私有字段的兼容性问题);
  • DOM元素数据绑定:如给按钮绑定点击次数,按钮删除后自动回收数据;
  • 避免循环引用:如父子对象相互引用时,用WeakMap存储关联关系。

七、总结:集合的选择指南

  1. 需去重、高效查找/删除 → 用Set
  2. 需任意类型键、按插入顺序遍历键值对 → 用Map
  3. 需存储对象/符号,且要自动回收 → 用WeakSet/WeakMap
  4. 需遍历、存储原始类型 → 用普通Set/Map,不用Weak系列。

集合是ES6带来的"高级工具",针对特定场景能大幅简化代码、提升效率,还能避免内存泄漏。掌握它们的用法,能让你的JS代码从"能用"升级到"优雅、高效、安全"。

相关推荐
快撑死的鱼2 小时前
Llama-factory 详细学习笔记:第六章:DPO (直接偏好优化) 实战 (难点)
笔记·学习·llama
d111111111d2 小时前
连续形式PID和离散PID-详情学习-江科大(学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习
四维碎片2 小时前
【Qt】生产者-消费者模式学习笔记
笔记·qt·学习
立志成为大牛的小牛2 小时前
数据结构——五十九、冒泡排序(王道408)
数据结构·学习·程序人生·考研·算法
试着2 小时前
【VSCode+AI+测试】连接ai大模型
ide·人工智能·vscode·python·学习·编辑器·ai-test
韩曙亮3 小时前
【思维模型】第一性原理 ② ( 利用 “ 第一性原理 “ 进行创新 : 归零 -> 解构 -> 重构 | 跨学科学习 )
学习·重构·第一性原理·思维模型·解构·归零
秦奈3 小时前
Unity复习学习随笔(五):Unity基础
学习·unity·游戏引擎
馬致远3 小时前
Vue TodoList 待办事项小案例(代码版)
前端·javascript·vue.js