Back

JavaScript 中 Map、Set 和 Object 有什么区别?

JavaScript 中 Map、Set 和 Object 有什么区别?

你正在重构一个组件,需要存储键值对。你习惯性地想用 object,但突然停下来思考:这里应该用 Map 吗?Set 会不会更合适?这个决定比看起来更重要——每种数据结构都有独特的性能特征和语义含义,会影响代码的清晰度和效率。

本文将对 JavaScript 数据结构进行比较,详细分析 Map、Set 和 Object 之间的实际差异,帮助你在生产代码中做出正确选择。

核心要点

  • Object 会将键强制转换为字符串,而 Map 会精确保留键的类型——这个区别可以防止微妙的冲突 bug。
  • 对于频繁的添加和删除操作,Map 性能优于 Object,而对于结构稳定的读密集型工作负载,Object 表现更佳。
  • Set 提供 O(1) 的成员检查,并且在现代 JavaScript 中原生支持 unionintersectiondifference 方法。
  • 默认使用 Object 处理结构化数据,当键是动态的或非字符串类型时使用 Map,当唯一性是主要关注点时使用 Set。

核心用途:何时使用 Map vs Set vs Object

JavaScript 中 Map 和 Object 的区别归根结底在于意图和键的处理方式:

  • Object:具有已知字符串/symbol 键的结构化数据。适用于配置、组件 props、API 响应等场景。
  • Map:键可以是任何类型的动态键值集合。适用于缓存、使用对象作为键的查找表、频繁的添加和删除操作。
  • Set:不带键的唯一值集合。适用于去重、跟踪已见项、成员检查。

键处理:Map 和 Object 的根本区别

Object 会将键强制转换为字符串。Map 会精确保留键的类型。

const obj = {}
obj[1] = 'number'
obj['1'] = 'string'
console.log(Object.keys(obj)) // ['1'] — 发生冲突

const map = new Map()
map.set(1, 'number')
map.set('1', 'string')
console.log(map.size) // 2 — 键是不同的

Map 还可以接受对象作为键——这是 object 在不使用序列化变通方案的情况下根本无法做到的。

const userCache = new Map()
const user = { id: 42 }
userCache.set(user, { lastSeen: Date.now() })

有关完整的键语义,请参阅 MDN 文档中的 Map

迭代和排序语义

这三种数据结构都有明确的迭代顺序,但语义有所不同:

Map 和 Set:纯粹的插入顺序。始终如此。

Object:字符串键按插入顺序排列,但整数索引键(例如 '1''42')会首先按数字顺序排序。这常常让开发者感到意外:

const obj = { b: 1, 2: 2, a: 3, 1: 4 }
console.log(Object.keys(obj)) // ['1', '2', 'b', 'a']

Map 和 Set 可以直接使用 for...of 进行迭代。Object 需要使用 Object.keys()Object.values()Object.entries()

大规模场景下的性能特征

对于 JavaScript Map vs Set vs Object 的性能问题,具体场景很重要:

Object 在使用字符串键的读密集型工作负载中表现出色。像 V8 这样的现代引擎通过隐藏类(hidden classes)优化属性访问,使读取非常快——前提是对象结构保持稳定。

Map 在频繁添加和删除操作时更胜一筹。对象属性删除(通过 delete)可能会使隐藏类失效,从而触发反优化。ECMAScript 规范保证 Map 的平均访问时间为亚线性,但不强制规定具体的内部实现。

Set 通过 has() 提供 O(1) 的成员检查,对于任何非平凡大小的集合,性能都优于数组的 includes()

原生 Set 操作

Set 现在包含了常见操作的内置方法(参见 MDN 文档中的 Set):

const a = new Set([1, 2, 3])
const b = new Set([2, 3, 4])

a.union(b)               // Set {1, 2, 3, 4}
a.intersection(b)        // Set {2, 3}
a.difference(b)          // Set {1}
a.symmetricDifference(b) // Set {1, 4}
a.isSubsetOf(b)          // false

这些方法在现代浏览器中得到了广泛支持。

前端实际应用场景

使用 Object 的场景:

  • 定义组件 props 或配置
  • 处理 JSON 序列化(Map 不能直接序列化)
  • 键在编写时已知

使用 Map 的场景:

  • 键是动态的或由用户提供(避免原型污染)
  • 需要使用对象引用作为键
  • 需要跟踪大小(map.size vs Object.keys(obj).length)
  • 构建需要频繁更新的缓存

使用 Set 的场景:

  • 跟踪唯一 ID 或已见项
  • 数组去重:[...new Set(array)]
  • 快速成员测试

快速参考

特性ObjectMapSet
键类型string, symbol任意类型N/A(仅值)
大小Object.keys().length.size.size
迭代间接直接直接
JSON 支持原生手动手动
原型风险

结论

在 Map、Set 和 Object 之间做选择,不是关于哪个”更好”的问题——而是关于如何将数据结构与你的使用场景相匹配。对于具有字符串键的结构化数据,Object 仍然是正确的选择。Map 能够干净地处理动态键值场景。Set 能够高效地解决唯一性问题。

为了简单起见,默认使用 object。当需要 Map 的特定功能时使用 Map。当唯一性是主要关注点时使用 Set。

常见问题

不能直接转换。JSON.stringify 不能原生处理 Map。你需要先使用 Array.from(map) 或展开运算符将 Map 转换为条目数组,然后对该数组进行字符串化。要恢复它,解析 JSON 并将结果数组传递给 Map 构造函数:new Map(JSON.parse(jsonString))。

Map 处理频繁的插入和删除操作时,不会出现对象属性删除可能导致的反优化。它们还通过 size 属性原生跟踪大小,接受任何值类型作为键(包括 DOM 节点或对象引用),并且不存在用户提供的键导致的原型污染风险。

这些方法现在在现代浏览器中得到了广泛支持。如果需要支持较旧的环境,请检查兼容性表或使用 polyfill。

当需要保留重复项的插入顺序、需要基于索引的访问,或者集合足够小以至于 Set 的开销不合理时,使用数组。对于较大的集合,当唯一性和快速查找是优先考虑的因素时,Set 是更好的选择。

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay