JavaScript集合(Set、WeakSet)与映射(Map、WeakMap)
一、Set数据类型
JavaScript中的Set是ES6(ES2015)引入的一种集合数据结构,用于存储唯一值(unique values)的有序列表。无论是原始类型(如数字、字符串)还是对象引用,Set都会自动去重。
基本特性:
| 特性 | 说明 |
|---|---|
| 值唯一 | 不允许重复元素(使用===判断相等,但NaN === NaN被视为相等)。 |
| 有序 | 元素按插入顺序迭代。 |
| 可存储任意类型 | 包括number、string、object、NaN、undefined等。 |
| 非索引结构 | 不能通过下标访问(不像数组),但可遍历。 |
注意:Set中的{}和{}被视为不同对象(因为引用不同),所以不会去重。
详细介绍:
- 常用方法与属性。
- Set与Array互转。
Set vs Array。
| 场景 | 推荐 |
|---|---|
| 需要去重 | Set。 |
| 需要频繁判断元素是否存在 | Set(has()时间复杂度O(1),Array的includes()是O(n))。 |
| 需要索引/顺序操作(如 sort, splice) | Array。 |
| 存储大量数据且频繁增删 | Set更高效。 |
特殊值处理。
const s =newSet(); s.add(NaN); s.add(NaN); console.log(s.size);// 1 → NaN被视为相等。 s.add(0); s.add(-0); console.log(s.size);// 2 → +0和-0被视为不同(符合IEEE 754)。Array → Set(自动去重)
const arr =[1,2,2,3];const set =newSet(arr);// Set {1, 2, 3}。经典去重技巧:
const unique =[...newSet([1,2,2,3])];// [1, 2, 3]。Set → Array
const set =newSet([1,2,3]);const arr =[...set];// 或Array.from(set)。遍历Set。Set是可迭代对象(iterable),支持以下方式遍历:
const set =newSet([1,2,3]);// 1. for...of。for(const item of set){ console.log(item);// 1, 2, 3。}// 2. forEach。// `Set`的`forEach`回调函数参数是`(value, value, set)`,没有key(与Map不同)。 set.forEach((value, valueAgain, setRef)=>{ console.log(value);// value 和 valueAgain 相同(Set 没有 key)。});// 3. 扩展运算符转为数组。[...set];// [1, 2, 3]。// 4. Array.from。 Array.from(set);// [1, 2, 3]。方法:
| 方法 | 说明 | 示例 |
|---|---|---|
add(value) | 添加元素(返回Set自身,可链式调用)。 | s.add(4) |
delete(value) | 删除元素(返回布尔值)。 | s.delete(1) → true |
has(value) | 检查是否存在(返回布尔值)。 | s.has(2) → true |
clear() | 清空所有元素。 | s.clear() |
forEach(callback) | 遍历元素。 | s.forEach(v => console.log(v)) |
属性:size——返回元素个数。
s2.size;// 3。创建Set。
// 空 Setconst s1 =newSet();// 从可迭代对象初始化(如数组)。const s2 =newSet([1,2,3,2,1]);// Set(3) {1, 2, 3}。// 字符串会被拆分为字符。const s3 =newSet('hello');// Set(4) {'h', 'e', 'l', 'o'}。高级用法示例:
去除对象数组中的重复项(基于某属性)。
const users =[{id:1,name:'Alice'},{id:2,name:'Bob'},{id:1,name:'Alice'}];const uniqueUsers = Array.from(newSet(users.map(u=> u.id)),id=> users.find(u=> u.id === id));// 或使用Map更高效。// 上述使用Set,时间复杂度为O(n^2),而使用下述Map,时间复杂度为O(n)。const uniqueUsers = Array.from(newMap(users.map(u=>[u.id, u])).values());/* users.map(u => [u.id, u]) 生成键值对数组:[[1, obj1], [2, obj2], [1, obj3], ...]。 new Map(...) Map的key自动去重,后出现的相同key会覆盖前面的(若想保留第一个,可反向遍历)。 .values() 获取所有唯一对象。 Array.from(...) 转为数组。 */求两个数组的并集、交集、差集。
const a =[1,2,3];const b =[3,4,5];// 并集。const union =[...newSet([...a,...b])];// [1, 2, 3, 4, 5]。// 交集。const intersection =[...newSet(a.filter(x=> b.includes(x)))];// 差集(a - b)。const diff = a.filter(x=>!newSet(b).has(x));// [1, 2]。注意事项:
Set不支持直接获取第n个元素(无.get(index)方法)。
Set无法序列化为 JSON(需先转数组):
JSON.stringify([...mySet]);对象去重依赖引用相等,不是内容相等:
newSet([{a:1},{a:1}]).size;// 2(两个不同对象)。总结:
Set是处理“唯一值集合”的最佳工具,尤其适合去重、成员检测、集合运算等场景。结合扩展运算符和数组方法,能写出简洁高效的代码。- 常用口诀:
- 去重要用Set。
- 存在检查用
.has()。 - 遍历用
for...of或forEach。 - 转数组用
[...set]。
二、WeakSet数据类型
WeakSet是JavaScript(ES6引入)中一种特殊的集合数据结构,它与Set类似,但有关键限制和用途,那就是WeakSet只能存储对象(不能是原始值),且对对象的引用是“弱引用”(weakly held)——不会阻止垃圾回收(GC)。
核心特性:
| 特性 | 说明 |
|---|---|
| 只能存对象 | 不能添加number、string、boolean等原始值。 |
| 弱引用 | 存入的对象如果没有其他引用,会被GC自动回收。 |
| 不可迭代 | 没有.size、.clear()、.entries()、for...of等方法。 |
| 无顺序 | 不保证元素顺序(也无法遍历)。 |
| 私有性 | 无法知道WeakSet中有哪些对象(设计如此)。 |
基本用法:
核心方法(只有 3 个!)。
| 方法 | 说明 | 示例 |
|---|---|---|
add(value) | 添加对象。 | ws.add(obj) |
has(value) | 检查对象是否存在。 | ws.has(obj) → true/false |
delete(value) | 删除对象。 | ws.delete(obj) |
const obj ={}; ws.add(obj); console.log(ws.has(obj));// true。 ws.delete(obj); console.log(ws.has(obj));// false。没有size属性! 你无法知道WeakSet里有多少元素。
创建和操作。
const ws =newWeakSet();// 只能添加对象。 ws.add({}); ws.add(document.body); ws.add(newDate());// 报错:不能添加原始值。 ws.add(42);// TypeError。 ws.add('hello');// TypeError。 ws.add(true);// TypeError。典型场景:
缓存或元数据(生命周期跟随对象)。
// 为每个请求对象附加临时元数据。const requestMetadata =newWeakSet();fetch('/api/data').then(res=>{ requestMetadata.add(res);// ... 处理响应。});// 当res对象被GC回收时,元数据自动消失。私有数据存储(不污染对象)。
// 模拟私有属性(避免在对象上直接挂 _privateData)。const privateData =newWeakSet();classUser{constructor(name){this.name = name; privateData.add(this);// 标记该实例有私有数据。}isValid(){// 检查是否属于“有效用户”。return privateData.has(this);}}标记对象(而不阻止GC)。
// 用于标记“已处理”的DOM元素。const processedElements =newWeakSet();functionprocessElement(el){if(processedElements.has(el)){return;// 已处理过,跳过。}// ... 处理逻辑。 processedElements.add(el);}// 当el从DOM移除且无其他引用时,// 它会自动从WeakSet中消失(无需手动清理)。WeakSet vs Set:
| 特性 | Set | WeakSet |
|---|---|---|
| 存储类型 | 任意值(对象/原始值)。 | 仅对象。 |
| 引用类型 | 强引用(阻止 GC)。 | 弱引用(不阻止 GC)。 |
| 可遍历 | 是(for...of, .size等)。 | 否(完全不可遍历)。 |
| 用途 | 通用去重、集合运算。 | 生命周期绑定对象的标记/元数据。 |
简单记忆:
- 用
Set存“我要主动管理的数据”。 - 用
WeakSet存“这个对象有某种状态,但我不负责它的生死”。
重要注意事项:
- 弱引用 ≠ 立即回收。
- 对象是否被回收取决于GC时机。
- 即使
WeakSet是唯一引用,也可能暂时未被回收。
不能用于原始值。
// 常见错误:const ids =newWeakSet(); ids.add(123);// TypeError: Invalid value used in weak set。无法遍历 = 无法知道内容。
const ws =newWeakSet(); ws.add({a:1}); ws.add({b:2});// 你无法列出所有元素!// 没有ws.size, 没有for...of, 没有.values()。这是刻意设计:防止开发者依赖内部状态,确保弱引用语义。
高级技巧:结合FinalizationRegistry(ES2021)。
监听对象被 GC 的时机(谨慎使用):
const registry =newFinalizationRegistry((heldValue)=>{ console.log(`${heldValue} 被回收了`);});const ws =newWeakSet();const obj ={id:1}; ws.add(obj); registry.register(obj,'Object with id=1'); obj =null;// 解除引用。// 未来某刻GC后,会输出"Object with id=1 被回收了"。FinalizationRegistry 是实验性API,不推荐常规使用。
总结:
WeakSet是一个“只增不查全貌”的对象标记工具,核心价值在于:“我知道这个对象有某种状态,但我不持有它,让它自由生灭。”
使用口诀:
- 存对象(不能存数字/字符串)。
- 做标记(如“已初始化”、“已验证”)。
- 免清理(随对象自动消失)。
- 别遍历(根本做不到)。
- 别计数(没有
.size)。
三、Map数据类型
Map是JavaScript(ES6/ES2015引入)中一种键值对集合(key-value collection) 数据结构,用于存储任意类型的键和值的映射关系。它比普通对象({})更强大、更灵活,尤其适合键不是字符串或需要精确控制键值行为的场景。
Map vs 普通对象:
| 特性 | 普通对象{} | Map |
|---|---|---|
| 键的类型 | 仅字符串/Symbol。 | 任意类型(包括对象、函数、数字等)。 |
| 键的顺序 | 无序(ES2015后部分有序)。 | 插入顺序(严格保持)。 |
| 大小获取 | 需手动计算(Object.keys(obj).length)。 | 直接.size。 |
| 迭代 | 需Object.keys()等辅助。 | 原生可迭代(for...of、.entries()等)。 |
| 原型污染风险 | 有(如obj.__proto__)。 | 无(纯净数据结构)。 |
| 性能 | 大量动态属性时较慢。 | 大量数据时更快(专为频繁增删优化)。 |
简单说:Map是“真正的哈希表”,而对象是“为固定结构设计的”。
基本用法:
- 遍历Map。
Map是可迭代对象,支持多种遍历方式:
转为数组。
const keys =[...map.keys()];// [ 'name', 1, {} ]。const values =[...map.values()];// [ 'Bob', 'number key', 'object key' ]。const entries =[...map.entries()];// [ ['name','Bob'], [1,'...'], ... ]。内置迭代器方法。
// 所有键。for(const key of map.keys()){...}// 所有值。for(const value of map.values()){...}// 所有键值对(默认)。for(const entry of map.entries()){ console.log(entry);// [key, value]。}for...of(推荐)。
for(const[key, value]of map){ console.log(key, value);}核心方法。
| 方法 | 说明 | 示例 |
|---|---|---|
set(key, value) | 添加/更新键值对。 | map.set('a', 1) |
get(key) | 获取值(不存在返回undefined)。 | map.get('a') → 1 |
has(key) | 检查是否存在键。 | map.has('a') → true |
delete(key) | 删除键值对(返回布尔值)。 | map.delete('a') |
clear() | 清空所有键值对。 | map.clear() |
size | 属性:返回元素数量。 | map.size → 0 |
const map =newMap(); map.set('name','Bob'); map.set(1,'number key'); map.set({},'object key');// 键可以是对象! console.log(map.get('name'));// 'Bob'。 console.log(map.has(1));// true。 console.log(map.size);// 3。创建Map。
// 空Map。const map =newMap();// 从可迭代对象初始化(如数组的数组)。const map2 =newMap([['name','Alice'],[42,'answer'],[{id:1},'user object']]);关键特性详解:
- 用DOM元素作键 → 存储其状态。
- 用函数作键 → 缓存计算结果。
保持插入顺序。
const map =newMap(); map.set(3,'three'); map.set(1,'one'); map.set(2,'two'); console.log([...map.keys()]);// [3, 1, 2](按插入顺序)。严格相等比较(SameValueZero)。键的比较使用SameValueZero算法(类似===,但NaN === NaN为真)。
const map =newMap(); map.set(NaN,'not a number'); console.log(map.get(NaN));// 'not a number'(普通对象做不到!)。键可以是任意类型。
const user1 ={id:1};const user2 ={id:2};const userMap =newMap(); userMap.set(user1,'Alice'); userMap.set(user2,'Bob'); console.log(userMap.get(user1));// 'Alice'。// 注意:必须用同一个对象引用! console.log(userMap.get({id:1}));// undefined(新对象)。适用于:
Map vs WeakMap:
| 特性 | Map | WeakMap |
|---|---|---|
| 键的类型 | 任意。 | 仅对象。 |
| 可枚举 | 是。 | 否(无 size、clear()、entries()等)。 |
| 垃圾回收 | 键被强引用(阻止 GC)。 | 弱引用(键可被 GC 回收)。 |
| 用途 | 通用键值存储。 | 私有数据、DOM 元素元数据。 |
WeakMap适合:不希望阻止对象被回收的场景(如给DOM元素附加临时数据)。
实用技巧:
缓存函数结果(记忆化)。
const cache =newMap();functionexpensiveFn(arg){if(cache.has(arg))return cache.get(arg);const result =/* 耗时计算 */; cache.set(arg, result);return result;}去重数组(基于对象内容)。
// 按id去重。const users =[{id:1},{id:2},{id:1}];const unique =[...newMap(users.map(u=>[u.id, u])).values()];Map转对象(仅字符串键)。
const map =newMap([['a',1],['b',2]]);const obj = Object.fromEntries(map);对象转Map。
const obj ={a:1,b:2};const map =newMap(Object.entries(obj));注意事项:
不支持点语法访问。
map.key;// undefined(必须用map.get('key'))。键是对象时,必须用相同引用。
map.set({x:1},'value'); map.get({x:1});// undefined(新对象)。不能用JSON.stringify()直接序列化。
JSON.stringify(newMap());// '{}'。// 解决方案:先转为数组。JSON.stringify([...map]);性能建议:
| 场景 | 推荐 |
|---|---|
| 键是字符串/数字,结构固定。 | 普通对象{}。 |
| 键类型多样、动态增删频繁。 | Map。 |
| 需要与对象互转(仅字符串键)。 | Object.entries()/Object.fromEntries()。 |
| 存储私有数据且不阻止GC。 | WeakMap。 |
总结:
Map是现代JavaScript中处理键值对的首选数据结构,当你遇到以下情况时,优先考虑它:
- 键不是字符串(如对象、数字、Symbol)。
- 需要频繁增删键值对。
- 需要可靠获取元素数量。
- 需要保持插入顺序。
- 担心原型污染。
记住核心 API:
const m =newMap(); m.set(key, value); m.get(key); m.has(key); m.delete(key); m.size;四、WeakMap数据类型
WeakMap是JavaScript(ES6引入)中一种键值对集合(key-value collection),它是Map的“弱引用”版本,专为以对象为键、且不希望阻止垃圾回收(GC) 的场景设计。
核心特性:
| 特性 | 说明 |
|---|---|
| 键必须是对象 | 不能使用字符串、数字等原始值作键。 |
| 值可以是任意类型 | 包括原始值、对象、函数等。 |
| 弱引用键 | 键对象如果没有其他引用,会被 GC 自动回收。 |
| 不可迭代 | 没有.size、.clear()、.keys()、for...of等方法。 |
| 私有性 | 无法枚举或查看内部内容(设计如此)。 |
核心思想:“我想给这个对象附加一些数据,但我不应该影响它的生命周期。”
基本用法:
不支持的操作。
// 报错:键不能是原始值。 wm.set('string',1);// TypeError。 wm.set(42,'value');// TypeError。// 不存在的方法。 wm.size;// undefined。 wm.clear();// TypeError: wm.clear is not a function。[...wm];// TypeError: wm is not iterable。创建和操作。
const wm =newWeakMap();// 键必须是对象。const obj1 ={};const obj2 ={id:1}; wm.set(obj1,'metadata for obj1'); wm.set(obj2,{count:42}); console.log(wm.get(obj1));// 'metadata for obj1'。 console.log(wm.has(obj2));// true。 wm.delete(obj1);典型应用场景:
- 不在对象上挂
_private属性。 - 实例销毁时,私有数据自动消失。
缓存计算结果(生命周期跟随对象)。
const cache =newWeakMap();functionexpensiveCalculation(obj){if(cache.has(obj)){return cache.get(obj);}const result =/* 耗时计算 */; cache.set(obj, result);return result;}const data ={values:[...]};expensiveCalculation(data);// 当data对象不再被使用,缓存自动释放。DOM 元素元数据缓存。
const domMetadata =newWeakMap();functionattachHandler(el){if(domMetadata.has(el))return;// 避免重复绑定。const state ={clickCount:0}; domMetadata.set(el, state); el.addEventListener('click',()=>{ state.clickCount++; console.log('Clicked', state.clickCount,'times');});}// 当el从DOM移除且无其他引用时,// state会自动被GC回收(无需手动清理!)。私有数据存储(不污染对象本身)。
// 模拟类的私有属性(ES2022前常用技巧)。const privateData =newWeakMap();classUser{constructor(name, ssn){this.name = name; privateData.set(this,{ ssn });// 私有数据。}getSSN(){return privateData.get(this).ssn;}}const user =newUser('Alice','123-45-6789'); console.log(user.getSSN());// '123-45-6789'。// 外部无法直接访问 ssn!优势:
WeakMap vs Map:
| 特性 | Map | WeakMap |
|---|---|---|
| 键的类型 | 任意类型。 | 仅对象。 |
| 引用类型 | 强引用(阻止 GC)。 | 弱引用(不阻止 GC)。 |
| 可遍历 | 是(.size, for...of等)。 | 否(完全不可遍历)。 |
| 用途 | 通用键值存储。 | 对象关联数据 + 自动内存管理。 |
选择原则:
- 需要遍历/计数 → 用
Map。 - 只想“悄悄”给对象附加数据 → 用
WeakMap。
重要注意事项:
- 弱引用 ≠ 立即回收。
- 对象是否被回收取决于GC时机。
- 即使
WeakMap是唯一引用,也可能暂时未被回收。
不能用于原始值作键。
// 常见错误:const wm =newWeakMap(); wm.set('id-123', userData);// TypeError。无法知道WeakMap里有什么。
const wm =newWeakMap(); wm.set({},'secret');// 你无法:// - 获取size。// - 列出所有键。// - 检查是否为空。// 这是刻意设计,确保弱引用语义不被破坏。高级技巧:结合FinalizationRegistry(谨慎使用)。
监听对象被GC的时机:
const registry =newFinalizationRegistry((heldValue)=>{ console.log(`对象 ${heldValue} 已被回收`);});const wm =newWeakMap();const obj ={id:1}; wm.set(obj,'data'); registry.register(obj,'Object with id=1'); obj =null;// 解除引用。// 未来GC后,会触发回调。FinalizationRegistry是实验性API,不推荐常规使用。
总结:
WeakMap是“对象专属的私密笔记本”:
- 你只能通过原对象查笔记。
- 对象消失了,笔记自动焚毁。
- 没人能偷看你的笔记本内容.
使用口诀:
- 键是对象(不能是字符串/数字)。
- 存私有数据(不污染对象)。
- 免内存泄漏(随对象自动清理)。
- 别想遍历(根本做不到)。
- 别问大小(没有
.size)。