JavaScript(JS)中的 Map 和 Set,它们都是 ES6 引入的集合类型,用于更灵活地存储和操作数据。
一、Map 详解
- Map 是什么?
Map 是键值对集合,类似对象 Object,但它的键可以是任意类型:
· 字符串、数字、布尔、null、undefined
· 对象、数组、函数
· NaN、Symbol 等
并且 Map 会保持插入顺序。
js
const map = new Map();
const objKey = { id: 1 };
const funcKey = function () {};
map.set('name', 'Alice');
map.set(1, 'one');
map.set(objKey, '对象键');
map.set(funcKey, '函数键');
map.set(NaN, 'NaN 也可以作为键');
console.log(map.get('name')); // Alice
console.log(map.get(objKey)); // 对象键
console.log(map.get(NaN)); // NaN 也可以作为键
- 创建 Map
js
// 空 Map
const map1 = new Map();
// 从二维数组创建
const map2 = new Map([
['a', 1],
['b', 2],
]);
// 从另一个 Map 创建
const map3 = new Map(map2);
// 从对象创建
const map4 = new Map(Object.entries({ a: 1, b: 2 }));
- Map 常用 API
方法/属性 说明
map.set(key, value) 添加/修改键值对,返回 Map 本身,可链式调用
map.get(key) 获取值,不存在返回 undefined
map.has(key) 判断是否存在某个键,返回布尔值
map.delete(key) 删除某个键,返回是否删除成功
map.clear() 清空所有键值对
map.size 返回键值对数量,是属性,不是方法
map.keys() 返回键的迭代器
map.values() 返回值的迭代器
map.entries() 返回 key, value 迭代器
map.forEach() 遍历
js
const map = new Map();
map.set('a', 1).set('b', 2).set('c', 3);
console.log(map.size); // 3
console.log(map.get('a')); // 1
console.log(map.has('b')); // true
map.delete('b');
console.log(map.has('b')); // false
map.clear();
console.log(map.size); // 0
- 遍历 Map
js
const map = new Map([
['a', 1],
['b', 2],
['c', 3],
]);
// for...of,默认遍历 entries
for (const [key, value] of map) {
console.log(key, value);
}
// 只遍历键
for (const key of map.keys()) {
console.log(key);
}
// 只遍历值
for (const value of map.values()) {
console.log(value);
}
// forEach
map.forEach((value, key, map) => {
console.log(key, value);
});
注意:Map 的 forEach 回调参数顺序是 (value, key, map),和数组的 forEach((item, index)) 不同。
- Map 的键比较规则
Map 的键比较使用 SameValueZero 算法:
· NaN 等于 NaN
· +0 和 -0 视为相同
· 对象、数组、函数等按引用比较,不是内容比较
js
const map = new Map();
map.set(NaN, 'nan');
console.log(map.get(NaN)); // nan
map.set({}, 'obj1');
console.log(map.get({})); // undefined,因为不是同一个对象引用
const obj = {};
map.set(obj, 'obj2');
console.log(map.get(obj)); // obj2
- Map 与 Object 的区别
对比项 Map Object
键类型 任意类型 只能是 string 或 symbol
顺序 严格插入顺序 数字键会排序,字符串键按插入顺序
大小 map.size Object.keys(obj).length
迭代 可直接迭代 需要 Object.keys/values/entries
原型 无原型链干扰 有原型链,可能键冲突
频繁增删 通常更优 一般
JSON 不能直接序列化 可直接 JSON.stringify
- Map 转换
js
const map = new Map([
['a', 1],
['b', 2],
]);
// Map 转数组
console.log([...map]); // [['a', 1], ['b', 2]]
console.log([...map.keys()]); // ['a', 'b']
console.log([...map.values()]); // [1, 2]
// Map 转对象
const obj = Object.fromEntries(map);
console.log(obj); // { a: 1, b: 2 }
// 对象转 Map
const map2 = new Map(Object.entries(obj));
// JSON 序列化
console.log(JSON.stringify([...map])); // [["a",1],["b",2]]
- Map 使用场景
· 需要非字符串键,比如对象、函数作为键
· 需要频繁增删键值对
· 需要严格保持插入顺序
· 需要方便获取大小 size
· 缓存、频率统计、数据关联
频率统计示例:
js
const arr = ['a', 'b', 'a', 'c', 'b', 'a'];
const countMap = new Map();
for (const item of arr) {
countMap.set(item, (countMap.get(item) || 0) + 1);
}
console.log(countMap); // Map(3) { 'a' => 3, 'b' => 2, 'c' => 1 }
二、Set 详解
- Set 是什么?
Set 是值的集合,其中的值唯一,不能重复。值可以是任意类型,并且保持插入顺序。
js
const set = new Set();
set.add(1);
set.add(2);
set.add(2); // 重复,不会添加
set.add('hello');
set.add({ a: 1 });
console.log(set); // Set(4) { 1, 2, 'hello', { a: 1 } }
- 创建 Set
js
// 空 Set
const set1 = new Set();
// 从数组创建
const set2 = new Set([1, 2, 2, 3]);
console.log(set2); // Set(3) { 1, 2, 3 }
// 从字符串创建
const set3 = new Set('hello');
console.log(set3); // Set(4) { 'h', 'e', 'l', 'o' }
// 从另一个 Set 创建
const set4 = new Set(set2);
- Set 常用 API
方法/属性 说明
set.add(value) 添加值,返回 Set 本身,可链式调用
set.has(value) 判断是否存在,返回布尔值
set.delete(value) 删除值,返回是否删除成功
set.clear() 清空
set.size 返回元素数量,是属性
set.keys() 返回值的迭代器
set.values() 返回值的迭代器,和 keys() 一样
set.entries() 返回 value, value 迭代器
set.forEach() 遍历
js
const set = new Set();
set.add(1).add(2).add(3);
console.log(set.size); // 3
console.log(set.has(2)); // true
set.delete(2);
console.log(set.has(2)); // false
set.clear();
console.log(set.size); // 0
- 遍历 Set
js
const set = new Set(['a', 'b', 'c']);
// for...of
for (const value of set) {
console.log(value);
}
// keys() 和 values() 一样
for (const value of set.values()) {
console.log(value);
}
// entries() 返回 [value, value]
for (const [v1, v2] of set.entries()) {
console.log(v1, v2);
}
// forEach
set.forEach((value, value2, set) => {
console.log(value, value2);
});
注意:Set 的 forEach 回调参数是 (value, value, set),第一个和第二个参数都是当前值。
- Set 的唯一性规则
Set 也使用 SameValueZero 比较:
· NaN 只存一个
· +0 和 -0 视为相同
· 对象按引用比较
js
const set = new Set();
set.add(NaN);
set.add(NaN);
console.log(set.size); // 1
set.add(+0);
set.add(-0);
console.log(set.size); // 2,因为前面已有 NaN,+0 和 -0 只算一个
const obj1 = {};
const obj2 = {};
set.add(obj1);
set.add(obj2);
console.log(set.size); // 4,两个不同对象引用
- Set 与 Array 的区别
对比项 Set Array
唯一性 值唯一 可重复
索引 无索引 有索引
查找 has 通常 O(1) includes 通常 O(n)
顺序 插入顺序 插入顺序
方法 较少 丰富,如 map/filter/reduce
去重 天然去重 需手动去重
- Set 转换
js
const set = new Set([1, 2, 3]);
// Set 转数组
const arr1 = [...set]; // [1, 2, 3]
const arr2 = Array.from(set); // [1, 2, 3]
// 数组转 Set
const set2 = new Set([1, 2, 2, 3]); // Set(3) { 1, 2, 3 }
// 数组去重
const unique = [...new Set([1, 2, 2, 3, 3, 4])];
console.log(unique); // [1, 2, 3, 4]
// JSON 序列化
console.log(JSON.stringify([...set])); // [1,2,3]
- Set 使用场景
· 数组去重
· 判断元素是否存在
· 集合运算:并集、交集、差集、对称差集
· 记录已访问、已选中的元素
集合运算示例:
js
const a = new Set([1, 2, 3]);
const b = new Set([3, 4, 5]);
// 并集
const union = new Set([...a, ...b]);
console.log(union); // Set(5) { 1, 2, 3, 4, 5 }
// 交集
const intersection = new Set([...a].filter(x => b.has(x)));
console.log(intersection); // Set(1) { 3 }
// 差集:a 有 b 没有
const difference = new Set([...a].filter(x => !b.has(x)));
console.log(difference); // Set(2) { 1, 2 }
// 对称差集:只在一个集合中出现
const symmetricDifference = new Set([
...[...a].filter(x => !b.has(x)),
...[...b].filter(x => !a.has(x)),
]);
console.log(symmetricDifference); // Set(4) { 1, 2, 4, 5 }
三、WeakMap 和 WeakSet
- WeakMap
WeakMap 是键值对集合,但:
· 键必须是对象
· 键是弱引用
· 不能遍历,没有 size、clear、keys、values、entries、forEach
· 只有 set、get、has、delete
js
const wm = new WeakMap();
const obj = {};
wm.set(obj, '一些数据');
console.log(wm.get(obj)); // 一些数据
console.log(wm.has(obj)); // true
wm.delete(obj);
console.log(wm.has(obj)); // false
特点:当 obj 没有其他引用时,可以被垃圾回收,WeakMap 中的对应数据也会自动释放,避免内存泄漏。
用途:
· 给 DOM 节点关联数据
· 给对象添加私有数据
· 缓存对象相关数据
- WeakSet
WeakSet 是值的集合,但:
· 值必须是对象
· 值是弱引用
· 不能遍历,没有 size、clear、keys、values、entries、forEach
· 只有 add、has、delete
js
const ws = new WeakSet();
const obj = {};
ws.add(obj);
console.log(ws.has(obj)); // true
ws.delete(obj);
console.log(ws.has(obj)); // false
用途:
· 标记对象是否被处理过
· 避免内存泄漏
· 不想手动管理集合生命周期时
四、对比总结
Map vs Object
特性 Map Object
键类型 任意 string / symbol
顺序 插入顺序 数字键排序,字符串键插入顺序
大小 size 手动计算
迭代 直接可迭代 需要转换
原型干扰 无 有
JSON 不直接支持 支持
Set vs Array
特性 Set Array
唯一性 唯一 可重复
索引 无 有
查找 has O(1) includes O(n)
方法 少 多
去重 天然支持 需手动
Map vs WeakMap
特性 Map WeakMap
键类型 任意 必须是对象
引用 强引用 弱引用
可遍历 是 否
size 有 无
clear 有 无
垃圾回收 不自动释放 键被回收后自动释放
Set vs WeakSet
特性 Set WeakSet
值类型 任意 必须是对象
引用 强引用 弱引用
可遍历 是 否
size 有 无
clear 有 无
垃圾回收 不自动释放 值被回收后自动释放
五、注意事项
- Map / Set 不是数组,不能直接使用 map、filter、reduce,需要先转数组。
- JSON.stringify 默认不支持 Map / Set:
· JSON.stringify(new Map()) 得到 {}
· JSON.stringify(new Set()) 得到 {}
· 需要手动转数组或对象。 - 对象作为键或值时,按引用比较,不是内容比较。
- Map / Set 的 size 是属性,不是方法,不要写成 size()。
- WeakMap / WeakSet 不可遍历,所以不能用于需要遍历的场景。
- WeakMap 的键、WeakSet 的值必须是对象,不能是原始值。
- Map 的 forEach 参数是 (value, key, map),Set 的 forEach 参数是 (value, value, set)。
六、一句话总结
· Map:键值对集合,键可以是任意类型,适合频繁增删、非字符串键、保持顺序。
· Set:唯一值集合,适合去重、存在性判断、集合运算。
· WeakMap / WeakSet:弱引用版本,键/值必须是对象,不可遍历,适合避免内存泄漏。
· 需要遍历、大小、清空:用 Map / Set。
· 需要弱引用、自动垃圾回收:用 WeakMap / WeakSet。