JavaScript集合(Set、WeakSet)与映射(Map、WeakMap)

一、Set数据类型

JavaScript中的Set是ES6(ES2015)引入的一种集合数据结构,用于存储唯一值(unique values)的有序列表。无论是原始类型(如数字、字符串)还是对象引用,Set都会自动去重。


基本特性:

特性说明
值唯一不允许重复元素(使用===判断相等,但NaN === NaN被视为相等)。
有序元素按插入顺序迭代。
可存储任意类型包括numberstringobjectNaNundefined等。
非索引结构不能通过下标访问(不像数组),但可遍历。

注意:Set中的{}{}被视为不同对象(因为引用不同),所以不会去重。


详细介绍:

  1. 常用方法与属性。
  2. Set与Array互转。

Set vs Array。

场景推荐
需要去重Set
需要频繁判断元素是否存在Sethas()时间复杂度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...offorEach
    • 转数组用[...set]

二、WeakSet数据类型

WeakSet是JavaScript(ES6引入)中一种特殊的集合数据结构,它与Set类似,但有关键限制和用途,那就是WeakSet只能存储对象(不能是原始值),且对对象的引用是“弱引用”(weakly held)——不会阻止垃圾回收(GC)。


核心特性:

特性说明
只能存对象不能添加numberstringboolean等原始值。
弱引用存入的对象如果没有其他引用,会被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

特性SetWeakSet
存储类型任意值(对象/原始值)。仅对象
引用类型强引用(阻止 GC)。弱引用(不阻止 GC)
可遍历是(for...of, .size等)。否(完全不可遍历)。
用途通用去重、集合运算。生命周期绑定对象的标记/元数据

简单记忆

  • Set存“我要主动管理的数据”。
  • WeakSet存“这个对象有某种状态,但我不负责它的生死”。

重要注意事项:

  1. 弱引用 ≠ 立即回收
    • 对象是否被回收取决于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是“真正的哈希表”,而对象是“为固定结构设计的”。


基本用法:

  1. 遍历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.size0
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

特性MapWeakMap
键的类型任意。仅对象
可枚举是。否(无 sizeclear()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

特性MapWeakMap
键的类型任意类型。仅对象
引用类型强引用(阻止 GC)。弱引用(不阻止 GC)
可遍历是(.size, for...of等)。否(完全不可遍历)。
用途通用键值存储。对象关联数据 + 自动内存管理

选择原则

  • 需要遍历/计数 → 用Map
  • 只想“悄悄”给对象附加数据 → 用WeakMap

重要注意事项:

  1. 弱引用 ≠ 立即回收
    • 对象是否被回收取决于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)。
Could not load content