跳到主要内容

Set 和 Map

问题

JavaScript 中的 SetMap 是什么?它们与数组和对象有什么区别?

答案

SetMap 是 ES6 引入的两种新的数据结构,用于解决数组和对象在某些场景下的不足。


Set

什么是 Set?

Set 是一个值的集合,其中的值是唯一的,不会重复。

const set = new Set<number>();

set.add(1);
set.add(2);
set.add(2); // 重复,不会添加

console.log(set); // Set(2) { 1, 2 }
console.log(set.size); // 2

Set 的常用方法

方法说明返回值
add(value)添加值Set 本身(可链式调用)
delete(value)删除值boolean
has(value)检查是否存在boolean
clear()清空所有值void
size获取元素数量number
const set = new Set<string>();

// 链式调用
set.add('a').add('b').add('c');

// 检查存在
console.log(set.has('a')); // true
console.log(set.has('d')); // false

// 删除
set.delete('b');
console.log(set); // Set(2) { 'a', 'c' }

// 清空
set.clear();
console.log(set.size); // 0

Set 的遍历

const set = new Set(['a', 'b', 'c']);

// for...of
for (const value of set) {
console.log(value);
}

// forEach
set.forEach((value) => {
console.log(value);
});

// 转换为数组
const arr = [...set];
// 或
const arr2 = Array.from(set);

Set vs Array

对比维度SetArray
值唯一性自动保证唯一允许重复
查找 has / includesO(1)O(1)(哈希查找)O(n)O(n)(线性扫描)
删除指定值delete(value)O(1)O(1)splice / filterO(n)O(n)
添加add(value)O(1)O(1)pushO(1)O(1)
索引访问❌ 不支持 set[0]arr[0]
获取大小.size.length
高阶方法❌ 无 map / filter / reduce✅ 全部支持
有序性按插入顺序按插入顺序
JSON 序列化❌ 需先转数组✅ 原生支持
核心差异:查找和删除性能

Set 基于哈希表实现,hasdeleteO(1)O(1);Array 的 includessplice(按值删除)是 O(n)O(n)当数据量大且需要频繁查找/删除时,Set 的性能优势非常明显。

性能对比:查找 10 万个元素
const size = 100_000;
const arr = Array.from({ length: size }, (_, i) => i);
const set = new Set(arr);

// Array.includes → O(n),查找末尾元素需遍历整个数组
console.time('Array.includes');
arr.includes(99_999); // 遍历 10 万次
console.timeEnd('Array.includes'); // ~1-2ms

// Set.has → O(1),哈希查找,直接命中
console.time('Set.has');
set.has(99_999); // 1 次哈希计算
console.timeEnd('Set.has'); // ~0.001ms
按值删除的对比
// Array:删除值为 3 的元素 → O(n)
const arr2 = [1, 2, 3, 4, 5];
const index = arr2.indexOf(3); // O(n) 查找
if (index !== -1) arr2.splice(index, 1); // O(n) 移动元素

// Set:删除值为 3 的元素 → O(1)
const set2 = new Set([1, 2, 3, 4, 5]);
set2.delete(3); // O(1) 直接删除

什么时候用 Set 代替 Array?

// ✅ 1. 需要频繁判断"某个值存不存在"
const visited = new Set<string>();
function visit(url: string): void {
if (visited.has(url)) return; // O(1)
visited.add(url);
// 处理...
}

// ✅ 2. 需要保证唯一性(如收集不重复的标签)
const tags = new Set<string>();
tags.add('JavaScript');
tags.add('TypeScript');
tags.add('JavaScript'); // 自动忽略
console.log([...tags]); // ['JavaScript', 'TypeScript']

// ✅ 3. 需要高效的按值删除
const onlineUsers = new Set<string>();
onlineUsers.add('user1');
onlineUsers.add('user2');
onlineUsers.delete('user1'); // O(1)

// ❌ 不适合用 Set 的场景:需要索引、排序、map/filter/reduce
const numbers = [3, 1, 4, 1, 5];
numbers.sort(); // Set 不支持
numbers.map(n => n * 2); // Set 不支持
console.log(numbers[0]); // Set 不支持索引访问

Set 的实际应用

1. 数组去重

const arr = [1, 2, 2, 3, 3, 3, 4];
const unique = [...new Set(arr)];
console.log(unique); // [1, 2, 3, 4]

// 封装为函数
function dedupe<T>(arr: T[]): T[] {
return [...new Set(arr)];
}

2. 集合运算

const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);

// 并集
const union = new Set([...setA, ...setB]);
console.log(union); // Set(6) { 1, 2, 3, 4, 5, 6 }

// 交集
const intersection = new Set([...setA].filter(x => setB.has(x)));
console.log(intersection); // Set(2) { 3, 4 }

// 差集(A - B)
const difference = new Set([...setA].filter(x => !setB.has(x)));
console.log(difference); // Set(2) { 1, 2 }

3. 检查重复

function hasDuplicates<T>(arr: T[]): boolean {
return new Set(arr).size !== arr.length;
}

console.log(hasDuplicates([1, 2, 3])); // false
console.log(hasDuplicates([1, 2, 2, 3])); // true

Map

什么是 Map?

Map 是一个键值对的集合,与普通对象不同,Map 的键可以是任意类型

const map = new Map<string, number>();

map.set('a', 1);
map.set('b', 2);

console.log(map.get('a')); // 1
console.log(map.size); // 2

Map 的常用方法

方法说明返回值
set(key, value)设置键值对Map 本身(可链式调用)
get(key)获取值value | undefined
delete(key)删除键值对boolean
has(key)检查键是否存在boolean
clear()清空所有键值对void
keys()返回所有键的迭代器MapIterator
values()返回所有值的迭代器MapIterator
entries()返回所有 [key, value] 的迭代器MapIterator
forEach(cb)遍历每个键值对void
size获取键值对数量number
const map = new Map<string, number>();

// 链式调用
map.set('x', 10).set('y', 20).set('z', 30);

// 获取值
console.log(map.get('x')); // 10
console.log(map.get('w')); // undefined

// 检查存在
console.log(map.has('x')); // true

// 删除
map.delete('y');
console.log(map.size); // 2

Map 的遍历

const map = new Map([
['name', 'Alice'],
['age', '25'],
]);

// 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) => {
console.log(`${key}: ${value}`);
});

Map vs Object

特性MapObject
键的类型任意类型字符串或 Symbol
键的顺序按插入顺序不保证(数字键会排序)
获取大小map.sizeObject.keys(obj).length
遍历直接可迭代需要 Object.keys()
性能频繁增删更优读取固定键更快
JSON 序列化不支持原生支持
// Map 的键可以是任意类型
const map = new Map<object, string>();
const objKey = { id: 1 };
map.set(objKey, 'value');
console.log(map.get(objKey)); // 'value'

// Object 的键只能是字符串
const obj: Record<string, string> = {};
obj[objKey.toString()] = 'value'; // 键变成 "[object Object]"

Map 的实际应用

1. 缓存函数结果(Memoization)

function memoize<T extends (...args: any[]) => any>(fn: T): T {
const cache = new Map<string, ReturnType<T>>();

return ((...args: Parameters<T>) => {
const key = JSON.stringify(args);

if (cache.has(key)) {
return cache.get(key)!;
}

const result = fn(...args);
cache.set(key, result);
return result;
}) as T;
}

// 使用
const expensiveCalculation = memoize((n: number) => {
console.log('计算中...');
return n * n;
});

expensiveCalculation(5); // 计算中... 25
expensiveCalculation(5); // 25(从缓存获取,不打印)

2. 统计频率

function countFrequency<T>(arr: T[]): Map<T, number> {
const map = new Map<T, number>();

for (const item of arr) {
map.set(item, (map.get(item) ?? 0) + 1);
}

return map;
}

const arr = ['a', 'b', 'a', 'c', 'a', 'b'];
console.log(countFrequency(arr));
// Map(3) { 'a' => 3, 'b' => 2, 'c' => 1 }

3. 两数之和(经典算法题)

function twoSum(nums: number[], target: number): number[] {
const map = new Map<number, number>(); // 值 -> 索引

for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];

if (map.has(complement)) {
return [map.get(complement)!, i];
}

map.set(nums[i], i);
}

return [];
}

WeakSet 和 WeakMap

WeakSet

WeakSet 只能存储对象引用,且是弱引用(不阻止垃圾回收)。

const weakSet = new WeakSet<object>();

let obj = { name: 'test' };
weakSet.add(obj);

console.log(weakSet.has(obj)); // true

obj = null; // 对象可能被垃圾回收
// weakSet 中的引用也会自动消失

特点

  • 只能存储对象
  • 不可遍历
  • 没有 size 属性
  • 不阻止垃圾回收

使用场景:标记对象是否被处理过

const processed = new WeakSet<object>();

function process(obj: object): void {
if (processed.has(obj)) {
console.log('已处理过');
return;
}

// 处理对象...
processed.add(obj);
}

WeakMap

WeakMap 的键必须是对象,且是弱引用

const weakMap = new WeakMap<object, string>();

let key = { id: 1 };
weakMap.set(key, 'value');

console.log(weakMap.get(key)); // 'value'

key = null; // 键对象可能被垃圾回收
// weakMap 中的键值对也会自动消失

使用场景:存储对象的私有数据

const privateData = new WeakMap<object, { secret: string }>();

class User {
constructor(name: string) {
privateData.set(this, { secret: 'hidden' });
}

getSecret(): string {
return privateData.get(this)!.secret;
}
}

const user = new User('Alice');
console.log(user.getSecret()); // 'hidden'
// 无法从外部访问 privateData

对比总结

特性ArrayObjectSetMap
存储方式有序列表键值对唯一值集合键值对集合
键类型数字索引字符串/Symbol-任意类型
值唯一性--
有序性部分
获取大小.length需计算.size.size

常见面试问题

Q1: Set 如何判断两个值相等?

Set 使用 SameValueZero 算法判断相等,类似于 ===,但认为 NaN === NaN

const set = new Set();

set.add(NaN);
set.add(NaN);
console.log(set.size); // 1(NaN 被认为相等)

set.add(0);
set.add(-0);
console.log(set.size); // 2(0 和 -0 被认为相等)

Q2: Map 和 Object 该如何选择?

场景推荐
键是字符串且固定Object
键是动态的或非字符串Map
需要频繁增删Map
需要 JSON 序列化Object
需要保持插入顺序Map

Q3: 如何将 Map 转为 Object,Object 转为 Map?

// Map -> Object
const map = new Map([['a', 1], ['b', 2]]);
const obj = Object.fromEntries(map);
console.log(obj); // { a: 1, b: 2 }

// Object -> Map
const obj2 = { x: 10, y: 20 };
const map2 = new Map(Object.entries(obj2));
console.log(map2); // Map(2) { 'x' => 10, 'y' => 20 }

Q4: WeakMap 和 Map 的区别?

特性MapWeakMap
键类型任意仅对象
垃圾回收阻止不阻止
可遍历
size 属性

Q5: WeakMap 和 WeakSet 的使用场景有哪些?

答案

WeakMapWeakSet 的键(或值)是弱引用,不会阻止垃圾回收,因此特别适合以下场景:

1. DOM 元素关联数据

// 为 DOM 元素附加额外数据,元素移除后数据自动回收
const elementData = new WeakMap<HTMLElement, { clickCount: number; lastClick: number }>();

function trackElement(el: HTMLElement): void {
elementData.set(el, { clickCount: 0, lastClick: 0 });

el.addEventListener('click', () => {
const data = elementData.get(el);
if (data) {
data.clickCount++;
data.lastClick = Date.now();
}
});
}

// 当 el 被从 DOM 移除且没有其他引用时,
// WeakMap 中的条目会自动被 GC 清理,不会内存泄漏

2. 私有数据存储

// 使用 WeakMap 模拟私有属性
const privateProps = new WeakMap<object, { _password: string }>();

class User {
name: string;

constructor(name: string, password: string) {
this.name = name;
privateProps.set(this, { _password: password });
}

validatePassword(input: string): boolean {
return privateProps.get(this)?._password === input;
}
}

const user = new User('Alice', 'secret123');
console.log(user.name); // 'Alice'
console.log(user.validatePassword('secret123')); // true
// 无法从外部直接访问 _password

3. 缓存计算结果(防止内存泄漏)

// 对象参数的计算结果缓存
const computeCache = new WeakMap<object, number>();

function expensiveCompute(obj: { values: number[] }): number {
if (computeCache.has(obj)) {
return computeCache.get(obj)!;
}

const result = obj.values.reduce((sum, v) => sum + v * v, 0);
computeCache.set(obj, result);
return result;
}

let data = { values: [1, 2, 3, 4, 5] };
expensiveCompute(data); // 计算并缓存
expensiveCompute(data); // 直接返回缓存

data = null as any; // data 不再使用后,缓存自动被 GC 清理

4. WeakSet 标记已处理对象

// 防止重复处理
const processed = new WeakSet<object>();

function processOnce(obj: object): void {
if (processed.has(obj)) {
console.log('已处理过,跳过');
return;
}

// 处理逻辑...
console.log('正在处理...');
processed.add(obj);
}

// 防止循环引用导致无限递归(如深拷贝)
function deepClone<T>(obj: T, seen = new WeakSet<object>()): T {
if (obj === null || typeof obj !== 'object') return obj;

if (seen.has(obj as object)) return obj; // 检测循环引用
seen.add(obj as object);

const clone = (Array.isArray(obj) ? [] : {}) as T;
for (const key of Object.keys(obj as object)) {
(clone as any)[key] = deepClone((obj as any)[key], seen);
}
return clone;
}
不可遍历的原因

WeakMap 和 WeakSet 不支持遍历(没有 keys()values()entries()forEachsize),原因是:

  • 弱引用的对象随时可能被 GC 回收,遍历结果不确定
  • 如果支持遍历,引擎需要维护完整的键列表,这与"弱引用"的语义矛盾
  • 设计上刻意限制,确保不会意外阻止垃圾回收

Q6: Map 和 Object 作为键值对容器有什么区别?什么时候用 Map?

答案

对比维度MapObject
键类型任意类型(对象、函数、基础类型)stringSymbol
键的顺序严格按插入顺序整数键升序 → 字符串键按插入顺序 → Symbol
获取大小map.sizeO(1)O(1)Object.keys(obj).lengthO(n)O(n)
迭代方式原生可迭代(for...ofObject.keys/entries 转换
频繁增删性能优化过,更快未专门优化
原型链无原型干扰继承 Object.prototype,可能键冲突
JSON 序列化不支持,需手动转换原生 JSON.stringify 支持
默认键有(toStringconstructor 等原型属性)

键类型差异示例

// Object:非字符串键会被强制转换
const obj: Record<string, string> = {};
const key1 = { id: 1 };
const key2 = { id: 2 };

obj[key1 as any] = 'value1';
obj[key2 as any] = 'value2';

// 两个不同对象变成同一个键 "[object Object]"
console.log(Object.keys(obj)); // ['[object Object]']
console.log(obj[key1 as any]); // 'value2'(被覆盖了!)

// Map:任意类型都可以作为独立的键
const map = new Map<object, string>();
map.set(key1, 'value1');
map.set(key2, 'value2');

console.log(map.size); // 2(两个独立的键)
console.log(map.get(key1)); // 'value1'
console.log(map.get(key2)); // 'value2'

键顺序差异示例

// Object:整数键会被自动排序
const obj2: Record<string, string> = {};
obj2['b'] = '2';
obj2['1'] = '1';
obj2['a'] = '3';
obj2['0'] = '0';

console.log(Object.keys(obj2)); // ['0', '1', 'b', 'a'] ← 整数键排前面

// Map:严格保持插入顺序
const map2 = new Map<string, string>();
map2.set('b', '2');
map2.set('1', '1');
map2.set('a', '3');
map2.set('0', '0');

console.log([...map2.keys()]); // ['b', '1', 'a', '0'] ← 插入顺序

什么时候用 Map

// 1. 键不是字符串时(如用对象做键)
const nodeDepths = new Map<HTMLElement, number>();
document.querySelectorAll('*').forEach((el, i) => {
nodeDepths.set(el as HTMLElement, i);
});

// 2. 需要频繁增删时(Map 对此有优化)
const sessions = new Map<string, { token: string; expiresAt: number }>();

function addSession(id: string, token: string): void {
sessions.set(id, { token, expiresAt: Date.now() + 3600000 });
}

function removeExpired(): void {
const now = Date.now();
for (const [id, session] of sessions) {
if (session.expiresAt < now) {
sessions.delete(id);
}
}
}

// 3. 需要直接获取大小时
console.log(`当前活跃会话: ${sessions.size}`);

// 4. 需要保证键顺序时(如实现 LRU 缓存)
class SimpleLRU<K, V> {
private cache = new Map<K, V>();
constructor(private maxSize: number) {}

get(key: K): V | undefined {
if (!this.cache.has(key)) return undefined;
const value = this.cache.get(key)!;
// 移到末尾(最近使用)
this.cache.delete(key);
this.cache.set(key, value);
return value;
}

set(key: K, value: V): void {
if (this.cache.has(key)) this.cache.delete(key);
this.cache.set(key, value);
// 超出容量,删除最早插入的(第一个)
if (this.cache.size > this.maxSize) {
const firstKey = this.cache.keys().next().value!;
this.cache.delete(firstKey);
}
}
}
选择建议
  • 用 Object:键是固定的字符串、需要 JSON 序列化、作为函数参数/返回值的简单数据结构
  • 用 Map:键是动态的或非字符串类型、需要频繁增删、需要保证顺序、需要高效获取 size

Q7: Set 和 Array 有什么区别?什么时候用 Set?

答案

核心区别有三点:

  1. 唯一性:Set 自动去重,Array 允许重复
  2. 查找性能Set.has()O(1)O(1)(哈希查找),Array.includes()O(n)O(n)(线性扫描)
  3. 删除性能Set.delete()O(1)O(1),Array 按值删除需要 indexOf + splice,是 O(n)O(n)

选择指南

场景推荐
需要频繁查找"某个值是否存在"SetO(1)O(1) vs O(n)O(n)
需要保证值唯一Set
需要按值高效删除Set
需要索引访问(arr[i]Array
需要 map / filter / reduceArray
需要排序Array
需要 JSON 序列化Array

实际开发中常见的模式是 Set 和 Array 配合使用:用 Set 做查找和去重,最终用展开运算符 [...set] 转回 Array 做后续处理。

// 典型组合用法:用 Set 去重 + 交集,最终转回 Array
function intersect<T>(a: T[], b: T[]): T[] {
const setB = new Set(b); // 把 b 放入 Set
return [...new Set(a)].filter( // a 去重
item => setB.has(item) // O(1) 查找交集
);
}

intersect([1, 2, 2, 3], [2, 3, 4]); // [2, 3]

相关链接