ES6 Set与Map完全指南:从入门到性能优化

引言

还记得刚学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(forEachfor...of

Set 是可迭代的,可以用 forEachfor...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

WeakSetSet 类似,但有 3 个关键区别:

  1. 只能存储对象 (不能存基本类型 number/string 等)。
  2. 没有 size 属性,不能遍历(因为弱引用随时可能被垃圾回收)。
  3. 适用于临时存储(比如检测某个对象是否被处理过)。
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(forEachfor...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

WeakMapMap 的区别:

  1. 键必须是对象(不能是基本类型)
  2. 不可遍历 (没有keys()/values()/size
  3. 键是弱引用(不影响垃圾回收)

典型用途:存储私有数据

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

    javascript 复制代码
    const bigSet = new Set();
    for (let i = 0; i < 1_000_000; i++) {
      bigSet.add(i); // 每次add都有微小的性能开销
    }
  • ✅ 高效方式(批量初始化)

    javascript 复制代码
    const data = Array.from({ length: 1_000_000 }, (_, i) => i);
    const bigSet = new Set(data); // 引擎可优化初始化过程

    性能对比(V8引擎测试):

    • 逐个添加:~500ms
    • 批量初始化:~100ms
(2)避免频繁增删(MapObject 更优)

当需要频繁增删键值对时:

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节点时,节点删除后仍被引用,无法垃圾回收。

    javascript 复制代码
    const cache = new Map();
    const node = document.getElementById('node');
    cache.set(node, 'data');
    
    // 即使移除DOM,node仍被Map引用,内存泄漏!
    node.remove();
  • 解决方案 :改用 WeakMap,键是弱引用。

    javascript 复制代码
    const cache = new WeakMap(); // 当node被移除,自动释放内存
    cache.set(node, 'data');
(2)避免存储重复对象
  • ❌ 低效内存使用

    javascript 复制代码
    const users = new Set();
    users.add({ id: 1, name: 'Alice' });
    users.add({ id: 1, name: 'Alice' }); // 两个独立对象,内存翻倍
  • ✅ 优化方式 :使用唯一标识符(如id)作为键。

    javascript 复制代码
    const 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())。

    javascript 复制代码
    const 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 的 SetMap 让数据处理变得更简单高效:

  • Set:自动去重,适合存储唯一值
  • Map:键值对集合,支持任意类型键

记住它们的优势:

性能更好 :查找、增删比传统方式快

代码更简洁 :省去手动去重或复杂判断

功能更强大:支持对象键、保持顺序等

下次遇到数组去重或键值存储需求,试试 SetMap 吧!简洁又高效,真香! 🚀

相关推荐
恋猫de小郭37 分钟前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端