Set和Map的解析与应用场景
Set 和 Map 是 ES6 引入的两种强大的数据结构,用于解决数组和普通对象在处理特定场景时的局限性。它们提供了更高效的操作和更清晰的语义。下面详细解析它们的特点、区别和应用场景:
一、Set - 唯一值集合
核心概念: 存储唯一值(任何类型:原始值或对象引用)的集合。值在 Set 中出现且仅出现一次。
主要特性和 API
- 唯一性: 自动去重。添加重复值会被忽略(无错误)。
- 键即值: Set 没有传统意义上的键。它存储的就是一个个独立的值。
- 有序性: 元素按插入顺序迭代(for…of, forEach)。
- 判断存在性高效: has(value) 方法在平均情况下时间复杂度接近 O(1),远快于数组的 includes 或 indexOf (O(n))。
- 常用 API:
- new Set([iterable]): 构造函数,可选传入一个可迭代对象(如数组)初始化。
- set.add(value): 添加值,返回 Set 本身(可链式调用)。
- set.delete(value): 删除值,成功返回 true,否则 false。
- set.has(value): 判断值是否存在,返回布尔值。
- set.clear(): 清空所有元素。
- set.size: 属性,获取元素个数。
- set.keys() / set.values(): 都返回一个包含所有值的迭代器(因为 Set 中值就是键)。
- set.entries(): 返回一个迭代器,每个元素是 [value, value](为了与 Map API 兼容)。
- set.forEach(callbackFn[, thisArg]): 遍历每个值。
应用场景
- 数组去重: 最经典、最高效的用法。
const arr = [1, 2, 2, 3, 4, 4, 4];
const uniqueArray = [...new Set(arr)]; // [1, 2, 3, 4]
- 成员存在性快速检查: 当你需要频繁检查一个值是否在一个大集合中存在时。
const validUsers = new Set(['alice', 'bob', 'charlie']);
function isValidUser(username) {return validUsers.has(username); // 极快!
}
- 存储唯一操作或状态: 避免重复执行相同的操作或记录唯一事件。
const processedItems = new Set();
function processItem(item) {if (processedItems.has(item.id)) return; // 跳过已处理的// ... 处理逻辑 ...processedItems.add(item.id);
}
- 数学集合运算基础: 可以手动实现交集、并集、差集等(虽然 ES6 没有内置这些方法)。
const setA = new Set([1, 2, 3]);
const setB = new Set([2, 3, 4]);
// 交集
const intersection = new Set([...setA].filter(x => setB.has(x))); // Set(2) {2, 3}
// 并集
const union = new Set([...setA, ...setB]); // Set(4) {1, 2, 3, 4}
// 差集 (A - B)
const difference = new Set([...setA].filter(x => !setB.has(x))); // Set(1) {1}
二、Map - 键值对集合
核心概念: 存储键值对的集合。键可以是任何类型(对象、函数、原始值),而不仅仅是字符串或 Symbol(这是与普通对象 Object 的关键区别)。
主要特性和 API
- 键的灵活性: 键可以是任意值(对象引用、函数、NaN 等),解决了普通对象键只能是字符串或 Symbol 的限制。
- 键的唯一性: 基于“SameValueZero”算法判断键是否相等(类似于 ===,但认为 NaN === NaN)。键也是唯一的。
- 有序性: 键值对按键的插入顺序迭代(for…of, forEach)。
- 直接获取大小: size 属性直接获取键值对数量,无需像对象那样手动计算 Object.keys(obj).length。
- 纯数据存储: 没有原型链继承的属性(如 toString, hasOwnProperty),避免了与自定义属性名冲突。
- 常用 API:
- new Map([iterable]): 构造函数,可选传入一个可迭代对象(如 [[key1, value1], [key2, value2]])初始化。
- map.set(key, value): 设置键 key 对应的值为 value,返回 Map 本身(可链式调用)。如果 key 已存在,则更新其值。
- map.get(key): 读取键 key 对应的值,不存在则返回 undefined。
- map.delete(key): 删除指定键及其关联的值,成功返回 true,否则 false。
- map.has(key): 判断指定键是否存在,返回布尔值。
- map.clear(): 清空所有键值对。
- map.size: 属性,获取键值对数量。
- map.keys(): 返回一个包含所有键的迭代器。
- map.values(): 返回一个包含所有值的迭代器。
- map.entries(): 返回一个包含所有 [key, value] 对的迭代器(for…of 默认使用这个)。
- map.forEach(callbackFn[, thisArg]): 遍历每个键值对。回调函数参数为 (value, key, map)。
应用场景
- 关联任意类型键与数据: 当键不是字符串或 Symbol 时(最常见的是使用对象作为键)。
const userMetadata = new Map();
const user1 = { id: 1 };
const user2 = { id: 2 };
userMetadata.set(user1, { role: 'admin', lastLogin: new Date() });
userMetadata.set(user2, { role: 'user', lastLogin: new Date() });
console.log(userMetadata.get(user1).role); // 'admin'
- 缓存(Memoization): 存储函数计算结果,避免重复计算。
const cache = new Map();
function expensiveCalculation(input) {if (cache.has(input)) {return cache.get(input);}const result = ...; // 耗时计算cache.set(input, result);return result;
}
- 需要维护插入顺序的字典: 普通对象不保证属性顺序(虽然现代引擎通常按创建顺序,但整数属性会特殊排序)。Map 严格保证键的插入顺序。
const orderedMap = new Map();
orderedMap.set('z', 26);
orderedMap.set('a', 1);
orderedMap.set('m', 13);
for (const [key, value] of orderedMap) {console.log(key); // 输出顺序: 'z', 'a', 'm'
}
- 频率计数: 计数任意类型元素的出现次数。
const words = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const frequency = new Map();
for (const word of words) {frequency.set(word, (frequency.get(word) || 0) + 1);
}
console.log(frequency.get('apple')); // 3
- 替代需要频繁增删键或检查键存在的普通对象: Map 的 set/get/has/delete 操作通常性能更优,且 size 属性方便。
三、Set vs Map vs Object 关键区别总结
特性 | Set | Map | Object (普通对象) |
---|---|---|---|
存储内容 | 唯一的值 | 键值对 (key => value) | 键值对 (string/Symbol => value) |
键的类型 | 值本身 (无显式键) | 任意类型 | 只能是 String 或 Symbol |
键的唯一性判断 | SameValueZero (值相等) | SameValueZero (键相等) | 字符串化后相等 |
元素/键值对顺序 | 插入顺序 (值) | 插入顺序 (键) | 不保证顺序 (整数属性特殊排序) |
获取元素数量 | .size 属性 | .size 属性 | Object.keys(obj).length |
默认可迭代性 | 是 (values()) | 是 (entries()) | 需手动获取键/值/条目 |
检查存在性 | .has(value) (高效) | .has(key) (高效) | key in obj / obj.hasOwnProperty(key) |
继承的属性 | 无 | 无 | 有 (来自 Object.prototype) |
删除元素 | .delete(value) | .delete(key) | delete obj.key (性能较差) |
字面量语法 | 无 | 无 | 有 ({ key: value }) |
序列化 (JSON) | 需手动转换 | 需手动转换 | 直接支持 (JSON.stringify) |
四、什么时候用 Set?什么时候用 Map?什么时候用 Object?
- 用 Set:
- 你需要存储一个唯一值的列表(自动去重)。
- 你需要高效地检查某个值是否存在于一个(可能很大的)集合中。
- 你需要执行集合操作(并集、交集、差集)。
- 用 Map:
- 你需要一个键值对集合。
- 你的键不是字符串或 Symbol(尤其是需要用对象作为键时)。
- 你需要严格维护键值对的插入顺序。
- 你需要频繁地添加/删除键值对,或需要高效地检查键是否存在。
- 你不需要继承自 Object.prototype 的属性,想要一个“纯净”的键值存储。
- 你需要一个缓存机制。
- 用 Object (普通对象):
- 你的键都是字符串或 Symbol。
- 你不需要维护严格的插入顺序(或者顺序要求符合对象默认的规则)。
- 你需要使用对象字面量语法快速创建。
- 你需要直接支持 JSON 序列化/反序列化。
- 你需要访问继承自 Object.prototype 的方法(虽然通常不推荐在数据存储对象上直接使用)。
- 你的结构相对简单且固定,增删操作不频繁。
总结:
Set 和 Map 是 ES6 提供的现代化、功能强大的集合类型。Set 专注于存储和管理唯一值,提供高效的成员检查。Map 则突破了普通对象键类型的限制,允许使用任意值作为键,并严格维护插入顺序,是存储键值对数据的更通用、更高效的选择。理解它们的特性和适用场景,能够让你在 JavaScript 开发中写出更清晰、更高效、更健壮的代码。在处理需要唯一值或需要对象作为键的场景时,优先考虑 Set 和 Map。