Map 与 Set:现代 JavaScript 的高效集合
在图书馆中,不同类型的资料有不同的管理方式:字典按字母顺序排列词条,期刊按日期归档,而会员卡系统则记录每个唯一的会员编号。传统的 JavaScript 对象和数组可以处理这些任务,但 ES6 引入的 Map 和 Set 提供了更专业、更高效的解决方案。Set 就像一个不允许重复的会员系统,而 Map 则像一本可以用任何东西作为索引的智能字典。它们不仅提供了更清晰的语义,还在性能和功能上有显著优势。
Set - 唯一值的集合
Set 是一个存储唯一值的集合,自动去除重复项。它就像一个自动防重的容器,无论你添加多少次相同的值,最终只会保留一个。
创建和基本操作
javascript
// 创建空 Set
let emptySet = new Set();
// 创建包含初始值的 Set
let numbers = new Set([1, 2, 3, 4, 5]);
console.log(numbers); // Set(5) { 1, 2, 3, 4, 5 }
// 自动去重
let withDuplicates = new Set([1, 2, 2, 3, 3, 3, 4]);
console.log(withDuplicates); // Set(4) { 1, 2, 3, 4 }
// 添加值
numbers.add(6);
console.log(numbers.size); // 6
// 添加重复值不会有效果
numbers.add(3);
console.log(numbers.size); // 6 (仍然是 6)
// 检查值是否存在
console.log(numbers.has(3)); // true
console.log(numbers.has(10)); // false
// 删除值
numbers.delete(3);
console.log(numbers.has(3)); // false
// 清空 Set
numbers.clear();
console.log(numbers.size); // 0Set 的核心方法简单明了:add() 添加值,has() 检查存在,delete() 删除值,clear() 清空集合,size 属性获取元素数量。
遍历 Set
Set 是可迭代的,可以用多种方式遍历:
javascript
let colors = new Set(["red", "green", "blue", "yellow"]);
// 使用 for...of
for (let color of colors) {
console.log(color);
}
// 使用 forEach
colors.forEach((color) => {
console.log(color);
});
// Set 的 forEach 回调接收三个参数:值、值(是的,第二个参数也是值)、Set 本身
colors.forEach((value, valueAgain, set) => {
console.log(`${value} === ${valueAgain}`); // true
});
// 转换为数组
let colorArray = [...colors];
console.log(colorArray); // ["red", "green", "blue", "yellow"]
// 使用 Array.from
let colorArray2 = Array.from(colors);实际应用:数组去重
Set 最常见的用途之一就是数组去重:
javascript
let products = [
"laptop",
"mouse",
"keyboard",
"mouse",
"laptop",
"monitor",
"keyboard",
"headphones",
];
// 使用 Set 去重
let uniqueProducts = [...new Set(products)];
console.log(uniqueProducts);
// ["laptop", "mouse", "keyboard", "monitor", "headphones"]
// 统计唯一值数量
function countUnique(arr) {
return new Set(arr).size;
}
console.log(countUnique(products)); // 5集合运算
Set 可以很方便地实现数学集合运算:
javascript
let setA = new Set([1, 2, 3, 4]);
let setB = new Set([3, 4, 5, 6]);
// 并集(Union)
let union = new Set([...setA, ...setB]);
console.log([...union]); // [1, 2, 3, 4, 5, 6]
// 交集(Intersection)
let intersection = new Set([...setA].filter((x) => setB.has(x)));
console.log([...intersection]); // [3, 4]
// 差集(Difference) - A 中有但 B 中没有的
let difference = new Set([...setA].filter((x) => !setB.has(x)));
console.log([...difference]); // [1, 2]
// 对称差集(Symmetric Difference) - 在 A 或 B 中,但不在两者交集中
let symmetricDiff = new Set([
...[...setA].filter((x) => !setB.has(x)),
...[...setB].filter((x) => !setA.has(x)),
]);
console.log([...symmetricDiff]); // [1, 2, 5, 6]实际应用:标签系统
javascript
class TagManager {
constructor() {
this.tags = new Set();
}
// 添加单个标签
addTag(tag) {
this.tags.add(tag.toLowerCase());
}
// 批量添加标签
addTags(...tags) {
tags.forEach(tag => this.tags.add(tag.toLowerCase());
}
// 删除标签
removeTag(tag) {
this.tags.delete(tag.toLowerCase());
}
// 检查标签是否存在
hasTag(tag) {
return this.tags.has(tag.toLowerCase());
}
// 获取所有标签
getAllTags() {
return Array.from(this.tags).sort();
}
// 清空标签
clear() {
this.tags.clear();
}
// 标签数量
count() {
return this.tags.size;
}
}
// 使用示例
let articleTags = new TagManager();
articleTags.addTags("javascript", "programming", "web");
articleTags.addTag("JAVASCRIPT"); // 自动去重(转小写后相同)
console.log(articleTags.getAllTags());
// ["javascript", "programming", "web"]
console.log(articleTags.hasTag("Web")); // true (不区分大小写)Map - 键值对集合
Map 是一个键值对集合,类似于对象,但有几个重要的区别:键可以是任何类型(不仅仅是字符串),保持插入顺序,并提供了更丰富的方法。
创建和基本操作
javascript
// 创建空 Map
let emptyMap = new Map();
// 从数组创建 Map
let userRoles = new Map([
["alice", "admin"],
["bob", "editor"],
["charlie", "viewer"],
]);
// 设置键值对
let config = new Map();
config.set("theme", "dark");
config.set("language", "en");
config.set("fontSize", 14);
// 链式调用
let settings = new Map()
.set("host", "localhost")
.set("port", 3000)
.set("timeout", 5000);
// 获取值
console.log(config.get("theme")); // "dark"
console.log(config.get("missing")); // undefined
// 检查键是否存在
console.log(config.has("theme")); // true
console.log(config.has("color")); // false
// 删除键值对
config.delete("fontSize");
console.log(config.has("fontSize")); // false
// 获取大小
console.log(userRoles.size); // 3
// 清空 Map
config.clear();
console.log(config.size); // 0任何类型都可以作为键
这是 Map 相比普通对象的最大优势:
javascript
let map = new Map();
// 对象作为键
let objKey = { id: 1 };
map.set(objKey, "Object as key");
console.log(map.get(objKey)); // "Object as key"
// 函数作为键
let funcKey = function () {};
map.set(funcKey, "Function as key");
// 数组作为键
let arrKey = [1, 2, 3];
map.set(arrKey, "Array as key");
// DOM 元素作为键(在浏览器中)
// let buttonKey = document.querySelector('button');
// map.set(buttonKey, 'Button element');
// Map 作为键
let mapKey = new Map();
map.set(mapKey, "Map as key");
console.log(map.size); // 4
// 注意:引用相同才能找到值
console.log(map.get({ id: 1 })); // undefined (不同的对象引用)
console.log(map.get(objKey)); // "Object as key" (相同引用)遍历 Map
javascript
let prices = new Map([
["laptop", 1299],
["mouse", 29],
["keyboard", 89],
["monitor", 399],
]);
// 使用 for...of 遍历键值对
for (let [product, price] of prices) {
console.log(`${product}: $${price}`);
}
// 使用 forEach
prices.forEach((price, product) => {
console.log(`${product}: $${price}`);
});
// 只遍历键
for (let product of prices.keys()) {
console.log(product);
}
// 只遍历值
for (let price of prices.values()) {
console.log(price);
}
// 获取所有条目
console.log([...prices.entries()]);
// [["laptop", 1299], ["mouse", 29], ...]Map vs Object
让我们对比 Map 和普通对象:
javascript
// 1. 键的类型
let obj = {};
let map = new Map();
// 对象:键会被转换为字符串
obj[1] = "one";
obj[true] = "yes";
console.log(Object.keys(obj)); // ["1", "true"] - 都变成字符串了
// Map:键保持原类型
map.set(1, "one");
map.set(true, "yes");
console.log([...map.keys()]); // [1, true] - 保持原类型
// 2. 性能
// Map 在频繁添加/删除键值对时性能更好
console.time("Object");
let testObj = {};
for (let i = 0; i < 10000; i++) {
testObj[`key${i}`] = i;
}
for (let i = 0; i < 10000; i++) {
delete testObj[`key${i}`];
}
console.timeEnd("Object");
console.time("Map");
let testMap = new Map();
for (let i = 0; i < 10000; i++) {
testMap.set(`key${i}`, i);
}
for (let i = 0; i < 10000; i++) {
testMap.delete(`key${i}`);
}
console.timeEnd("Map");
// 3. 顺序保证
// Map 保证遍历顺序就是插入顺序
let orderedMap = new Map();
orderedMap.set("z", 1);
orderedMap.set("a", 2);
orderedMap.set("m", 3);
console.log([...orderedMap.keys()]); // ["z", "a", "m"] - 插入顺序
// 4. 大小获取
console.log(map.size); // 直接属性
console.log(Object.keys(obj).length); // 需要计算实际应用:缓存系统
javascript
class Cache {
constructor(maxSize = 100) {
this.cache = new Map();
this.maxSize = maxSize;
}
// 获取缓存值
get(key) {
if (!this.cache.has(key)) {
return null;
}
// LRU: 将访问的项移到最后(最近使用)
let value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
// 设置缓存值
set(key, value) {
// 如果键已存在,先删除
if (this.cache.has(key)) {
this.cache.delete(key);
}
// 如果超过最大容量,删除最旧的项(第一个)
if (this.cache.size >= this.maxSize) {
let firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
// 检查是否有缓存
has(key) {
return this.cache.has(key);
}
// 清空缓存
clear() {
this.cache.clear();
}
// 获取缓存大小
size() {
return this.cache.size;
}
// 获取所有键
keys() {
return Array.from(this.cache.keys());
}
}
// 使用示例
let apiCache = new Cache(3); // 最多缓存 3 项
apiCache.set("/api/users", [{ id: 1, name: "Alice" }]);
apiCache.set("/api/products", [{ id: 101, name: "Laptop" }]);
apiCache.set("/api/orders", [{ id: 1001, status: "pending" }]);
console.log(apiCache.keys());
// ["/api/users", "/api/products", "/api/orders"]
// 添加第四项会移除最旧的
apiCache.set("/api/settings", { theme: "dark" });
console.log(apiCache.keys());
// ["/api/products", "/api/orders", "/api/settings"]
// "/api/users" 被移除了
// 访问某项会将其移到最后
apiCache.get("/api/products");
console.log(apiCache.keys());
// ["/api/orders", "/api/settings", "/api/products"]
// "/api/products" 被移到最后了对象与 Map 相互转换
javascript
// 对象转 Map
let obj = {
name: "Emma",
age: 24,
city: "London",
};
let mapFromObj = new Map(Object.entries(obj));
console.log(mapFromObj.get("name")); // "Emma"
// Map 转对象
let map = new Map([
["theme", "dark"],
["language", "en"],
["notifications", true],
]);
let objFromMap = Object.fromEntries(map);
console.log(objFromMap);
// { theme: "dark", language: "en", notifications: true }实战案例:用户权限管理系统
让我们综合运用 Map 和 Set 构建一个权限管理系统:
javascript
class PermissionSystem {
constructor() {
// 使用 Map 存储用户权限(用户 ID -> Set<权限>)
this.userPermissions = new Map();
// 使用 Map 存储角色权限(角色名 -> Set<权限>)
this.rolePermissions = new Map();
// 使用 Map 存储用户角色(用户 ID -> Set<角色>)
this.userRoles = new Map();
}
// 定义角色权限
defineRole(roleName, permissions) {
this.rolePermissions.set(roleName, new Set(permissions));
}
// 为用户分配角色
assignRole(userId, roleName) {
if (!this.userRoles.has(userId)) {
this.userRoles.set(userId, new Set());
}
this.userRoles.get(userId).add(roleName);
}
// 为用户授予直接权限
grantPermission(userId, permission) {
if (!this.userPermissions.has(userId)) {
this.userPermissions.set(userId, new Set());
}
this.userPermissions.get(userId).add(permission);
}
// 撤销用户的直接权限
revokePermission(userId, permission) {
if (this.userPermissions.has(userId)) {
this.userPermissions.get(userId).delete(permission);
}
}
// 获取用户所有权限(角色权限 + 直接权限)
getUserPermissions(userId) {
let permissions = new Set();
// 添加直接权限
if (this.userPermissions.has(userId)) {
for (let perm of this.userPermissions.get(userId)) {
permissions.add(perm);
}
}
// 添加角色权限
if (this.userRoles.has(userId)) {
for (let role of this.userRoles.get(userId)) {
if (this.rolePermissions.has(role)) {
for (let perm of this.rolePermissions.get(role)) {
permissions.add(perm);
}
}
}
}
return permissions;
}
// 检查用户是否有某个权限
hasPermission(userId, permission) {
let permissions = this.getUserPermissions(userId);
return permissions.has(permission);
}
// 检查用户是否有所有指定权限
hasAllPermissions(userId, ...requiredPermissions) {
let permissions = this.getUserPermissions(userId);
return requiredPermissions.every((perm) => permissions.has(perm));
}
// 检查用户是否有任一指定权限
hasAnyPermission(userId, ...requiredPermissions) {
let permissions = this.getUserPermissions(userId);
return requiredPermissions.some((perm) => permissions.has(perm));
}
// 获取统计信息
getStats() {
return {
totalUsers: this.userRoles.size,
totalRoles: this.rolePermissions.size,
rolesInfo: Array.from(this.rolePermissions.entries()).map(
([role, perms]) => ({
role,
permissionCount: perms.size,
permissions: Array.from(perms),
})
),
};
}
}
// 使用示例
let permSystem = new PermissionSystem();
// 定义角色
permSystem.defineRole("admin", [
"read",
"write",
"delete",
"manage_users",
"manage_settings",
]);
permSystem.defineRole("editor", ["read", "write", "edit"]);
permSystem.defineRole("viewer", ["read"]);
// 为用户分配角色
permSystem.assignRole("user_001", "admin");
permSystem.assignRole("user_002", "editor");
permSystem.assignRole("user_003", "viewer");
// user_002 额外授予一个直接权限
permSystem.grantPermission("user_002", "manage_comments");
// 检查权限
console.log(permSystem.hasPermission("user_001", "delete")); // true
console.log(permSystem.hasPermission("user_002", "delete")); // false
console.log(permSystem.hasPermission("user_002", "manage_comments")); // true
// 获取用户所有权限
console.log([...permSystem.getUserPermissions("user_002")]);
// ["read", "write", "edit", "manage_comments"]
// 检查多个权限
console.log(
permSystem.hasAllPermissions("user_001", "read", "write", "delete")
);
// true
console.log(permSystem.hasAnyPermission("user_003", "write", "delete"));
// false (viewer 只有 read 权限)
// 获取统计
console.log(permSystem.getStats());
// {
// totalUsers: 3,
// totalRoles: 3,
// rolesInfo: [...]
// }性能对比
理解 Map/Set 与传统方式的性能差异很重要:
javascript
let size = 10000;
// Set vs Array(去重)
console.time("Array unique");
let arr = [];
for (let i = 0; i < size; i++) {
if (!arr.includes(i % 100)) {
arr.push(i % 100);
}
}
console.timeEnd("Array unique");
console.time("Set unique");
let set = new Set();
for (let i = 0; i < size; i++) {
set.add(i % 100);
}
console.timeEnd("Set unique");
// Set 快得多!
// Map vs Object(查找)
console.time("Object lookup");
let obj = {};
for (let i = 0; i < size; i++) {
obj[`key${i}`] = i;
}
for (let i = 0; i < size; i++) {
let value = obj[`key${i}`];
}
console.timeEnd("Object lookup");
console.time("Map lookup");
let map = new Map();
for (let i = 0; i < size; i++) {
map.set(`key${i}`, i);
}
for (let i = 0; i < size; i++) {
let value = map.get(`key${i}`);
}
console.timeEnd("Map lookup");
// 性能相近,但 Map 功能更强选择指南
使用 Set 当:
- 需要存储唯一值
- 需要快速检查某个值是否存在
- 需要去重
- 需要集合运算(并集、交集等)
使用 Map 当:
- 需要键值对存储,且键不仅仅是字符串
- 需要频繁添加/删除键值对
- 需要保持插入顺序
- 需要高性能的键查找
使用 Object 当:
- 需要 JSON 序列化(Map/Set 不能直接 JSON.stringify)
- 键确定是字符串或符号
- 需要使用原型继承
- 数据结构相对固定
使用 Array 当:
- 需要索引访问
- 需要数组专有方法(map, filter, reduce 等)
- 允许重复值
- 需要排序
常见陷阱
1. Set 的相等性判断
javascript
let set = new Set();
set.add({ id: 1 });
set.add({ id: 1 }); // 不同对象引用,不会去重
console.log(set.size); // 2 (有两个对象)
// 基本类型正常去重
let nums = new Set();
nums.add(1);
nums.add(1);
console.log(nums.size); // 12. Map 键的比较
javascript
let map = new Map();
// NaN 作为键
map.set(NaN, "value");
console.log(map.get(NaN)); // "value" (Map 认为 NaN === NaN)
// +0 和 -0 被认为相同
map.set(+0, "plus zero");
map.set(-0, "minus zero");
console.log(map.size); // 1 (两个键被认为相同)3. 转换时的限制
javascript
let map = new Map();
map.set({ id: 1 }, "object key");
// JSON.stringify 不能直接序列化 Map
console.log(JSON.stringify(map)); // "{}" - 丢失数据!
// 正确方式:先转为数组
let serialized = JSON.stringify([...map]);
let deserialized = new Map(JSON.parse(serialized));总结
Map 和 Set 是 ES6 引入的强大数据结构:
- Set - 唯一值集合,自动去重,高效的存在性检查
- Map - 键值对集合,任何类型的键,保持插入顺序,性能优异
它们相比传统的对象和数组提供了:
- 更清晰的语义(意图明确)
- 更好的性能(特别是大数据量时)
- 更丰富的功能(如 size 属性、专门的遍历方法)
- 更灵活的键类型(Map)
在现代 JavaScript 开发中,合理选择和使用 Map、Set、Object、Array 能显著提升代码质量和性能。当需要唯一性时用 Set,需要灵活键值对时用 Map,它们会让你的代码更简洁、更高效。