第3章 3.4 Set/Map/WeakMap 集合
上一章我们学会了用 ES6 Class 给数据「打包封装」,就像把散装水果装进包装盒。但装完之后,问题来了:盒子多了,怎么快速找到其中一个?怎么判断两颗钻石是不是同一颗?怎么确保某个私密属性只有我自己能访问?
这一章,我们来认识三个「高级盒子」—— Set、Map、WeakMap。它们是 JavaScript 的三种特殊集合,专门解决「找东西」「存关系」「藏秘密」的问题。学会了它们,你的数据操作直接从「杂货铺」升级到「精品店」。
🎯 开场 3 分钟:为什么要学这个?
场景带入
想象你开了一家水果店:
- 痛点 1:顾客说「我要买苹果」,你怎么快速知道「有没有苹果」和「还剩几个」?总不能一个个数吧?
- 痛点 2:老客户的信息怎么存?「张三,买了 3 次苹果,欠款 50 块」—— 用普通数组怎么优雅地表示这种「对应关系」?
- 痛点 3:有个员工要存一个「只属于他」的秘密属性,不想让别人看到,怎么办?
这些问题,普通数组和对象解决起来很\n\n
\n\n
\n\n别扭,但 Set、Map、WeakMap 只需要几句话就能搞定。
学会之后你能
- 用 Set 给数组去重、判断是否存在某元素
- 用 Map 存储「键值对」关系,并快速查询
- 用 WeakMap 实现真正的「私有属性」
- 理解 JavaScript 的垃圾回收机制,不再内存泄漏
🧱 基础 25 分钟:核心概念
3.4.1 Set:去重神器
是什么?
Set 就是一个「不重复的数组」。就像你去超市存包,每个包都有一个专属号码牌,同一个号码只能出现一次。
为什么要用?
想象你有一个「已购用户」列表,同一个用户可能重复购买多次,但你想知道「到底有多少独立用户」—— 用 Set 自动去重,10 年老粉只算一次。
怎么用?
// 创建一个 Set
const fruits = new Set();
// 添加元素(add)
fruits.add('苹果');
fruits.add('香蕉');
fruits.add('苹果'); // 重复添加,实际只存一个
console.log(fruits); // Set(2) {'苹果', '香蕉'}
console.log(fruits.size); // 2,有多少种水果
// 从数组创建(自动去重)
const orders = ['张三', '李四', '王五', '张三', '李四'];
const uniqueCustomers = new Set(orders);
console.log(uniqueCustomers); // Set(3) {'张三', '李四', '王五'}
代码解释:
- 第 1 行:创建空 Set
- 第 2-4 行:用 add() 添加水果,'苹果' 加了两次但只保留一个
- 第 7 行:直接从数组构建,'张三' 和 '李四' 被自动去除
Set 的常用方法
const fruits = new Set(['苹果', '香蕉', '橙子']);
// 判断是否存在(has)
console.log(fruits.has('苹果')); // true
console.log(fruits.has('葡萄')); // false
// 删除元素(delete)
fruits.delete('香蕉');
console.log(fruits); // Set(2) {'苹果', '橙子'}
// 遍历(forEach 或 for...of)
fruits.forEach(fruit => {
console.log('我有:' + fruit);
});
for (const fruit of fruits) {
console.log('库存:' + fruit);
}
代码解释:
- has() 就像问「有这个水果吗?」,返回 true/false
- delete() 删除特定元素
- 遍历方式和数组一样,但元素不重复
3.4.2 Map:键值对字典
是什么?
Map 就是「可以存任意类型键」的字典。普通对象的键只能是字符串,但 Map 可以用数字、对象、函数做键。
类比:就像一个高级名片夹,每个人名(键)对应一张名片(值),名片夹知道每个人的所有信息。
为什么要用?
想象你要存「员工 ID → 员工信息」:
- 用普通对象:
{'001': {name: '张三', age: 25}}—— 可以,但键会被转成字符串 - 用 Map:
Map {[1] => {name: '张三'}, [2] => {name: '李四'}}—— 更直观,且支持任意类型键
怎么用?
// 创建 Map
const userMap = new Map();
// 设置键值对(set)
userMap.set('张三', {年龄: 25, 职业: '老师'});
userMap.set('李四', {年龄: 30, 职业: '医生'});
userMap.set(10086, {年龄: 28, 职业: '工程师'}); // 数字做键,完全 OK
console.log(userMap);
console.log(userMap.size); // 3
// 获取值(get)
const info = userMap.get('张三');
console.log(info); // {年龄: 25, 职业: '老师'}
// 判断键是否存在(has)
console.log(userMap.has('李四')); // true
console.log(userMap.has('王五')); // false
// 删除(delete)
userMap.delete(10086);
console.log(userMap.size); // 2
代码解释:
- set(key, value) 存数据,get(key) 取数据
- 键可以是字符串、数字、对象——普通对象做不到
- size 属性直接告诉你有多少条记录
Map 的遍历
const userMap = new Map([
['张三', {年龄: 25, 职业: '老师'}],
['李四', {年龄: 30, 职业: '医生'}],
]);
// 遍历所有键值对
for (const [name, info] of userMap) {
console.log(name + '是' + info.职业);
}
// 用 forEach
userMap.forEach((info, name) => {
console.log(`${name}: ${info.年龄}岁`);
});
3.4.3 WeakMap:私密保险箱
是什么?
WeakMap 是「键是对象且没有强引用」的 Map。听起来很拗口,听我讲个故事:
想象你在健身房存包:
- 普通 Map:你把包存进去,但健身房保留了「你存过包」的记录(强引用),即使你拿走包,健身房还记得
- WeakMap:你把包存进去,健身房不保留记录,只有你自己能打开(弱引用)。包一旦被你拿走,健身房完全不记得这回事
「弱引用」的意思是:如果一个对象只被 WeakMap 引用,没有任何其他地方引用它,垃圾回收器会直接把它清理掉。
为什么要用?
WeakMap 最大用途是:存对象的「私有属性」。
// 不用 WeakMap,给对象加个「隐藏」属性
const user = { name: '张三' };
user._private = '这是我不想让别人看到的秘密'; // 任何人都能访问到
console.log(user._private); // 能访问!不安全
// 用 WeakMap 实现真正的私有
const privateData = new WeakMap();
const user = { name: '张三' };
privateData.set(user, '这是我只有自己能访问的秘密');
console.log(privateData.get(user)); // 只有我有 WeakMap,才能拿到
console.log(user._private); // undefined,根本不知道有秘密
代码解释:
- WeakMap 的键只能是对象(不能是字符串、数字)
- privateData 存在外部,只有知道这个 WeakMap 的人才能访问对应的值
- user 对象本身完全干净,不知道自己有任何秘密
WeakMap 的限制
const weak = new WeakMap();
// 可以 set
const obj = { id: 1 };
weak.set(obj, '私有数据');
// 可以 get、has、delete
console.log(weak.get(obj)); // '私有数据'
console.log(weak.has(obj)); // true
weak.delete(obj); // 删除这条记录
// ❌ 不能遍历!没有 forEach、for...of、keys()、values()
weak.forEach(...) // 报错!TypeError
// ❌ 不能用 size(因为不确定里面有多少)
console.log(weak.size) // undefined
为什么这样设计? 因为 WeakMap 是为了「私有」设计的,不应该被遍历。如果你需要遍历,用普通 Map。
3.4.4 三者对比
| 特性 | Set | Map | WeakMap |
|---|---|---|---|
| 用途 | 存「不重复的值」 | 存「键值对」 | 存「对象的私有数据」 |
| 键的要求 | 值不重复 | 键可以是任意类型 | 键只能是对象 |
| 重复处理 | 自动去重 | 键重复会覆盖 | 键重复会覆盖 |
| 遍历 | 可以 | 可以 | 不可以 |
| 内存管理 | 手动删除 | 手动删除 | 自动回收(垃圾回收) |
| 典型场景 | 去重、存在判断 | 字典查询、配置存储 | 私有属性、缓存 |
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):统计「不重复用户」
场景:你有一个用户行为日志,要统计有多少独立用户访问过。
// 用户访问日志(模拟数据)
const visitLog = [
'用户A',
'用户B',
'用户A', // 重复访问
'用户C',
'用户B', // 重复访问
'用户D',
'用户A' // 第三次访问
];
// 用 Set 去重
const uniqueUsers = new Set(visitLog);
console.log('独立用户数:' + uniqueUsers.size); // 4
console.log('用户列表:');
uniqueUsers.forEach(user => {
console.log(' - ' + user);
});
预期输出:
独立用户数:4
用户列表:
- 用户A
- 用户B
- 用户C
- 用户D
一句话解释:Set 自动过滤掉重复的 '用户A' 和 '用户B',size 直接告诉你「去重后还剩几个」。
项目 2(15 分钟):学生成绩查询系统
场景:从 JSON 数据加载学生成绩,用 Map 实现「输入学号 → 秒查成绩」。
// 模拟从数据库加载的学生数据
const studentData = [
{ id: '2024001', name: '张小明', scores: { 数学: 92, 语文: 88, 英语: 95 } },
{ id: '2024002', name: '李小红', scores: { 数学: 78, 语文: 85, 英语: 80 } },
{ id: '2024003', name: '王大力', scores: { 数学: 95, 语文: 91, 英语: 88 } },
];
// 构建 Map:学号 → 学生信息
const studentMap = new Map();
studentData.forEach(student => {
studentMap.set(student.id, student);
});
console.log('学生人数:' + studentMap.size); // 3
// 快速查询
function 查询成绩(学号) {
const student = studentMap.get(学号);
if (!student) {
console.log('查无此人');
return;
}
console.log(`【${student.name}】的成绩:`);
const { 数学, 语文, 英语 } = student.scores;
console.log(` 数学:${数学}分`);
console.log(` 语文:${语文}分`);
console.log(` 英语:${英语}分`);
const avg = (数学 + 语文 + 英语) / 3;
console.log(` 平均:${avg.toFixed(1)}分`);
}
// 测试查询
查询成绩('2024001');
console.log('---');
查询成绩('2024005'); // 不存在的学号
预期输出:
学生人数:3
【张小明】的成绩:
数学:92分
语文:88分
英语:95分
平均:91.7分
---
查无此人
一句话解释:把数组转成 Map 后,查询从 O(n) 变成 O(1),无论查 100 个还是 10000 个学生,秒回。
项目 3(15 分钟):带缓存的「敏感信息管理系统」
场景:模拟一个用户系统,有些敏感操作需要「验证后才能查看」,用 WeakMap 存私有验证状态。
// 用 WeakMap 存储每个用户的「验证状态」和「私有令牌」
const privateStore = new WeakMap();
const userCache = new WeakMap();
// 创建用户(模拟注册)
function 创建用户(name, isVIP) {
const user = { name, isVIP };
// 初始状态:未验证
privateStore.set(user, {
verified: false,
token: Math.random().toString(36).substring(2), // 随机令牌
lastLogin: new Date().toLocaleString()
});
return user;
}
// 验证用户(只有验证过才能查看敏感信息)
function 验证用户(user) {
const privateData = privateStore.get(user);
if (!privateData) {
console.log('用户不存在');
return false;
}
privateData.verified = true;
console.log(`✅ ${user.name} 验证成功`);
return true;
}
// 查看用户敏感信息(需要先验证)
function 查看敏感信息(user) {
const privateData = privateStore.get(user);
if (!privateData) {
console.log('用户不存在');
return;
}
if (!privateData.verified) {
console.log('❌ 请先验证身份');
return;
}
console.log(`📋 ${user.name} 的敏感信息:`);
console.log(` 令牌:${privateData.token}`);
console.log(` 上次登录:${privateData.lastLogin}`);
console.log(` VIP身份:${user.isVIP ? '是' : '否'}`);
}
// 测试流程
const user1 = 创建用户('张三', true);
const user2 = 创建用户('李四', false);
console.log('=== 直接查看(未验证)===');
查看敏感信息(user1);
查看敏感信息(user2);
console.log('\n=== 验证后查看 ===');
验证用户(user1);
验证用户(user2);
查看敏感信息(user1);
查看敏感信息(user2);
预期输出:
=== 直接查看(未验证)===
❌ 请先验证身份
❌ 请先验证身份
=== 验证后查看 ===
✅ 张三 验证成功
✅ 李四 验证成功
📋 张三 的敏感信息:
牌:a3f8b2c1...
次登录:2024/1/15 14:30:00
IP身份:是
📋 李四 的敏感信息:
牌:b7c4d9e2...
次登录:2024/1/15 14:30:00
IP身份:否
一句话解释:WeakMap 保证了「令牌」「验证状态」这些私密数据只有持有 user 对象的人才能访问,而且 user 被垃圾回收后,对应的私密数据也会自动消失(不会内存泄漏)。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:Set/Map 的键是「严格相等」
// ❌ 错误:以为 1 和 '1' 是同一个键
const map1 = new Map();
map1.set('1', '值1');
map1.set(1, '值2');
console.log(map1.get('1')); // '值1',不是 '值2'!
console.log(map1.size); // 2,两个不同的键
// ✅ 正确:同类型才被认为是同一个键
const map2 = new Map();
map2.set('name', '张三');
map2.set('name', '李四'); // 覆盖
console.log(map2.get('name')); // '李四'
坑 2:遍历 Map 时修改 Map 本身
// ❌ 错误:边遍历边添加,可能死循环
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) {
map.set(key + key, value * 10); // 无限添加!
}
// ✅ 正确:先收集要处理的键
const map = new Map([['a', 1], ['b', 2]]);
const keys = [...map.keys()];
for (const key of keys) {
map.set(key + key, map.get(key) * 10);
}
console.log(map);
坑 3:WeakMap 的键必须是对象
// ❌ 错误:字符串不能做 WeakMap 的键
const weak = new WeakMap();
weak.set('name', '张三'); // 报错!TypeError
// ✅ 正确:只能用对象做键
const obj = { id: 1 };
const weak = new WeakMap();
weak.set(obj, '私有数据');
坑 4:Set 不是数组
// ❌ 错误:以为 Set 有数组的方法
const set = new Set([1, 2, 3]);
console.log(set.push(4)); // 报错!Set 没有 push
// ✅ 正确:Set 用 add 添加,用 [...set] 转成数组
const set = new Set([1, 2, 3]);
set.add(4);
console.log([...set]); // [1, 2, 3, 4]
坑 5:忘记 Map 的 size 是属性不是方法
// ❌ 错误:把 size 当方法调用
const map = new Map([['a', 1]]);
console.log(map.size()); // undefined
// ✅ 正确:size 是属性
console.log(map.size); // 1
性能小贴士:大量数据查询用 Map 代替数组
// ❌ 低效:每次查询都遍历数组
const users = [{id: '001', name: '张三'}, {id: '002', name: '李四'}, /* 1000个... */];
function findUser(id) {
return users.find(u => u.id === id); // O(n),1000个就慢
}
// ✅ 高效:一次建索引,永久 O(1) 查询
const userMap = new Map();
users.forEach(u => userMap.set(u.id, u));
function findUser(id) {
return userMap.get(id); // O(1),无论多少都飞快
}
调试技巧:用 console.table() 查看 Map/Set
const map = new Map([
['数学', 95],
['语文', 88],
['英语', 92]
]);
const set = new Set(['苹果', '香蕉', '橙子']);
// 普通 console
console.log(map); // Map(3) {'数学' => 95, ...}
console.log(set); // Set(3) {'苹果', '香蕉', '橙子'}
// 用 table 更清晰
console.table([...map.entries()]); // 表格形式显示键值对
console.table([...set]); // 表格形式显示集合内容
✏️ 练习题 + 作业题
练习题(5 道,10 分钟内完成)
练习 1(2 分钟):Set 去重
- 输入:['红', '蓝', '红', '绿', '蓝', '红']
- 预期输出:独立颜色数量 3
- 提示:用 new Set() 和 .size
练习 2(2 分钟):Map 键值查询
- 输入:Map {'name': '小明', 'age': 18}
- 预期输出:判断 'name' 是否存在 → true
- 提示:记得用 .has() 方法
练习 3(2 分钟):判断 Set 成员
- 输入:Set {1, 2, 3},判断是否存在 4
- 预期输出:false
- 提示:has() 方法返回布尔值
练习 4(3 分钟):给 Map 加新键
- 输入:已有的 Map {'a': 1, 'b': 2},添加 'c': 3
- 预期输出:Map 大小变成 3
- 提示:用 set() 添加新键值对不会覆盖原有的
练习 5(3 分钟):找错
- 题目:下面代码想统计不重复单词,但报错
const words = new Set('apple banana apple cherry banana');
console.log(words.size); // 报错!
- 预期输出:报错说
words.size is undefined - 提示:Set 构造函数接受数组,不接受字符串。字符串会被当成可迭代对象逐字符拆分
作业题(30 分钟 - 2 小时)
作业:做一个「投票统计系统」
需求描述:模拟一次投票活动,统计每个候选人的票数,并输出排名。
功能点:
1. 用 Map 存储候选人 → 票数的映射
2. 投票记录用 Set 去重(同一选民不能重复投)
3. 用 WeakMap 记录每个候选人的「私密备注」(只有创建者能看)
4. 输出每个候选人的得票数和排名
加分项:
1. 支持「查看某个候选人的私密备注」(需要验证)
2. 用 console.table() 展示结果
验收标准:
- 能跑起来,输入测试数据后输出正确排名
- 代码有适当注释
- 演示 WeakMap 的「私有」特性
提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
一句话总结
本文学了三个「高级容器」:Set 自动去重、Map 键值查询、WeakMap 存私密属性——让数据操作从「土法炼钢」变成「机械化生产」。
延伸学习资源
- MDN Set 官方文档 —— 最权威的 API 参考
- MDN Map 官方文档 —— 有更多进阶用法
- 《JavaScript 高级程序设计》第 4 版,第 6 章「集合」—— 系统学习必读
互动钩子
你在实际项目里用过 Set 或 Map 吗? 比如去重、缓存、配置管理……评论区聊聊你的场景,老粉优先回复!
📌 下章预告:学会了集合三兄弟,下一章我们要用一个「学生成绩管理系统」把它们串起来——Set 去重、Map 查询、WeakMap 藏秘密,一个都不少!

评论(0)