前端进阶:Map和Set的8个实用技巧,告别低效代码

更新日期: 2026-04-23 阅读: 16 标签: 代码

用{}存数据、用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是对象/函数/DOMMapObject的key只能是字符串
保持插入顺序MapObject的数字key会被排序
频繁增删keyMap性能优于Object
数组去重Set简洁,O(1)查找
集合运算(交/并/差)Set天然支持has()判断
大量includes判断SetO(1) vs O(n),性能差距巨大
私有数据且不内存泄漏WeakMapkey是对象,GC自动清理
关联DOM节点数据WeakMap节点被移除,数据自动释放

小结

Map和Set的核心价值:

  • Map:任意类型key、保持顺序、频繁增删时性能优于Object

  • Set:天然去重、集合运算、O(1)查找

  • WeakMap:私有数据 + 自动内存管理(Vue3核心用法)

何时用Object?静态配置、JSON序列化、字符串key的简单映射,Object就够了,不必过度使用Map。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/13684

相关推荐

减少嵌套,降低代码复杂度

减少嵌套会让代码可读性更好,同时也能更容易的找出bug,开发人员可以更快的迭代,程序也会越来越稳定。简化代码,让编程更轻松!

js高亮显示关键词_页面、搜索关键词高亮显示

页面实现关键词高亮显示:在项目期间遇到一个需求,就是搜索关键词时需要高亮显示,主要通过正则匹配来实现页面关键词高亮显示。在搜索结果中高亮显示关键词:有一组关键词数组,在数组中筛选出符合关键字的内容并将关键字高亮

源代码是什么意思

源代码(也称源程序),是指一系列人类可读的计算机语言指令。 在现代程序语言中,源代码可以是以书籍或者磁带的形式出现,但最为常用的格式是文本文件,这种典型格式的目的是为了编译出计算机程序。

Js代码压缩工具推荐

JavaScript 代码压缩是指去除源代码里的所有不必要的字符,而不改变其功能的过程。这些不必要的字符通常包括空格字符,换行字符,注释以及块分隔符等用来增加可读性的代码,但并不需要它来执行。

tinymce与prism代码高亮实现及汉化的配置

TinyMCE是一个轻量级的基于浏览器的所见即所得编辑器,由JavaScript写成。它对IE6+和Firefox1.5+都有着非常良好的支持。功能方强大,并且功能配置灵活简单。另一特点是加载速度非常快的。

Google内部在代码质量上的实践

良好的编程习惯涉及到很多方面,但在软件行业内,大多数的公司或组织都不会把良好的编程习惯列为主要关注点。 例如,具有可读性和可维护性的代码比编写好的测试代码或使用正确的工具更有意义,前者的意义在于可以让代码更易于理解和修改。

接手代码太烂,要不要辞职?

朋友发表了一条说说:入职新公司,从重构代码到放弃”,我就问他怎么了?他说,刚进一家新公司,接手代码太烂,领导让我先熟悉业务逻辑,然后去修复之前项目中遗留的bug,实在不行就重构

网站加入收藏、设为首页Js代码(兼容各种浏览器)

这里虽然说是兼容,但是有些浏览器的设置就是不支持用js来把页面设为首页,加入收藏夹,只能让用户手动去在浏览器或者按键去设置这些功能,这里说的兼容是指当浏览器有这个设置的时候js会有提示

提升JavaScript代码质量的最佳实践

在JavaScript编程中,代码质量优化是一项重要的技能。它可以帮助我们提高代码的可读性、可维护性和性能。本文将通过一些实际优化过程中的案例,展示如何通过一些技巧和最佳实践,使我们的代码更加优雅。

javascript代码语句结束要不要加分号?

在 C 语言中,分号是语句结束的标志,在语句结束的地方一定要以分号结束。而 JavaScript 的分号却是可选的,若语句都各占一行,则可以省略分号。avaScript 中的 ASI 机制,允许我们省略分号。ASI 机制不是说在解析过程中解析器自动把分号添加到代码中

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!