前端进阶:Map和Set的8个实用技巧,告别低效代码
用{}存数据、用indexOf去重,是不是已经刻入肌肉记忆了?今天彻底升级你的Map和Set工具箱,看完就能用到项目里。
先搞清楚:Map vs Object,Set vs Array
很多人把Map当「高级对象」用,把Set当「去重工具」用,然后就止步于此了。实际上它们能做的远不止这些。
技巧1:Map的key可以是任意类型
Object的key只能是字符串或Symbol,Map没有这个限制。
const userCache = new Map()
// 用DOM元素做key
const btn = document.querySelector('#submit')
userCache.set(btn, { clickCount: 0 })
// 用对象做key(记录对象间的关系)
const alice = { name: 'Alice' }
const bob = { name: 'Bob' }
const friendMap = new Map()
friendMap.set(alice, [bob])
// 用函数做key
const fn = () => 'hello'
userCache.set(fn, '函数的元数据')
console.log(friendMap.get(alice)) // [{ name: 'Bob' }]实战场景:组件实例到状态映射、DOM节点到事件记录、React中记录每个Ref对应的状态。
技巧2:Map保留插入顺序,Object不保证
// Object的key顺序有坑(数字key会被排序)
const obj = { 3: 'c', 1: 'a', 2: 'b' }
console.log(Object.keys(obj)) // ['1', '2', '3'] 被排序了
// Map严格保持插入顺序
const map = new Map()
map.set(3, 'c')
map.set(1, 'a')
map.set(2, 'b')
console.log([...map.keys()]) // [3, 1, 2] 保持插入顺序实战场景:保持菜单配置顺序、记录操作历史、维护有序的tab状态。
技巧3:用Map做计数器,比Object简洁
const words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
// 用Object(需要手动处理undefined)
const countObj = {}
words.forEach(w => countObj[w] = (countObj[w] || 0) + 1)
// 用Map(一行搞定,语义更清晰)
const countMap = new Map()
words.forEach(w => countMap.set(w, (countMap.get(w) ?? 0) + 1))
// 排序:找出出现次数最多的词
const sorted = [...countMap.entries()].sort((a, b) => b[1] - a[1])
console.log(sorted[0]) // ['apple', 3]技巧4:Set去重不只是new Set(arr)
// 基础:数组去重
const nums = [1, 2, 2, 3, 3, 3]
const unique = [...new Set(nums)] // [1, 2, 3]
// 进阶:对象数组按某个字段去重
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice(重复)' },
]
const seen = new Set()
const uniqueUsers = users.filter(u => {
if (seen.has(u.id)) return false
seen.add(u.id)
return true
})
console.log(uniqueUsers) // 只保留id为1的第一条技巧5:Set实现集合运算(交集、并集、差集)
const setA = new Set([1, 2, 3, 4])
const setB = new Set([3, 4, 5, 6])
// 并集:A ∪ B
const union = new Set([...setA, ...setB])
// Set {1, 2, 3, 4, 5, 6}
// 交集:A ∩ B
const intersection = new Set([...setA].filter(x => setB.has(x)))
// Set {3, 4}
// 差集:A - B(在A中但不在B中)
const difference = new Set([...setA].filter(x => !setB.has(x)))
// Set {1, 2}实战场景:权限计算(用户权限 ∩ 页面所需权限)、标签筛选(A类标签 - 已选标签)。
技巧6:Map链式操作和解构迭代
const config = new Map([
['host', 'localhost'],
['port', 3000],
['debug', true],
])
// 链式set(Map.set()返回Map自身)
config
.set('timeout', 5000)
.set('retries', 3)
// 解构迭代(比for...of更直观)
for (const [key, value] of config) {
console.log(`${key}: ${value}`)
}
// 转对象(仅在key都是字符串时可用)
const obj = Object.fromEntries(config)
// { host: 'localhost', port: 3000, debug: true, timeout: 5000, retries: 3 }
// 转回Map(从普通对象恢复)
const map2 = new Map(Object.entries(obj))技巧7:WeakMap做私有数据(Vue3的实际做法)
// WeakMap:key必须是对象,不阻止GC(不会内存泄漏)
const privateData = new WeakMap()
class BankAccount {
constructor(balance) {
// balance存在WeakMap里,外部无法直接访问
privateData.set(this, { balance })
}
deposit(amount) {
const data = privateData.get(this)
data.balance += amount
}
getBalance() {
return privateData.get(this).balance
}
}
const account = new BankAccount(1000)
account.deposit(500)
console.log(account.getBalance()) // 1500
console.log(account.balance) // undefined,真正私有Vue3源码中:reactiveMap、readonlyMap都是WeakMap,key是原始对象,value是代理对象,对象销毁时自动清理,不会造成内存泄漏。
技巧8:Set配合has()做O(1)查找
// Array.includes()是O(n),数据量大时很慢
const blocklist = ['spam@evil.com', 'hack@bad.com', /* ...10万条... */]
function isBlocked(email) {
return blocklist.includes(email) // 线性查找,慢
}
// Set.has()是O(1),无论多少数据都是常数时间
const blockSet = new Set(blocklist)
function isBlockedFast(email) {
return blockSet.has(email) // 哈希查找,快
}
// 实战:大量权限判断
const userPermissions = new Set(['read', 'write', 'export'])
function checkPermission(action) {
return userPermissions.has(action) // O(1),不用indexOf或includes
}完整Demo(可直接在浏览器运行)
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Map和Set进阶演示</title>
<style>
body { font-family: monospace; background: #1a1a2e; color: #eee; padding: 20px; }
button { background: #16213e; color: #0f3460; border: 1px solid #0f3460;
padding: 8px 16px; margin: 5px; cursor: pointer; border-radius: 4px; color: #e94560; }
button:hover { background: #0f3460; }
pre { background: #16213e; padding: 15px; border-radius: 8px;
border-left: 3px solid #e94560; white-space: pre-wrap; }
</style>
</head>
<body>
<h2 style="color:#e94560">Map和Set进阶技巧演示</h2>
<button onclick="demo1()">技巧3:Map计数器</button>
<button onclick="demo2()">技巧5:集合运算</button>
<button onclick="demo3()">技巧6:Map转Object</button>
<button onclick="demo4()">技巧8:O(1)查找性能</button>
<pre id="output">点击上方按钮查看演示...</pre>
<script>
const out = document.getElementById('output')
const log = (...args) => { out.textContent += args.join(' ') + '\n' }
const clear = () => { out.textContent = '' }
function demo1() {
clear()
log('=== 技巧3:Map计数器 ===')
const words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
const countMap = new Map()
words.forEach(w => countMap.set(w, (countMap.get(w) ?? 0) + 1))
const sorted = [...countMap.entries()].sort((a, b) => b[1] - a[1])
sorted.forEach(([word, count]) => log(`${word}: ${'★'.repeat(count)} (${count}次)`))
}
function demo2() {
clear()
log('=== 技巧5:集合运算 ===')
const A = new Set([1, 2, 3, 4])
const B = new Set([3, 4, 5, 6])
log('集合A:', [...A].join(', '))
log('集合B:', [...B].join(', '))
log('并集A∪B:', [...new Set([...A, ...B])].join(', '))
log('交集A∩B:', [...A].filter(x => B.has(x)).join(', '))
log('差集A-B:', [...A].filter(x => !B.has(x)).join(', '))
}
function demo3() {
clear()
log('=== 技巧6:Map与Object互转 ===')
const config = new Map([['host','localhost'],['port',3000],['debug',true]])
config.set('timeout', 5000).set('retries', 3)
log('Map内容:')
for (const [k, v] of config) log(` ${k}: ${v}`)
const obj = Object.fromEntries(config)
log('\n转成Object:', JSON.stringify(obj))
const map2 = new Map(Object.entries(obj))
log('再转回Map,size:', map2.size)
}
function demo4() {
clear()
log('=== 技巧8:O(1)与O(n)查找性能对比 ===')
const SIZE = 100000
const data = Array.from({length: SIZE}, (_, i) => `email${i}@test.com`)
const target = `email${SIZE - 1}@test.com`
const arr = data
const set = new Set(data)
const t1 = performance.now()
for (let i = 0; i < 1000; i++) arr.includes(target)
const t2 = performance.now()
for (let i = 0; i < 1000; i++) set.has(target)
const t3 = performance.now()
log(`Array.includes(1000次): ${(t2-t1).toFixed(2)}ms`)
log(`Set.has(1000次): ${(t3-t2).toFixed(2)}ms`)
log(`Set快了约: ${((t2-t1)/(t3-t2)).toFixed(0)}倍`)
}
</script>
</body>
</html>速查表
| 场景 | 用什么 | 原因 |
|---|---|---|
| key是对象/函数/DOM | Map | Object的key只能是字符串 |
| 保持插入顺序 | Map | Object的数字key会被排序 |
| 频繁增删key | Map | 性能优于Object |
| 数组去重 | Set | 简洁,O(1)查找 |
| 集合运算(交/并/差) | Set | 天然支持has()判断 |
| 大量includes判断 | Set | O(1) vs O(n),性能差距巨大 |
| 私有数据且不内存泄漏 | WeakMap | key是对象,GC自动清理 |
| 关联DOM节点数据 | WeakMap | 节点被移除,数据自动释放 |
小结
Map和Set的核心价值:
Map:任意类型key、保持顺序、频繁增删时性能优于Object
Set:天然去重、集合运算、O(1)查找
WeakMap:私有数据 + 自动内存管理(Vue3核心用法)
何时用Object?静态配置、JSON序列化、字符串key的简单映射,Object就够了,不必过度使用Map。
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!