返回首页

26. 说说你对Set和Map的理解?

问题解析

Set 和 Map 是 ES6 引入的两种新的数据结构,它们为 JavaScript 提供了更高效的集合操作能力。面试中通常会考察它们的基本用法、特点、与普通对象的区别以及应用场景。

核心概念

1. Set

Set 是一种无序且唯一的数据集合,类似于数组,但成员的值都是唯一的,没有重复。

// 创建 Set
const set = new Set();

// 通过数组创建
const set2 = new Set([1, 2, 3, 4, 4, 4]);
console.log(set2); // Set {1, 2, 3, 4}

// 基本操作
set.add(1);
set.add(2);
set.add(1); // 重复值不会被添加
console.log(set); // Set {1, 2}

2. Map

Map 是一种键值对的集合,类似于对象,但键可以是任意类型(包括对象、函数等)。

// 创建 Map
const map = new Map();

// 通过数组创建
const map2 = new Map([
  ['name', 'Alice'],
  ['age', 25]
]);

// 基本操作
map.set('key1', 'value1');
map.set(123, 'number key');
map.set({ id: 1 }, 'object key');

console.log(map.get('key1')); // 'value1'

详细解答

1. Set 的详细用法

const set = new Set();

// 添加元素
set.add(1);
set.add(2);
set.add(3);

// 检查元素是否存在
console.log(set.has(1)); // true
console.log(set.has(4)); // false

// 删除元素
set.delete(2);

// 获取大小
console.log(set.size); // 2

// 清空
set.clear();

// 遍历
const colors = new Set(['red', 'green', 'blue']);

// for...of
for (const color of colors) {
  console.log(color);
}

// forEach
colors.forEach((value, key) => {
  console.log(key, value); // key 和 value 相同
});

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

2. Map 的详细用法

const map = new Map();

// 设置键值对
map.set('name', 'Alice');
map.set('age', 25);

// 获取值
console.log(map.get('name')); // 'Alice'

// 检查键是否存在
console.log(map.has('name')); // true

// 删除键值对
map.delete('age');

// 获取大小
console.log(map.size);

// 清空
map.clear();

// 遍历
const userMap = new Map([
  ['name', 'Bob'],
  ['age', 30],
  ['city', 'Beijing']
]);

// for...of (返回 [key, value])
for (const [key, value] of userMap) {
  console.log(key, value);
}

// forEach
userMap.forEach((value, key) => {
  console.log(key, value);
});

// 获取所有键/值/条目
console.log([...userMap.keys()]);
console.log([...userMap.values()]);
console.log([...userMap.entries()]);

3. Set 和 Map 与普通对象、数组的对比

特性 Set Array Map Object
数据类型 值的集合 有序列表 键值对 键值对
重复性 不重复 允许重复 键唯一 键唯一(字符串/Symbol)
键类型 无(只有值) 索引(数字) 任意类型 字符串/Symbol
顺序性 按插入顺序 按索引顺序 按插入顺序 不保证顺序
查找效率 O(1) O(n) O(1) O(1)
适用场景 去重、集合运算 有序数据 复杂键的映射 简单数据结构

深入理解

1. Set 的去重原理

// Set 使用 SameValueZero 算法判断相等
const set = new Set();

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

// 特殊值
set.add(NaN);
set.add(NaN); // 不会添加,Set 认为 NaN 等于 NaN

set.add(+0);
set.add(-0); // 不会添加,+0 和 -0 相等

console.log(set); // Set {1, '1', NaN, 0}

// 对象的去重(引用比较)
const objSet = new Set();
objSet.add({ a: 1 });
objSet.add({ a: 1 }); // 会添加,因为是不同引用

const obj = { b: 2 };
objSet.add(obj);
objSet.add(obj); // 不会添加,同一引用

2. Map 的键比较

const map = new Map();

// 不同类型的键
map.set('1', 'string');
map.set(1, 'number');
map.set(true, 'boolean');

console.log(map.get('1'));   // 'string'
console.log(map.get(1));     // 'number'

// 对象作为键
const keyObj = { id: 1 };
map.set(keyObj, 'object key');
console.log(map.get(keyObj)); // 'object key'

// 只有同一引用才能获取
console.log(map.get({ id: 1 })); // undefined

3. Set 的集合运算

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 {1, 2, 3, 4, 5, 6}

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

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

// 子集判断
const isSubset = (setA, setB) => {
  return [...setA].every(x => setB.has(x));
};
console.log(isSubset(new Set([1, 2]), setA)); // true

最佳实践

1. Set 的应用场景

// 1. 数组去重
const numbers = [1, 2, 2, 3, 4, 4, 5];
const unique = [...new Set(numbers)];
console.log(unique); // [1, 2, 3, 4, 5]

// 2. 字符串去重
const str = 'hello world';
const uniqueChars = [...new Set(str)].join('');
console.log(uniqueChars); // 'helo wrd'

// 3. 检查重复
const hasDuplicates = (arr) => {
  return new Set(arr).size !== arr.length;
};

// 4. 记录已访问元素
const visited = new Set();
function processNode(node) {
  if (visited.has(node.id)) return;
  visited.add(node.id);
  // 处理节点...
}

// 5. 存储唯一标签
const tags = new Set();
tags.add('javascript');
tags.add('javascript'); // 忽略
tags.add('react');

2. Map 的应用场景

// 1. 缓存计算结果
const cache = new Map();
function fibonacci(n) {
  if (cache.has(n)) return cache.get(n);
  if (n <= 1) return n;
  const result = fibonacci(n - 1) + fibonacci(n - 2);
  cache.set(n, result);
  return result;
}

// 2. 存储元数据(对象作为键)
const metadata = new Map();
const user = { name: 'Alice' };
metadata.set(user, { created: Date.now(), visits: 0 });

// 3. 计数器
const counter = new Map();
const words = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
words.forEach(word => {
  counter.set(word, (counter.get(word) || 0) + 1);
});
console.log(counter.get('apple')); // 3

// 4. 双向映射
const nameToId = new Map();
const idToName = new Map();

function addMapping(id, name) {
  nameToId.set(name, id);
  idToName.set(id, name);
}

// 5. 配置对象(支持任意键)
const config = new Map([
  ['apiUrl', 'https://api.example.com'],
  [Symbol('timeout'), 5000],
  [{ env: 'prod' }, 'production']
]);

3. WeakSet 和 WeakMap 简介

// WeakSet:只能存储对象,对象被回收时自动移除
const weakSet = new WeakSet();
let obj = { data: 'sensitive' };
weakSet.add(obj);

obj = null; // 对象被垃圾回收,weakSet 中自动移除

// WeakMap:键必须是对象,对象被回收时键值对自动移除
const weakMap = new WeakMap();
let key = { id: 1 };
weakMap.set(key, 'value');

key = null; // 键被垃圾回收,weakMap 中自动移除

// 适用场景:DOM 节点关联数据
const elementData = new WeakMap();
const button = document.querySelector('#btn');
elementData.set(button, { clickCount: 0 });

// 当 button 从 DOM 移除后,关联数据会被自动清理

面试要点

  1. Set 特点:无序、唯一、值不重复
  2. Map 特点:键值对、键可以是任意类型、按插入顺序排列
  3. 去重原理:使用 SameValueZero 算法,NaN 等于 NaN,+0 等于 -0
  4. 与 Object 的区别:Map 的键可以是任意类型,Object 的键只能是字符串或 Symbol
  5. WeakSet/WeakMap:弱引用,不阻止垃圾回收,适合 DOM 节点关联数据

常见面试题

// 面试题 1:如何实现数组去重?
const arr = [1, 2, 2, 3, 3, 3, 4];
// 方法1:Set
const unique1 = [...new Set(arr)];
// 方法2:filter
const unique2 = arr.filter((item, index) => arr.indexOf(item) === index);

// 面试题 2:Map 和 Object 的区别?
// - Map 的键可以是任意类型,Object 只能是字符串/Symbol
// - Map 保留插入顺序,Object 不保证
// - Map 有 size 属性,Object 需要手动计算
// - Map 更适合频繁增删的场景

// 面试题 3:如何实现两个数组的交集?
const intersection = (arr1, arr2) => {
  const set2 = new Set(arr2);
  return [...new Set(arr1)].filter(x => set2.has(x));
};

// 面试题 4:为什么 Map 比 Object 更适合做哈希表?
// 1. Map 的键类型更灵活
// 2. Map 的迭代顺序确定
// 3. Map 的性能在频繁增删时更好
// 4. Map 可以直接获取大小

// 面试题 5:WeakMap 的应用场景?
// 1. 私有属性实现
const privateData = new WeakMap();
class Person {
  constructor(name) {
    privateData.set(this, { name });
  }
  getName() {
    return privateData.get(this).name;
  }
}

// 2. DOM 节点数据缓存
const domCache = new WeakMap();
function getData(element) {
  if (!domCache.has(element)) {
    domCache.set(element, { clicks: 0 });
  }
  return domCache.get(element);
}