12k
All articles

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

对比 Map、Set 和 Object 在 JavaScript 中的键处理方式、迭代顺序及性能特征,以便选择合适的数据结构。

OpenReplay Team
OpenReplay Team
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。

常见问题

我可以将 Map 转换为 JSON 并转换回来吗?

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

为什么我要使用 Map 而不是普通对象来做缓存?

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

所有浏览器都支持像 union 和 intersection 这样的 Set 方法吗?

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

什么时候我应该优先使用数组而不是 Set 来存储唯一值?

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

Open-source session replay

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.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.