一、Set 数据类型
JavaScript 中的 Set 是 ES6(ES2015)引入的一种集合数据结构,用于存储唯一值(unique values)的有序列表。无论是原始类型(如数字、字符串)还是对象引用,Set 都会自动去重。
本文详细介绍了 JavaScript ES6 引入的四种数据结构:Set、WeakSet、Map 和 WeakMap。Set 用于存储唯一值,支持去重和集合运算;WeakSet 仅存对象且为弱引用,不阻止垃圾回收。Map 是键值对集合,支持任意类型键并保持插入顺序;WeakMap 仅以对象为键,适合存储私有数据或元数据而不影响对象生命周期。文章对比了它们与普通对象的区别,提供了常用 API、遍历方法及典型应用场景,帮助开发者根据需求选择合适的数据结构。
JavaScript 中的 Set 是 ES6(ES2015)引入的一种集合数据结构,用于存储唯一值(unique values)的有序列表。无论是原始类型(如数字、字符串)还是对象引用,Set 都会自动去重。
| 特性 | 说明 |
|---|
| 值唯一 | 不允许重复元素(使用 === 判断相等,但 NaN === NaN 被视为相等)。 |
| 有序 | 元素按插入顺序迭代。 |
| 可存储任意类型 | 包括 number、string、object、NaN、undefined 等。 |
| 非索引结构 | 不能通过下标访问(不像数组),但可遍历。 |
注意:Set 中的 {} 和 {} 被视为不同对象(因为引用不同),所以不会去重。
| 方法 | 说明 | 示例 |
|---|---|---|
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。
// 空 Set
const s1 = new Set();
// 从可迭代对象初始化(如数组)。
const s2 = new Set([1, 2, 3, 2, 1]); // Set(3) {1, 2, 3}
// 字符串会被拆分为字符。
const s3 = new Set('hello'); // Set(4) {'h', 'e', 'l', 'o'}
Set vs Array。
| 场景 | 推荐 |
|---|---|
| 需要去重 | Set。 |
| 需要频繁判断元素是否存在 | Set(has() 时间复杂度 O(1),Array 的 includes() 是 O(n))。 |
| 需要索引/顺序操作(如 sort, splice) | Array。 |
| 存储大量数据且频繁增删 | Set 更高效。 |
特殊值处理。
const s = new Set();
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 = new Set(arr); // Set {1, 2, 3}
经典去重技巧:
const unique = [...new Set([1, 2, 2, 3])]; // [1, 2, 3]
Set → Array
const set = new Set([1, 2, 3]);
const arr = [...set]; // 或 Array.from(set)
遍历 Set。Set 是可迭代对象(iterable),支持以下方式遍历:
const set = new Set([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]
去除对象数组中的重复项(基于某属性)。
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' }
];
const uniqueUsers = Array.from(
new Set(users.map(u => u.id)),
id => users.find(u => u.id === id)
);
// 或使用 Map 更高效。
// 上述使用 Set,时间复杂度为 O(n^2),而使用下述 Map,时间复杂度为 O(n)。
const uniqueUsersMap = Array.from(new Map(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 = [...new Set([...a, ...b])]; // [1, 2, 3, 4, 5]
// 交集。
const intersection = [...new Set(a.filter(x => b.includes(x)))];
// 差集(a - b)。
const diff = a.filter(x => !new Set(b).has(x)); // [1, 2]
Set不支持直接获取第 n 个元素(无 .get(index) 方法)。Set 无法序列化为 JSON(需先转数组):JSON.stringify([...mySet]);
new Set([{ a: 1 }, { a: 1 }]).size; // 2(两个不同对象)。
Set 是处理'唯一值集合'的最佳工具,尤其适合去重、成员检测、集合运算等场景。结合扩展运算符和数组方法,能写出简洁高效的代码。.has()。for...of 或 forEach。[...set]。WeakSet 是 JavaScript(ES6 引入)中一种特殊的集合数据结构,它与 Set 类似,但有关键限制和用途,那就是 WeakSet 只能存储对象(不能是原始值),且对对象的引用是'弱引用'(weakly held)——不会阻止垃圾回收(GC)。
| 特性 | 说明 |
|---|---|
| 只能存对象 | 不能添加 number、string、boolean 等原始值。 |
| 弱引用 | 存入的对象如果没有其他引用,会被 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 = new WeakSet();
// 只能添加对象。
ws.add({});
ws.add(document.body);
ws.add(new Date()); // 报错:不能添加原始值。
ws.add(42); // TypeError
ws.add('hello'); // TypeError
ws.add(true); // TypeError
缓存或元数据(生命周期跟随对象)。
// 为每个请求对象附加临时元数据。
const requestMetadata = new WeakSet();
fetch('/api/data').then(res => {
requestMetadata.add(res); // ... 处理响应。
});
// 当 res 对象被 GC 回收时,元数据自动消失。
私有数据存储(不污染对象)。
// 模拟私有属性(避免在对象上直接挂 _privateData)。
const privateData = new WeakSet();
class User {
constructor(name) {
this.name = name;
privateData.add(this); // 标记该实例有私有数据。
}
isValid() {
// 检查是否属于'有效用户'。
return privateData.has(this);
}
}
标记对象(而不阻止 GC)。
// 用于标记'已处理'的 DOM 元素。
const processedElements = new WeakSet();
function processElement(el) {
if (processedElements.has(el)) {
return; // 已处理过,跳过。
}
// ... 处理逻辑。
processedElements.add(el);
}
// 当 el 从 DOM 移除且无其他引用时,
// 它会自动从 WeakSet 中消失(无需手动清理)。
| 特性 | Set | WeakSet |
|---|---|---|
| 存储类型 | 任意值(对象/原始值)。 | 仅对象。 |
| 引用类型 | 强引用(阻止 GC)。 | 弱引用(不阻止 GC)。 |
| 可遍历 | 是(for...of, .size 等)。 | 否(完全不可遍历)。 |
| 用途 | 通用去重、集合运算。 | 生命周期绑定对象的标记/元数据。 |
简单记忆:
Set 存'我要主动管理的数据'。WeakSet 存'这个对象有某种状态,但我不负责它的生死'。WeakSet 是唯一引用,也可能暂时未被回收。不能用于原始值。
// 常见错误:
const ids = new WeakSet();
ids.add(123); // TypeError: Invalid value used in weak set
无法遍历 = 无法知道内容。
const ws = new WeakSet();
ws.add({ a: 1 });
ws.add({ b: 2 });
// 你无法列出所有元素!
// 没有 ws.size, 没有 for...of, 没有 .values()
这是刻意设计:防止开发者依赖内部状态,确保弱引用语义。
监听对象被 GC 的时机(谨慎使用):
const registry = new FinalizationRegistry((heldValue) => {
console.log(`${heldValue} 被回收了`);
});
const ws = new WeakSet();
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 是 JavaScript(ES6/ES2015 引入)中一种键值对集合(key-value collection) 数据结构,用于存储任意类型的键和值的映射关系。它比普通对象({})更强大、更灵活,尤其适合键不是字符串或需要精确控制键值行为的场景。
| 特性 | 普通对象 {} | Map |
|---|---|---|
| 键的类型 | 仅字符串/Symbol。 | 任意类型(包括对象、函数、数字等)。 |
| 键的顺序 | 无序(ES2015 后部分有序)。 | 插入顺序(严格保持)。 |
| 大小获取 | 需手动计算(Object.keys(obj).length)。 | 直接 .size。 |
| 迭代 | 需 Object.keys() 等辅助。 | 原生可迭代(for...of、.entries() 等)。 |
| 原型污染风险 | 有(如 obj.__proto__)。 | 无(纯净数据结构)。 |
| 性能 | 大量动态属性时较慢。 | 大量数据时更快(专为频繁增删优化)。 |
简单说: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.size → 0 |
const map = new Map();
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 = new Map();
// 从可迭代对象初始化(如数组的数组)。
const map2 = new Map([
['name', 'Alice'],
[42, 'answer'],
[{ id: 1 }, 'user object']
]);
保持插入顺序。
const map = new Map();
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 = new Map();
map.set(NaN, 'not a number');
console.log(map.get(NaN)); // 'not a number'(普通对象做不到!)。
键可以是任意类型。
const user1 = { id: 1 };
const user2 = { id: 2 };
const userMap = new Map();
userMap.set(user1, 'Alice');
userMap.set(user2, 'Bob');
console.log(userMap.get(user1)); // 'Alice'
// 注意:必须用同一个对象引用!
console.log(userMap.get({ id: 1 })); // undefined(新对象)。
适用于:
| 特性 | Map | WeakMap |
|---|---|---|
| 键的类型 | 任意。 | 仅对象。 |
| 可枚举 | 是。 | 否(无 size、clear()、entries() 等)。 |
| 垃圾回收 | 键被强引用(阻止 GC)。 | 弱引用(键可被 GC 回收)。 |
| 用途 | 通用键值存储。 | 私有数据、DOM 元素元数据。 |
WeakMap 适合:不希望阻止对象被回收的场景(如给 DOM 元素附加临时数据)。
缓存函数结果(记忆化)。
const cache = new Map();
function expensiveFn(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 = [...new Map(users.map(u => [u.id, u])).values()];
Map 转对象(仅字符串键)。
const map = new Map([['a', 1], ['b', 2]]);
const obj = Object.fromEntries(map);
对象转 Map。
const obj = { a: 1, b: 2 };
const map = new Map(Object.entries(obj));
不支持点语法访问。
map.key; // undefined(必须用 map.get('key'))。
键是对象时,必须用相同引用。
map.set({ x: 1 }, 'value');
map.get({ x: 1 }); // undefined(新对象)。
不能用 JSON.stringify() 直接序列化。
JSON.stringify(new Map()); // '{}'
// 解决方案:先转为数组。
JSON.stringify([...map]);
| 场景 | 推荐 |
|---|---|
| 键是字符串/数字,结构固定。 | 普通对象 {}。 |
| 键类型多样、动态增删频繁。 | Map。 |
| 需要与对象互转(仅字符串键)。 | Object.entries()/Object.fromEntries()。 |
| 存储私有数据且不阻止 GC。 | WeakMap。 |
Map 是现代 JavaScript 中处理键值对的首选数据结构,当你遇到以下情况时,优先考虑它:
记住核心 API:
const m = new Map();
m.set(key, value);
m.get(key);
m.has(key);
m.delete(key);
m.size;
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 = new WeakMap();
// 键必须是对象。
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 = new WeakMap();
function expensiveCalculation(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 = new WeakMap();
function attachHandler(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 = new WeakMap();
class User {
constructor(name, ssn) {
this.name = name;
privateData.set(this, { ssn }); // 私有数据。
}
getSSN() {
return privateData.get(this).ssn;
}
}
const user = new User('Alice', '123-45-6789');
console.log(user.getSSN()); // '123-45-6789'
// 外部无法直接访问 ssn!
优势:
| 特性 | Map | WeakMap |
|---|---|---|
| 键的类型 | 任意类型。 | 仅对象。 |
| 引用类型 | 强引用(阻止 GC)。 | 弱引用(不阻止 GC)。 |
| 可遍历 | 是(.size, for...of 等)。 | 否(完全不可遍历)。 |
| 用途 | 通用键值存储。 | 对象关联数据 + 自动内存管理。 |
选择原则:
Map。WeakMap。WeakMap 是唯一引用,也可能暂时未被回收。不能用于原始值作键。
// 常见错误:
const wm = new WeakMap();
wm.set('id-123', userData); // TypeError
无法知道 WeakMap 里有什么。
const wm = new WeakMap();
wm.set({}, 'secret');
// 你无法:
// - 获取 size。
// - 列出所有键。
// - 检查是否为空。
// 这是刻意设计,确保弱引用语义不被破坏。
监听对象被 GC 的时机:
const registry = new FinalizationRegistry((heldValue) => {
console.log(`对象 ${heldValue} 已被回收`);
});
const wm = new WeakMap();
const obj = { id: 1 };
wm.set(obj, 'data');
registry.register(obj, 'Object with id=1');
obj = null; // 解除引用。
// 未来 GC 后,会触发回调。
FinalizationRegistry 是实验性 API,不推荐常规使用。
WeakMap 是'对象专属的私密笔记本':
使用口诀:
.size)。
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online