Map 与 Set
ES6 引入了 Map 和 Set 两种新的数据结构,弥补了传统对象和数组在某些场景下的不足。
概述
什么是 Map?
Map 是一种键值对集合,类似于对象,但有以下关键区别:
- 键的类型:Map 的键可以是任意类型(对象、函数、NaN 等),而对象的键只能是字符串或 Symbol
- 有序性:Map 会记住键值对的插入顺序
- 可迭代性:Map 直接支持迭代,无需先转换
什么是 Set?
Set 是一种值的集合,类似于数组,但有以下关键区别:
- 值的唯一性:Set 中的值是唯一的,不会出现重复值
- 无索引访问:Set 不支持通过索引访问值
- 有序性:Set 会记住值的插入顺序
核心特性对比
| 特性 | Map | Set | Object | Array |
|---|---|---|---|---|
| 键/值类型 | 任意类型 | 任意类型 | 字符串/Symbol | 数字索引 |
| 是否有序 | ✓ | ✓ | 部分 | ✓ |
| 可迭代 | ✓ | ✓ | 需转换 | ✓ |
| 去重功能 | ✗ | ✓ | ✗ | 需手动实现 |
| 性能(频繁增删) | 优 | 优 | 差 | 差 |
Map 基础
基本用法
javascript
const map = new Map()
// 添加键值对
map.set('name', 'John')
map.set('age', 30)
map.set(1, 'one')
map.set(true, 'yes')
// 获取值
console.log(map.get('name')) // 'John'
console.log(map.get(1)) // 'one'
// 检查键
console.log(map.has('name')) // true
console.log(map.has('email')) // false
// 删除键值对
map.delete('age')
console.log(map.has('age')) // false
// 清空 Map
map.clear()
console.log(map.size) // 0键的类型
javascript
const map = new Map()
// 键可以是任意类型
const obj = { name: 'John' }
const arr = [1, 2, 3]
const func = () => 'hello'
map.set(obj, 'object')
map.set(arr, 'array')
map.set(func, 'function')
console.log(map.get(obj)) // 'object'
console.log(map.get(arr)) // 'array'
console.log(map.get(func)) // 'function'
// NaN 作为键
map.set(NaN, 'not a number')
console.log(map.get(NaN)) // 'not a number'
console.log(map.has(NaN)) // true初始化 Map
javascript
// 从数组创建
const map = new Map([
['name', 'John'],
['age', 30]
])
console.log(map.size) // 2
// 从对象创建
const obj = { name: 'John', age: 30 }
const mapFromObj = new Map(Object.entries(obj))
console.log(mapFromObj.get('name')) // 'John'遍历 Map
javascript
const map = new Map([
['name', 'John'],
['age', 30],
['city', 'New York']
])
// forEach
map.forEach((value, key) => {
console.log(`${key}: ${value}`)
})
// for...of
for (const [key, value] of map) {
console.log(`${key}: ${value}`)
}
// keys()
for (const key of map.keys()) {
console.log(key)
}
// values()
for (const value of map.values()) {
console.log(value)
}
// entries()
for (const entry of map.entries()) {
console.log(entry)
}Map 与 Object 的区别
| 特性 | Map | Object |
|---|---|---|
| 键的类型 | 任意类型 | 字符串或 Symbol |
| 键的顺序 | 有序 | 无序(ES6 后部分有序) |
| size 属性 | 有 | 无 |
| 遍历 | 可直接遍历 | 需要转换 |
| 性能 | 频繁增删更优 | 稍差 |
Map 转换
javascript
const map = new Map([
['name', 'John'],
['age', 30]
])
// Map 转数组
const arr = [...map]
console.log(arr) // [['name', 'John'], ['age', 30]]
// Map 转对象
const obj = Object.fromEntries(map)
console.log(obj) // { name: 'John', age: 30 }
// 对象转 Map
const obj2 = { name: 'Jane', age: 25 }
const mapFromObj = new Map(Object.entries(obj2))
console.log(mapFromObj.get('name')) // 'Jane'Map API 详解
构造函数
javascript
// 创建空 Map
const map1 = new Map()
// 从可迭代对象创建(数组、Map、Set 等)
const map2 = new Map([
['key1', 'value1'],
['key2', 'value2']
])
// 从另一个 Map 创建
const map3 = new Map(map2)
// 从对象创建
const obj = { a: 1, b: 2 }
const map4 = new Map(Object.entries(obj))参数说明:
iterable(可选):可迭代对象,其元素为键值对数组[key, value]
实例方法
set(key, value)
添加或更新键值对。
javascript
const map = new Map()
// 添加新键值对
map.set('name', 'John')
// 更新已有键
map.set('name', 'Jane')
// 链式调用
map.set('a', 1).set('b', 2).set('c', 3)
// 返回 Map 对象本身
console.log(map.set('d', 4) === map) // true参数:
key:要设置的键,可以是任意类型value:要设置的值,可以是任意类型
返回值:Map 对象本身
注意事项:
- 如果键已存在,会覆盖原有值
- 支持链式调用
get(key)
获取键对应的值。
javascript
const map = new Map([['name', 'John'], ['age', 30]])
console.log(map.get('name')) // 'John'
console.log(map.get('age')) // 30
console.log(map.get('email')) // undefined参数:
key:要查找的键
返回值:键对应的值,如果键不存在则返回 undefined
has(key)
检查是否存在指定键。
javascript
const map = new Map([['name', 'John']])
console.log(map.has('name')) // true
console.log(map.has('age')) // false
// NaN 的特殊情况
map.set(NaN, 'not a number')
console.log(map.has(NaN)) // true参数:
key:要检查的键
返回值:布尔值,表示键是否存在
注意事项:
- Map 使用
SameValueZero算法比较键 NaN等于NaN
delete(key)
删除指定键值对。
javascript
const map = new Map([['name', 'John'], ['age', 30]])
console.log(map.delete('name')) // true
console.log(map.delete('email')) // false(键不存在)
console.log(map.size) // 1参数:
key:要删除的键
返回值:布尔值,表示是否删除成功
clear()
清空所有键值对。
javascript
const map = new Map([['name', 'John'], ['age', 30]])
map.clear()
console.log(map.size) // 0返回值:undefined
迭代方法
keys()
返回所有键的迭代器。
javascript
const map = new Map([['a', 1], ['b', 2], ['c', 3]])
for (const key of map.keys()) {
console.log(key) // 'a', 'b', 'c'
}
// 转换为数组
const keys = [...map.keys()]
console.log(keys) // ['a', 'b', 'c']values()
返回所有值的迭代器。
javascript
const map = new Map([['a', 1], ['b', 2], ['c', 3]])
for (const value of map.values()) {
console.log(value) // 1, 2, 3
}
// 转换为数组
const values = [...map.values()]
console.log(values) // [1, 2, 3]entries()
返回所有键值对的迭代器。
javascript
const map = new Map([['a', 1], ['b', 2]])
for (const [key, value] of map.entries()) {
console.log(`${key}: ${value}`) // 'a: 1', 'b: 2'
}
// 等价于直接遍历 Map
for (const [key, value] of map) {
console.log(`${key}: ${value}`) // 'a: 1', 'b: 2'
}forEach(callback, thisArg)
遍历所有键值对。
javascript
const map = new Map([['a', 1], ['b', 2]])
map.forEach((value, key, map) => {
console.log(`${key}: ${value}`)
})
// 使用 thisArg
const obj = {
multiplier: 10,
print(key, value) {
console.log(`${key}: ${value * this.multiplier}`)
}
}
map.forEach(function(value, key) {
this.print(key, value)
}, obj)参数:
callback(value, key, map):回调函数value:当前值key:当前键map:正在遍历的 Map
thisArg(可选):回调函数中this的值
实例属性
size
返回键值对的数量。
javascript
const map = new Map([['a', 1], ['b', 2]])
console.log(map.size) // 2
map.set('c', 3)
console.log(map.size) // 3
map.delete('a')
console.log(map.size) // 2返回值:数字,表示键值对数量
Set 基础
基本用法
javascript
const set = new Set()
// 添加值
set.add(1)
set.add(2)
set.add(2) // 重复值会被忽略
set.add('2') // 类型不同,不会忽略
console.log(set.size) // 3
// 检查值
console.log(set.has(1)) // true
console.log(set.has(3)) // false
// 删除值
set.delete(1)
console.log(set.has(1)) // false
// 清空 Set
set.clear()
console.log(set.size) // 0数组去重
javascript
const arr = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
// 使用 Set 去重
const unique = [...new Set(arr)]
console.log(unique) // [1, 2, 3, 4]
// 或者使用 Array.from
const unique2 = Array.from(new Set(arr))
console.log(unique2) // [1, 2, 3, 4]初始化 Set
javascript
// 从数组创建
const set = new Set([1, 2, 3, 3, 3])
console.log(set.size) // 3
// 从字符串创建
const setFromStr = new Set('hello')
console.log(setFromStr) // Set { 'h', 'e', 'l', 'o' }遍历 Set
javascript
const set = new Set([1, 2, 3])
// forEach
set.forEach(value => {
console.log(value)
})
// for...of
for (const value of set) {
console.log(value)
}
// keys() 和 values() 相同
for (const value of set.values()) {
console.log(value)
}
// entries()
for (const entry of set.entries()) {
console.log(entry) // [1, 1], [2, 2], [3, 3]
}Set 操作
javascript
const set1 = new Set([1, 2, 3])
const set2 = new Set([2, 3, 4])
// 并集
const union = new Set([...set1, ...set2])
console.log([...union]) // [1, 2, 3, 4]
// 交集
const intersection = new Set([...set1].filter(x => set2.has(x)))
console.log([...intersection]) // [2, 3]
// 差集
const difference = new Set([...set1].filter(x => !set2.has(x)))
console.log([...difference]) // [1]
// 子集
const isSubset = [...set1].every(x => set2.has(x))
console.log(isSubset) // falseES2025 原生 Set 方法
ES2025 为 Set.prototype 新增了 7 个集合操作方法,无需再手动实现:
javascript
const setA = new Set([1, 2, 3, 4])
const setB = new Set([3, 4, 5, 6])
// 并集
setA.union(setB) // Set {1, 2, 3, 4, 5, 6}
// 交集
setA.intersection(setB) // Set {3, 4}
// 差集(A 中有但 B 中没有)
setA.difference(setB) // Set {1, 2}
// 对称差集(并集减交集)
setA.symmetricDifference(setB) // Set {1, 2, 5, 6}
// 子集判断
new Set([1, 2]).isSubsetOf(setA) // true
// 超集判断
setA.isSupersetOf(new Set([1, 2])) // true
// 是否不相交
new Set([1, 2]).isDisjointOf(new Set([3, 4])) // true注意:ES2025 原生方法返回新的
Set实例,不会修改原始 Set。旧的手动实现方式仍可在不支持 ES2025 的环境中使用。
Set 转换
javascript
const set = new Set([1, 2, 3])
// Set 转数组
const arr = [...set]
console.log(arr) // [1, 2, 3]
// Set 转数组(方法二)
const arr2 = Array.from(set)
console.log(arr2) // [1, 2, 3]Set API 详解
构造函数
javascript
// 创建空 Set
const set1 = new Set()
// 从数组创建
const set2 = new Set([1, 2, 3, 3, 3])
console.log(set2) // Set { 1, 2, 3 }
// 从字符串创建
const set3 = new Set('hello')
console.log(set3) // Set { 'h', 'e', 'l', 'o' }
// 从另一个 Set 创建
const set4 = new Set(set2)
// 从 arguments 创建
function example() {
const argsSet = new Set(arguments)
console.log(argsSet)
}
example(1, 2, 3) // Set { 1, 2, 3 }参数说明:
iterable(可选):可迭代对象,重复值会被忽略
实例方法
add(value)
添加值到 Set。
javascript
const set = new Set()
// 添加值
set.add(1)
set.add(2)
// 添加重复值会被忽略
set.add(2)
console.log(set.size) // 2
// 链式调用
set.add(3).add(4).add(5)
// 返回 Set 对象本身
console.log(set.add(6) === set) // true
// 添加不同类型的值
set.add('1') // 字符串 '1' 和数字 1 不同
set.add(true) // 布尔值
set.add({}) // 对象
set.add([1, 2]) // 数组
set.add(null)
set.add(undefined)
set.add(NaN) // NaN 等于 NaN参数:
value:要添加的值,可以是任意类型
返回值:Set 对象本身
注意事项:
- 重复值会被忽略
- 支持链式调用
- 使用
SameValueZero算法比较值
has(value)
检查值是否存在。
javascript
const set = new Set([1, 2, 3])
console.log(set.has(1)) // true
console.log(set.has(4)) // false
// NaN 的特殊情况
set.add(NaN)
console.log(set.has(NaN)) // true
// 对象引用
const obj = { name: 'John' }
set.add(obj)
console.log(set.has(obj)) // true
console.log(set.has({ name: 'John' })) // false(不同引用)参数:
value:要检查的值
返回值:布尔值,表示值是否存在
delete(value)
删除指定值。
javascript
const set = new Set([1, 2, 3])
console.log(set.delete(2)) // true
console.log(set.delete(4)) // false(值不存在)
console.log(set.size) // 2参数:
value:要删除的值
返回值:布尔值,表示是否删除成功
clear()
清空所有值。
javascript
const set = new Set([1, 2, 3])
set.clear()
console.log(set.size) // 0返回值:undefined
迭代方法
keys() 和 values()
Set 中 keys() 和 values() 行为相同,都返回值的迭代器。
javascript
const set = new Set([1, 2, 3])
// keys()
for (const key of set.keys()) {
console.log(key) // 1, 2, 3
}
// values()
for (const value of set.values()) {
console.log(value) // 1, 2, 3
}
// 两者等价
console.log([...set.keys()]) // [1, 2, 3]
console.log([...set.values()]) // [1, 2, 3]entries()
返回 [value, value] 形式的迭代器。
javascript
const set = new Set([1, 2, 3])
for (const entry of set.entries()) {
console.log(entry) // [1, 1], [2, 2], [3, 3]
}
// 这是为了与 Map API 保持一致forEach(callback, thisArg)
遍历所有值。
javascript
const set = new Set([1, 2, 3])
// forEach 遍历
set.forEach((value, key, set) => {
console.log(value) // 1, 2, 3
// 注意:Set 中 key 和 value 相同
})
// 使用 thisArg
const obj = {
multiplier: 10,
print(value) {
console.log(value * this.multiplier)
}
}
set.forEach(function(value) {
this.print(value)
}, obj) // 10, 20, 30参数:
callback(value, key, set):回调函数value:当前值key:当前值(Set 中 key 和 value 相同)set:正在遍历的 Set
thisArg(可选):回调函数中this的值
实例属性
size
返回值的数量。
javascript
const set = new Set([1, 2, 3])
console.log(set.size) // 3
set.add(4)
console.log(set.size) // 4
set.delete(1)
console.log(set.size) // 3返回值:数字,表示值的数量
使用场景与最佳实践
Map 使用场景
1. 需要非字符串键
javascript
// ❌ 使用 Object:键会被转换为字符串
const obj = {}
const key1 = { id: 1 }
const key2 = [1, 2, 3]
obj[key1] = 'value1' // 键变成 "[object Object]"
obj[key2] = 'value2' // 键变成 "1,2,3"
console.log(obj['[object Object]']) // 'value2'(被覆盖了)
// ✅ 使用 Map:保持原始类型
const map = new Map()
const user = { id: 1, name: 'John' }
const role = ['admin', 'user']
map.set(user, { permissions: ['read', 'write'] })
map.set(role, 'roles')
console.log(map.get(user)) // { permissions: ['read', 'write'] }
console.log(map.get(role)) // 'roles'2. 需要频繁增删键值对
javascript
// 场景:缓存系统
class Cache {
constructor() {
this.data = new Map()
this.accessCount = new Map()
}
set(key, value) {
this.data.set(key, value)
this.accessCount.set(key, 0)
}
// ... 中间省略 ...
clear() {
this.data.clear()
this.accessCount.clear()
}
}3. 需要有序的键值对集合
javascript
// 场景:构建有序的配置系统
class ConfigManager {
constructor() {
this.configs = new Map()
}
// 按添加顺序设置配置
setConfig(key, value, priority = 0) {
this.configs.set(key, { value, priority })
}
// 按优先级获取配置
getSortedConfigs() {
return [...this.configs.entries()]
.sort((a, b) => b[1].priority - a[1].priority)
.map(([key, { value }]) => ({ key, value }))
}
}
const manager = new ConfigManager()
manager.setConfig('database', 'mysql', 2)
manager.setConfig('cache', 'redis', 3)
manager.setConfig('logging', 'winston', 1)
console.log(manager.getSortedConfigs())
// [{ key: 'cache', value: 'redis' }, ...]4. 需要精确的键值对数量
javascript
// 场景:限制缓存大小
class LRUCache {
constructor(maxSize = 100) {
this.cache = new Map()
this.maxSize = maxSize
}
get(key) {
if (!this.cache.has(key)) return undefined
// 移到最后(最近使用)
const value = this.cache.get(key)
// ... 中间省略 ...
}
get size() {
return this.cache.size
}
}Set 使用场景
1. 数组去重
javascript
// ❌ 传统方法
const arr = [1, 2, 2, 3, 3, 3]
const unique = arr.filter((item, index) => arr.indexOf(item) === index)
// ✅ 使用 Set
const unique = [...new Set(arr)]
// 对象数组去重(基于某个属性)
const users = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
{ id: 1, name: 'John' }
]
const uniqueUsers = [...new Map(users.map(user => [user.id, user])).values()]
console.log(uniqueUsers)2. 判断值是否存在(高性能)
javascript
// ❌ 使用数组
const arr = [1, 2, 3, 4, 5]
console.log(arr.includes(3)) // O(n)
// ✅ 使用 Set
const set = new Set([1, 2, 3, 4, 5])
console.log(set.has(3)) // O(1)
// 性能对比
const largeArray = Array.from({ length: 100000 }, (_, i) => i)
const largeSet = new Set(largeArray)
console.time('Array.includes')
for (let i = 0; i < 1000; i++) {
largeArray.includes(50000)
}
console.timeEnd('Array.includes') // ~50ms
console.time('Set.has')
for (let i = 0; i < 1000; i++) {
largeSet.has(50000)
}
console.timeEnd('Set.has') // ~0.1ms3. 集合运算
javascript
class SetOperations {
// 并集
static union(setA, setB) {
return new Set([...setA, ...setB])
}
// 交集
static intersection(setA, setB) {
return new Set([...setA].filter(x => setB.has(x)))
}
// 差集
// ... 中间省略 ...
console.log(SetOperations.union(setA, setB)) // Set {1, 2, 3, 4, 5}
console.log(SetOperations.intersection(setA, setB)) // Set {2, 3}
console.log(SetOperations.difference(setA, setB)) // Set {1, 4}
console.log(SetOperations.symmetricDifference(setA, setB)) // Set {1, 4, 5}
console.log(SetOperations.isSubset(new Set([2, 3]), setA)) // true4. 跟踪唯一项
javascript
// 场景:跟踪在线用户
class OnlineUsers {
constructor() {
this.users = new Set()
}
join(userId) {
this.users.add(userId)
console.log(`User ${userId} joined. Total: ${this.users.size}`)
}
leave(userId) {
// ... 中间省略 ...
const onlineUsers = new OnlineUsers()
onlineUsers.join(1) // User 1 joined. Total: 1
onlineUsers.join(2) // User 2 joined. Total: 2
onlineUsers.join(1) // User 1 joined. Total: 2(重复,不会增加)
onlineUsers.leave(1) // User 1 left. Total: 1最佳实践
1. 选择合适的数据结构
javascript
// ✅ 使用 Map 的场景
// - 需要非字符串键
// - 需要频繁增删
// - 需要保证插入顺序
// - 需要精确的 size
// ❌ 不推荐使用 Map 的场景
// - 键只需要字符串或 Symbol
// - 需要序列化为 JSON
// - 需要使用对象字面量语法
// ✅ 使用 Set 的场景
// - 需要去重
// - 需要频繁判断值是否存在
// - 需要集合运算
// ❌ 不推荐使用 Set 的场景
// - 需要通过索引访问
// - 需要存储重复值
// - 需要排序2. 避免内存泄漏
javascript
// ❌ 错误:使用普通对象作为缓存可能导致内存泄漏
const cache = {}
function addToCache(obj, value) {
cache[obj.id] = value // 对象引用被永久保存
}
// ✅ 正确:使用 WeakMap 避免内存泄漏
const cache = new WeakMap()
function addToCache(obj, value) {
cache.set(obj, value) // 当 obj 被垃圾回收时,缓存会自动清理
}
// ❌ 错误:使用 Set 存储对象引用可能导致内存泄漏
const listeners = new Set()
element.addListener(listener => {
listeners.add(listener) // 永远不会被清理
})
// ✅ 正确:使用 WeakSet 或手动清理
const listeners = new WeakSet()
// 或
element.removeListener(listener => {
listeners.delete(listener)
})3. 性能优化
javascript
// ✅ 批量操作优于多次单个操作
const map = new Map()
// ❌ 多次单个操作
data.forEach(item => map.set(item.id, item))
// ✅ 使用构造函数初始化
const map = new Map(data.map(item => [item.id, item]))
// ✅ 避免频繁的类型转换
const set = new Set([1, 2, 3])
// ❌ 频繁转换
for (let i = 0; i < 1000; i++) {
const arr = [...set]
console.log(arr.includes(2))
}
// ✅ 直接使用 Set 方法
for (let i = 0; i < 1000; i++) {
console.log(set.has(2))
}4. 正确处理对象作为键
javascript
// ❌ 错误:每次创建新对象
const map = new Map()
map.set({ id: 1 }, 'value1')
console.log(map.get({ id: 1 })) // undefined(不同引用)
// ✅ 正确:使用同一个引用
const key = { id: 1 }
map.set(key, 'value1')
console.log(map.get(key)) // 'value1'
// ✅ 使用唯一标识符
const userMap = new Map()
function getUserKey(user) {
return `user_${user.id}`
}
userMap.set(getUserKey({ id: 1 }), { name: 'John' })实际应用
使用 Map 缓存计算结果
javascript
const cache = new Map()
function expensiveCalculation(n) {
if (cache.has(n)) {
return cache.get(n)
}
// 模拟耗时计算
const result = n * n
cache.set(n, result)
return result
}
console.log(expensiveCalculation(5)) // 计算
console.log(expensiveCalculation(5)) // 从缓存读取使用 Set 跟踪唯一值
javascript
class UniqueTracker {
constructor() {
this.items = new Set()
}
add(item) {
if (this.items.has(item)) {
console.log(`${item} already exists`)
return false
}
this.items.add(item)
return true
// ... 中间省略 ...
}
const tracker = new UniqueTracker()
tracker.add('item1')
tracker.add('item2')
tracker.add('item1') // 'item1 already exists'使用 Map 存储对象关联数据
javascript
const userActions = new Map()
const user1 = { id: 1, name: 'John' }
const user2 = { id: 2, name: 'Jane' }
userActions.set(user1, ['login', 'view'])
userActions.set(user2, ['login', 'purchase'])
console.log(userActions.get(user1)) // ['login', 'view']
// 对象作为键,不会被转换为字符串进阶应用示例
1. 使用 Map 实现简单的路由系统
javascript
class Router {
constructor() {
this.routes = new Map()
this.middlewares = new Set()
}
// 添加路由
add(path, handler) {
this.routes.set(path, handler)
return this
}
// ... 中间省略 ...
.add('/contact', () => 'Contact Page')
.use((path) => console.log(`Request: ${path}`))
console.log(router.handle('/')) // 'Home Page'
console.log(router.handle('/about')) // 'About Page'
console.log(router.listRoutes()) // ['/', '/about', '/contact']2. 使用 Set 实现标签系统
javascript
class TagSystem {
constructor() {
this.tagMap = new Map() // itemId -> Set of tags
this.itemMap = new Map() // tag -> Set of itemIds
}
// 添加标签
addTag(itemId, tag) {
// 为项目添加标签
if (!this.tagMap.has(itemId)) {
this.tagMap.set(itemId, new Set())
}
// ... 中间省略 ...
tagSystem.addTag(2, 'javascript')
tagSystem.addTag(2, 'backend')
console.log(tagSystem.getTags(1)) // ['javascript', 'frontend']
console.log(tagSystem.getItems('javascript')) // [1, 2]
console.log(tagSystem.findByTags('javascript', 'frontend')) // [1]3. 使用 Map 实现依赖注入容器
javascript
class DIContainer {
constructor() {
this.registrations = new Map()
this.instances = new Map()
this.resolving = new Set() // 防止循环依赖
}
// 注册服务
register(name, factory, dependencies = []) {
this.registrations.set(name, { factory, dependencies })
}
// ... 中间省略 ...
return response.json()
}
}), ['httpClient'])
const userService = container.resolve('userService')
console.log(container.getAllServices()) // ['config', 'httpClient', 'userService']4. 使用 Set 和 Map 实现简单的权限系统
javascript
class PermissionSystem {
constructor() {
this.roles = new Map() // role -> Set of permissions
this.users = new Map() // userId -> Set of roles
this.resources = new Set() // all resources
}
// 添加资源
addResource(resource) {
this.resources.add(resource)
}
// ... 中间省略 ...
perm.assignRole('user2', 'editor')
perm.assignRole('user3', 'viewer')
console.log(perm.hasPermission('user1', 'delete')) // true
console.log(perm.hasPermission('user2', 'delete')) // false
console.log(perm.getUserPermissions('user1')) // ['read', 'write', 'delete', 'manage']5. 使用 Map 实现事件发射器
javascript
class EventEmitter {
constructor() {
this.events = new Map()
this.onceEvents = new Map()
}
// 订阅事件
on(event, callback) {
if (!this.events.has(event)) {
this.events.set(event, new Set())
}
this.events.get(event).add(callback)
// ... 中间省略 ...
emitter.emit('connect') // 'Connected!'
emitter.emit('connect') // 不会触发(一次性事件)
emitter.emit('message', 'Hello') // 'Received: Hello'
unsubscribe() // 取消订阅
emitter.emit('message', 'World') // 不会触发6. 使用 Map 实现简单的状态管理
javascript
class Store {
constructor(initialState = {}) {
this.state = new Map(Object.entries(initialState))
this.listeners = new Set()
this.middleware = new Set()
}
// 获取状态
getState() {
return Object.fromEntries(this.state)
}
// ... 中间省略 ...
console.log(`Listener: ${key} changed from`, oldValue, 'to', value)
})
store.set('count', 1) // 触发中间件和监听器
store.set('count', 2)
console.log(store.getState()) // { count: 2, user: null }性能对比
Map vs Object 性能对比
1. 添加操作
javascript
const iterations = 100000
// Map 添加
console.time('Map.set')
const map = new Map()
for (let i = 0; i < iterations; i++) {
map.set(`key${i}`, i)
}
console.timeEnd('Map.set') // ~5-10ms
// Object 添加
console.time('Object property')
const obj = {}
for (let i = 0; i < iterations; i++) {
obj[`key${i}`] = i
}
console.timeEnd('Object property') // ~5-15ms2. 查找操作
javascript
const map = new Map()
const obj = {}
// 初始化
for (let i = 0; i < iterations; i++) {
map.set(`key${i}`, i)
obj[`key${i}`] = i
}
// Map 查找
console.time('Map.get')
for (let i = 0; i < iterations; i++) {
map.get(`key${i}`)
}
console.timeEnd('Map.get') // ~2-5ms
// Object 查找
console.time('Object access')
for (let i = 0; i < iterations; i++) {
obj[`key${i}`]
}
console.timeEnd('Object access') // ~2-5ms3. 删除操作
javascript
const map = new Map()
const obj = {}
// 初始化
for (let i = 0; i < iterations; i++) {
map.set(`key${i}`, i)
obj[`key${i}`] = i
}
// Map 删除
console.time('Map.delete')
for (let i = 0; i < iterations; i++) {
map.delete(`key${i}`)
}
console.timeEnd('Map.delete') // ~3-8ms
// Object 删除
console.time('delete operator')
for (let i = 0; i < iterations; i++) {
delete obj[`key${i}`]
}
console.timeEnd('delete operator') // ~8-15ms
// Map 在删除操作上性能更优4. 频繁增删场景
javascript
// Map 频繁增删
console.time('Map frequent operations')
const map = new Map()
for (let i = 0; i < iterations; i++) {
map.set(i, i)
if (i % 2 === 0) {
map.delete(i)
}
}
console.timeEnd('Map frequent operations') // ~5-10ms
// Object 频繁增删
console.time('Object frequent operations')
const obj = {}
for (let i = 0; i < iterations; i++) {
obj[i] = i
if (i % 2 === 0) {
delete obj[i]
}
}
console.timeEnd('Object frequent operations') // ~15-25ms
// Map 在频繁增删场景下性能显著优于 ObjectSet vs Array 性能对比
1. 查找操作
javascript
const iterations = 100000
// 初始化
const set = new Set()
const arr = []
for (let i = 0; i < iterations; i++) {
set.add(i)
arr.push(i)
}
// Set 查找
console.time('Set.has')
for (let i = 0; i < 10000; i++) {
set.has(Math.floor(Math.random() * iterations))
}
console.timeEnd('Set.has') // ~0.5-1ms
// Array 查找
console.time('Array.includes')
for (let i = 0; i < 10000; i++) {
arr.includes(Math.floor(Math.random() * iterations))
}
console.timeEnd('Array.includes') // ~150-300ms
// Set.has() 性能远优于 Array.includes()
// Set: O(1) vs Array: O(n)2. 去重操作
javascript
const data = Array.from({ length: 10000 }, () =>
Math.floor(Math.random() * 5000)
)
// Set 去重
console.time('Set deduplication')
const unique1 = [...new Set(data)]
console.timeEnd('Set deduplication') // ~1-3ms
// Array.filter 去重
console.time('Array.filter deduplication')
const unique2 = data.filter((item, index) =>
data.indexOf(item) === index
)
console.timeEnd('Array.filter deduplication') // ~50-100ms
// Set 去重性能远优于传统方法3. 添加操作
javascript
const iterations = 100000
// Set 添加
console.time('Set.add')
const set = new Set()
for (let i = 0; i < iterations; i++) {
set.add(i)
}
console.timeEnd('Set.add') // ~5-10ms
// Array 添加
console.time('Array.push')
const arr = []
for (let i = 0; i < iterations; i++) {
arr.push(i)
}
console.timeEnd('Array.push') // ~3-8ms
// Array.push 略快于 Set.add,但 Set 会自动去重性能对比总结
| 操作 | Map | Object | Set | Array | 说明 |
|---|---|---|---|---|---|
| 添加 | 快 | 快 | 快 | 最快 | 性能相近 |
| 查找 | O(1) | O(1) | O(1) | O(n) | Map/Set 查找更快 |
| 删除 | 快 | 慢 | 快 | - | Map 删除更快 |
| 频繁增删 | 优 | 差 | - | - | Map 性能优势明显 |
| 去重 | - | - | 快 | 慢 | Set 去重最快 |
性能优化建议
javascript
// ✅ 需要频繁判断值是否存在时,使用 Set
const validIds = new Set([1, 2, 3, 4, 5])
if (validIds.has(userId)) {
// ...
}
// ✅ 需要频繁增删键值对时,使用 Map
const cache = new Map()
cache.set(key, value)
cache.delete(key)
// ✅ 需要大量去重时,使用 Set
const uniqueValues = [...new Set(array)]
// ✅ 需要非字符串键时,使用 Map
const elementData = new Map()
elementData.set(domElement, data)
// ⚠️ 性能不是唯一考虑因素
// 根据具体场景选择合适的数据结构
// - Object:JSON 序列化、字面量语法、原型链
// - Map:非字符串键、有序、频繁增删
// - Set:去重、集合运算、快速查找
// - Array:索引访问、排序、多维数据总结
Map是键值对集合,键可以是任意类型Set是值的集合,值唯一不重复Map和Set都是有序的Map适合需要非字符串键的场景Set适合去重和集合运算- 频繁增删数据的场景,
Map性能优于Object
常见问题解答(FAQ)
// ... 中间省略 ...
- 使用
Object:- 键只需要字符串或 Symbol
- 需要序列化为 JSON
- 需要使用对象字面量语法
- 需要原型链或方法
code
// 示例:选择合适的数据结构
// ✅ 使用 Map
const userMetadata = new Map()
const user = { id: 1 }
userMetadata.set(user, { lastLogin: Date.now() })
// ✅ 使用 Object
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retryAttempts: 3
}
const json = JSON.stringify(config) // Object 可以直接序列化2. Set 和 Array 有什么区别?应该选择哪一个?
主要区别:
| 特性 | Set | Array |
|---|---|---|
| 值的唯一性 | 唯一 | 可重复 |
| 索引访问 | ✗ | ✓ |
| 有序性 | ✓ | ✓ |
| 查找性能(has/includes) | O(1) | O(n) |
| 去重 | 自动 | 需手动实现 |
选择建议:
-
使用
Set:- 需要唯一值
- 需要频繁判断值是否存在
- 需要进行集合运算
- 不需要索引访问
-
使用
Array:- 需要通过索引访问元素
- 需要存储重复值
- 需要排序或过滤
- 需要使用数组的各种方法
javascript
// 示例:选择合适的数据结构
// ✅ 使用 Set
const uniqueTags = new Set(['javascript', 'typescript', 'javascript'])
console.log(uniqueTags.has('javascript')) // O(1)
// ✅ 使用 Array
const sortedScores = [95, 87, 92, 88]
sortedScores.sort((a, b) => b - a) // 需要排序
console.log(sortedScores[0]) // 需要索引访问3. 如何判断一个对象是 Map 还是 Set?
javascript
// 方法 1:使用 instanceof
const map = new Map()
const set = new Set()
console.log(map instanceof Map) // true
console.log(set instanceof Set) // true
// 方法 2:使用 constructor.name
console.log(map.constructor.name) // 'Map'
console.log(set.constructor.name) // 'Set'
// 方法 3:使用 Object.prototype.toString
console.log(Object.prototype.toString.call(map)) // '[object Map]'
console.log(Object.prototype.toString.call(set)) // '[object Set]'
// 检测函数
function isMap(value) {
return value instanceof Map ||
Object.prototype.toString.call(value) === '[object Map]'
}
function isSet(value) {
return value instanceof Set ||
Object.prototype.toString.call(value) === '[object Set]'
}4. 如何克隆 Map 或 Set?
javascript
// Map 克隆
const originalMap = new Map([['a', 1], ['b', 2]])
// 方法 1:使用构造函数(浅拷贝)
const clonedMap1 = new Map(originalMap)
// 方法 2:使用展开运算符(浅拷贝)
const clonedMap2 = new Map([...originalMap])
// 深拷贝
const deepClonedMap = new Map(
JSON.parse(JSON.stringify([...originalMap]))
)
// Set 克隆
const originalSet = new Set([1, 2, 3])
// 方法 1:使用构造函数
const clonedSet1 = new Set(originalSet)
// 方法 2:使用展开运算符
const clonedSet2 = new Set([...originalSet])5. 如何合并多个 Map 或 Set?
javascript
// 合并 Map
const map1 = new Map([['a', 1], ['b', 2]])
const map2 = new Map([['b', 3], ['c', 4]])
const map3 = new Map([['d', 5]])
// 方法 1:展开运算符(后面的会覆盖前面的)
const mergedMap1 = new Map([...map1, ...map2, ...map3])
console.log(mergedMap1.get('b')) // 3(map2 覆盖了 map1)
// 方法 2:逐个添加
const mergedMap2 = new Map(map1)
map2.forEach((value, key) => mergedMap2.set(key, value))
map3.forEach((value, key) => mergedMap2.set(key, value))
// 合并 Set
const set1 = new Set([1, 2, 3])
const set2 = new Set([3, 4, 5])
const set3 = new Set([5, 6, 7])
const mergedSet = new Set([...set1, ...set2, ...set3])
console.log([...mergedSet]) // [1, 2, 3, 4, 5, 6, 7]6. 如何序列化和反序列化 Map?
Map 不能直接序列化为 JSON,需要先转换。
javascript
const map = new Map([
['name', 'John'],
['age', 30],
['hobbies', ['reading', 'coding']]
])
// 序列化
// 方法 1:转换为对象
const jsonObj = Object.fromEntries(map)
const jsonStr = JSON.stringify(jsonObj)
// 方法 2:转换为数组
// ... 中间省略 ...
return JSON.stringify([...map])
}
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr))
}7. Map 和 Set 的性能如何?
查找性能:
javascript
// Map vs Object 查找性能
const map = new Map()
const obj = {}
// 添加 100000 个键值对
for (let i = 0; i < 100000; i++) {
map.set(`key${i}`, i)
obj[`key${i}`] = i
}
// 查找性能测试
console.time('Map.get')
// ... 中间省略 ...
console.time('Array.includes')
for (let i = 0; i < 10000; i++) {
arr.includes(Math.floor(Math.random() * 100000))
}
console.timeEnd('Array.includes') // ~200ms性能总结:
Map和Object的查找性能相近(都是 O(1))Set.has()明显快于Array.includes()(O(1) vs O(n))Map在频繁增删场景下性能优于ObjectSet适合需要频繁判断值是否存在的场景
8. 如何实现 Map 或 Set 的子类?
javascript
// 继承 Map
class ExtendedMap extends Map {
// 添加过滤方法
filter(callback) {
const filtered = new ExtendedMap()
for (const [key, value] of this) {
if (callback(value, key, this)) {
filtered.set(key, value)
}
}
return filtered
}
// ... 中间省略 ...
// 添加交集方法
intersection(otherSet) {
return new ExtendedSet([...this].filter(x => otherSet.has(x)))
}
}9. NaN 在 Map 和 Set 中是如何处理的?
javascript
// Map 中的 NaN
const map = new Map()
map.set(NaN, 'not a number')
map.set(NaN, 'still not a number') // 覆盖之前的值
map.set(0/0, 'another NaN') // 也覆盖
console.log(map.size) // 1
console.log(map.has(NaN)) // true
console.log(map.get(NaN)) // 'another NaN'
console.log(map.get(0/0)) // 'another NaN'
console.log(map.get(Number('foo'))) // 'another NaN'
// Set 中的 NaN
const set = new Set()
set.add(NaN)
set.add(NaN) // 被忽略
set.add(0/0) // 被忽略
set.add(Number('foo')) // 被忽略
console.log(set.size) // 1
console.log(set.has(NaN)) // true
// 原因:Map 和 Set 使用 SameValueZero 算法
// SameValueZero 认为 NaN === NaN10. 为什么 Map 和 Set 没有 map、filter 等数组方法?
Map 和 Set 没有内置的 map、filter 等方法,原因如下:
- 语义不同:Map 是键值对集合,Set 是值集合,与数组的使用场景不同
- API 设计哲学:ES6 设计者希望保持 API 简洁,避免过度复杂
- 可扩展性:开发者可以根据需要自己扩展
解决方案:
javascript
// Map 的 map 和 filter
const map = new Map([['a', 1], ['b', 2], ['c', 3]])
// 使用数组方法转换
const filtered = new Map(
[...map].filter(([key, value]) => value > 1)
)
const mapped = new Map(
[...map].map(([key, value]) => [key, value * 2])
)
// Set 的 map 和 filter
const set = new Set([1, 2, 3, 4, 5])
const filteredSet = new Set([...set].filter(x => x > 2))
const mappedSet = new Set([...set].map(x => x * 2))
// 或使用工具函数
function mapMap(map, callback) {
return new Map(
[...map].map(([key, value]) => [key, callback(value, key, map)])
)
}
function filterMap(map, callback) {
return new Map(
[...map].filter(([key, value]) => callback(value, key, map))
)
}