引言
还记得刚学JavaScript那会儿,每次遇到数组去重的问题,总要写一堆循环加判断吗?或者想把一个对象当字典用,却发现键名只能字符串,那种憋屈的感觉?别担心,ES6带来的Set和Map就是来拯救我们的!
想象一下:你正在开发一个社交网站,需要处理成千上万的用户标签。用数组?去重能让你怀疑人生。用普通对象?遇到对象作为键值时就只能干瞪眼。这就是为什么Set和Map会成为现代JavaScript开发者的秘密武器------它们就像是数组和对象的"升级版",专治各种数据结构不服。
Set就像是个严格的门卫,保证每个值都是独一无二的,你再也不用写复杂的去重逻辑。而Map则像个万能的保险箱,不仅能用字符串当钥匙,甚至对象、函数这些"大件"都能当钥匙用。它们不仅让代码更简洁,处理大数据时还特别高效。那就让我们进入到这篇文章。
正文
Set数据结构详解:ES6中的值不重复集合
1. Set是什么?为什么需要它?
在JavaScript里,我们通常用 数组(Array) 来存储一组数据。但数组有个问题:它允许重复值。比如:
javascript
const arr = [1, 2, 2, 3, 3, 3];
console.log(arr); // [1, 2, 2, 3, 3, 3]
如果我们想要一个不重复的集合,传统做法是手动去重:
javascript
const uniqueArr = [...new Set(arr)]; // 以前可能要写循环+indexOf
console.log(uniqueArr); // [1, 2, 3]
但这样太麻烦了!于是,ES6 引入了 Set
------ 一个存储唯一值的集合,自动去重,操作更高效。
2. 基本用法:创建、增删查
(1)创建 Set
javascript
// 空 Set
const set = new Set();
// 从数组初始化(自动去重)
const fruits = new Set(['🍎', '🍌', '🍎', '🍊']);
console.log(fruits); // Set { '🍎', '🍌', '🍊' }
(2)添加元素(add
)
javascript
const numbers = new Set();
numbers.add(1);
numbers.add(2);
numbers.add(2); // 重复值会被忽略
console.log(numbers); // Set { 1, 2 }
(3)删除元素(delete
)
javascript
numbers.delete(1); // 删除成功返回 true
numbers.delete(99); // 删除不存在的值返回 false
(4)检查是否存在(has
)
javascript
console.log(numbers.has(2)); // true
console.log(numbers.has(5)); // false
(5)清空 Set(clear
)
javascript
numbers.clear();
console.log(numbers); // Set {}
(6)获取元素数量(size
)
javascript
const colors = new Set(['red', 'green', 'blue']);
console.log(colors.size); // 3
3. Set 的特别之处
(1)值的唯一性(===
,但 NaN
例外)
Set
使用 SameValueZero
算法(类似 ===
,但 NaN
被视为相等):
javascript
const specialSet = new Set();
specialSet.add(NaN);
specialSet.add(NaN); // 只会存一个 NaN
console.log(specialSet); // Set { NaN }
const obj1 = { name: 'Alice' };
const obj2 = { name: 'Alice' };
specialSet.add(obj1);
specialSet.add(obj2); // 两个不同对象,都会存进去
console.log(specialSet); // Set { NaN, { name: 'Alice' }, { name: 'Alice' } }
(2)遍历 Set(forEach
、for...of
)
Set
是可迭代的,可以用 forEach
或 for...of
遍历:
javascript
const letters = new Set(['a', 'b', 'c']);
// forEach
letters.forEach((value) => {
console.log(value); // a, b, c
});
// for...of
for (const letter of letters) {
console.log(letter); // a, b, c
}
(3)Set 没有索引,不能像数组那样直接取值
javascript
const nums = new Set([10, 20, 30]);
console.log(nums[0]); // undefined ❌
如果需要按索引访问,可以转成数组:
javascript
const numsArray = [...nums];
console.log(numsArray[0]); // 10 ✅
4. Set 的常见应用场景
(1)数组去重(最简单的方案)
javascript
const duplicates = [1, 2, 2, 3, 3, 3];
const unique = [...new Set(duplicates)];
console.log(unique); // [1, 2, 3]
(2)集合运算(并集、交集、差集)
javascript
const setA = new Set([1, 2, 3]);
const setB = new Set([2, 3, 4]);
// 并集
const union = new Set([...setA, ...setB]); // Set {1, 2, 3, 4}
// 交集
const intersection = new Set([...setA].filter(x => setB.has(x))); // Set {2, 3}
// 差集(A 有,B 没有)
const difference = new Set([...setA].filter(x => !setB.has(x))); // Set {1}
(3)存储 DOM 节点(避免重复操作)
javascript
const buttons = document.querySelectorAll('button');
const uniqueButtons = new Set(buttons);
uniqueButtons.forEach(button => {
button.addEventListener('click', handleClick);
});
5. WeakSet:弱引用的 Set
WeakSet
和 Set
类似,但有 3 个关键区别:
- 只能存储对象 (不能存基本类型
number
/string
等)。 - 没有
size
属性,不能遍历(因为弱引用随时可能被垃圾回收)。 - 适用于临时存储(比如检测某个对象是否被处理过)。
javascript
const weakSet = new WeakSet();
const obj = { id: 1 };
weakSet.add(obj);
console.log(weakSet.has(obj)); // true
// 当 obj 被回收后,weakSet 会自动清理它
Map数据结构详解:ES6中的键值对集合
1. Map是什么?为什么需要它?
在ES6之前,JavaScript里通常用 普通对象(Object) 来存储键值对。但对象有个很大的限制:键只能是字符串(或Symbol)。比如:
javascript
const obj = {};
obj[1] = '数字键会自动转字符串';
obj[{ id: 1 }] = '对象键会被转成[object Object]';
console.log(obj);
// { '1': '数字键会自动转字符串', '[object Object]': '对象键会被转成[object Object]' }
这样会导致很多问题:
- 数字键被强制转字符串 (
obj[1]
和obj['1']
相同) - 对象键失效 (所有对象都会被转成
'[object Object]'
) - 无法保证插入顺序(旧版JS对象不保证属性顺序)
于是,ES6 引入了 Map
------ 一个真正的键值对集合,支持任意类型的键 ,并且保持插入顺序。
2. 基本用法:创建、增删查改
(1)创建 Map
javascript
// 空 Map
const map = new Map();
// 从二维数组初始化
const userMap = new Map([
['name', 'Alice'],
[1, '年龄'],
[true, '是否VIP']
]);
console.log(userMap);
// Map { 'name' => 'Alice', 1 => '年龄', true => '是否VIP' }
(2)添加/修改键值(set
)
javascript
const map = new Map();
map.set('name', 'Bob'); // 字符串键
map.set(1, '数字键'); // 数字键
map.set({ id: 1 }, '对象键'); // 对象键
(3)获取值(get
)
javascript
console.log(map.get('name')); // 'Bob'
console.log(map.get(1)); // '数字键'
console.log(map.get({ id: 1 })); // undefined ❌(不同对象引用)
(4)删除键值(delete
)
javascript
map.delete('name'); // 删除成功返回 true
map.delete('不存在的键'); // 返回 false
(5)检查键是否存在(has
)
javascript
console.log(map.has(1)); // true
console.log(map.has('age')); // false
(6)清空 Map(clear
)
javascript
map.clear();
console.log(map); // Map {}
(7)获取键值对数量(size
)
javascript
console.log(map.size); // 0
3. Map 的特别之处
(1)键可以是任意类型
javascript
const func = () => {};
const obj = { id: 1 };
const advancedMap = new Map();
advancedMap.set(func, '函数作为键');
advancedMap.set(obj, '对象作为键');
advancedMap.set(NaN, 'NaN也可以作为键');
console.log(advancedMap.get(func)); // '函数作为键'
console.log(advancedMap.get(NaN)); // 'NaN也可以作为键'
(2)保持插入顺序(Object不保证顺序)
javascript
const obj = { 3: '三', 1: '一', 2: '二' };
console.log(Object.keys(obj)); // ['1', '2', '三'](顺序可能变)
const map = new Map([
[3, '三'],
[1, '一'],
[2, '二']
]);
console.log([...map.keys()]); // [3, 1, 2](严格按插入顺序)
(3)遍历 Map(forEach
、for...of
)
javascript
const userMap = new Map([
['name', 'Alice'],
['age', 25],
['isVIP', true]
]);
// forEach
userMap.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
// for...of(返回 [key, value] 数组)
for (const [key, value] of userMap) {
console.log(key, value);
}
(4)获取所有键/值/键值对
javascript
const keys = [...userMap.keys()]; // ['name', 'age', 'isVIP']
const values = [...userMap.values()]; // ['Alice', 25, true]
const entries = [...userMap.entries()]; // [['name', 'Alice'], ...]
4. Map vs Object(什么时候用Map?)
特性 | Map | Object |
---|---|---|
键类型 | 任意类型(函数、对象、NaN等) | 仅字符串或Symbol |
键顺序 | 严格按插入顺序 | 不保证顺序(旧版JS) |
大小获取 | map.size |
Object.keys(obj).length |
默认键 | 无 | 有原型链上的键(如toString ) |
性能 | 频繁增删时更优 | 字面量初始化更快 |
✅ 适合用 Map 的场景:
- 键需要是非字符串(如对象、函数)
- 需要严格保持插入顺序
- 需要频繁增删键值对
- 避免原型链污染(如用户输入作为键时)
✅ 适合用 Object 的场景:
- 键都是字符串/Symbol
- 需要JSON序列化(Map不能直接转JSON)
- 需要方法调用 (如
obj.toString()
)
5. 常见应用场景
(1)DOM节点关联数据(避免污染DOM)
javascript
const buttons = document.querySelectorAll('button');
const buttonData = new Map();
buttons.forEach(button => {
buttonData.set(button, { clicks: 0 });
button.addEventListener('click', () => {
const data = buttonData.get(button);
data.clicks++;
console.log(`点击次数: ${data.clicks}`);
});
});
(2)缓存计算结果(函数记忆化)
javascript
const cache = new Map();
function heavyCompute(x) {
if (cache.has(x)) return cache.get(x);
const result = x * x; // 模拟复杂计算
cache.set(x, result);
return result;
}
(3)替代"对象字典"(更安全的键)
javascript
// 用Object的问题:原型链可能被修改
const unsafeDict = {};
unsafeDict.toString = '恶意代码'; // 污染原型
// 用Map更安全
const safeDict = new Map();
safeDict.set('toString', '不会影响原型');
6. WeakMap:弱引用的Map
WeakMap
和 Map
的区别:
- 键必须是对象(不能是基本类型)
- 不可遍历 (没有
keys()
/values()
/size
) - 键是弱引用(不影响垃圾回收)
典型用途:存储私有数据
javascript
const privateData = new WeakMap();
class User {
constructor(name) {
privateData.set(this, { name }); // this作为键
}
getName() {
return privateData.get(this).name;
}
}
const user = new User('Alice');
console.log(user.getName()); // 'Alice'
// 当user被回收时,关联数据自动清除
介绍完了set和map,接下来介绍一下他们在性能优化,处理数据方面的作用吧
Set与Map性能优化策略
1. 大数据集处理技巧
(1)批量初始化 vs 逐个添加
-
❌ 低效方式(逐个
add
/set
)javascriptconst bigSet = new Set(); for (let i = 0; i < 1_000_000; i++) { bigSet.add(i); // 每次add都有微小的性能开销 }
-
✅ 高效方式(批量初始化)
javascriptconst data = Array.from({ length: 1_000_000 }, (_, i) => i); const bigSet = new Set(data); // 引擎可优化初始化过程
性能对比(V8引擎测试):
- 逐个添加:~500ms
- 批量初始化:~100ms
(2)避免频繁增删(Map
比 Object
更优)
当需要频繁增删键值对时:
javascript
// 测试:增删10万次
const map = new Map();
let obj = {};
console.time('Map');
for (let i = 0; i < 100_000; i++) {
map.set(i, 'value');
map.delete(i);
}
console.timeEnd('Map'); // ~50ms
console.time('Object');
for (let i = 0; i < 100_000; i++) {
obj[i] = 'value';
delete obj[i];
}
console.timeEnd('Object'); // ~120ms
结论 :Map
的增删操作比 Object
快约2倍。
2. 内存使用优化
(1)使用 WeakMap
/WeakSet
减少内存泄漏
-
问题场景 :用
Map
缓存DOM节点时,节点删除后仍被引用,无法垃圾回收。javascriptconst cache = new Map(); const node = document.getElementById('node'); cache.set(node, 'data'); // 即使移除DOM,node仍被Map引用,内存泄漏! node.remove();
-
解决方案 :改用
WeakMap
,键是弱引用。javascriptconst cache = new WeakMap(); // 当node被移除,自动释放内存 cache.set(node, 'data');
(2)避免存储重复对象
-
❌ 低效内存使用
javascriptconst users = new Set(); users.add({ id: 1, name: 'Alice' }); users.add({ id: 1, name: 'Alice' }); // 两个独立对象,内存翻倍
-
✅ 优化方式 :使用唯一标识符(如
id
)作为键。javascriptconst userMap = new Map(); userMap.set(1, { id: 1, name: 'Alice' }); // 相同id覆盖旧值
3. 遍历性能对比
(1)for...of
vs forEach
vs 转数组
测试遍历100万个元素的Set
:
javascript
const bigSet = new Set(Array.from({ length: 1_000_000 }, (_, i) => i));
console.time('for...of');
for (const item of bigSet) {} // ~15ms
console.timeEnd('for...of');
console.time('forEach');
bigSet.forEach(item => {}); // ~18ms
console.timeEnd('forEach');
console.time('转数组');
[...bigSet].forEach(item => {}); // ~120ms(额外数组分配开销)
console.timeEnd('转数组');
结论:
for...of
最快(直接访问迭代器)- 避免转数组遍历(内存和CPU双重开销)
(2)Map
遍历优化
-
按需遍历 :只取需要的部分(如
keys()
或values()
)。javascriptconst bigMap = new Map(Array.from({ length: 1_000_000 }, (_, i) => [i, i * 2])); // 只需要键时,避免遍历值 console.time('仅遍历键'); for (const key of bigMap.keys()) {} // ~12ms console.timeEnd('仅遍历键'); console.time('遍历键值'); for (const [key, value] of bigMap) {} // ~20ms console.timeEnd('遍历键值');
4. 综合性能建议
场景 | 优化策略 |
---|---|
初始化大数据集 | 用数组批量初始化 new Set(arr) /new Map(entries) |
频繁增删键值对 | 优先选 Map 而非 Object |
DOM/对象关联数据 | 用 WeakMap 防止内存泄漏 |
遍历大数据 | 用 for...of 直接迭代,避免转数组 |
内存敏感场景 | 避免在 Set /Map 中存储重复对象 |
终极技巧:
- 在Chrome DevTools的 Memory面板 检查
Set
/Map
的内存占用。 - 使用
performance.now()
测量关键操作的耗时。
总结
ES6 的 Set 和 Map 让数据处理变得更简单高效:
- Set:自动去重,适合存储唯一值
- Map:键值对集合,支持任意类型键
记住它们的优势:
✅ 性能更好 :查找、增删比传统方式快
✅ 代码更简洁 :省去手动去重或复杂判断
✅ 功能更强大:支持对象键、保持顺序等
下次遇到数组去重或键值存储需求,试试 Set
和 Map
吧!简洁又高效,真香! 🚀