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 吧!简洁又高效,真香! 🚀

相关推荐
范范之交几秒前
JavaScript基础语法two
开发语言·前端·javascript
界面开发小八哥34 分钟前
DevExtreme Angular UI控件更新:引入全新严格类型配置组件
前端·ui·界面控件·angular.js·devexpress
bitbitDown43 分钟前
重构缓存时踩的坑:注释了三行没用的代码却导致白屏
前端·javascript·vue.js
xiaopengbc1 小时前
火狐(Mozilla Firefox)浏览器离线安装包下载
前端·javascript·firefox
用户016523844411 小时前
Webpack5 入门与实战,前端开发必备技能无密
前端
小高0071 小时前
🔥🔥🔥前端性能优化实战手册:从网络到运行时,一套可复制落地的清单
前端·javascript·面试
古夕1 小时前
my-first-ai-web_问题记录01:Next.js的App Router架构下的布局(Layout)使用
前端·javascript·react.js
杨超越luckly1 小时前
HTML应用指南:利用POST请求获取上海黄金交易所金价数据
前端·信息可视化·金融·html·黄金价格
Jerry1 小时前
Compose 中的基本布局
前端
Hilaku1 小时前
深入WeakMap和WeakSet:管理数据和防止内存泄漏
前端·javascript·性能优化