深入剖析 JavaScript 中 Object 与 Map 的区别与应用场景
前言
在 JavaScript 中,Object
和 Map
都是用于存储键值对的集合,常常被混用,例如在数据缓存和快速查找的场景中。两者各有优势,但在不同的使用场景下选择合适的结构非常重要。那么,如何判断在什么情况下使用哪种结构更合适呢?
Object
与 Map
的关键区别
键的类型
Object
的键只能是字符串或符号(Symbol)。如果使用其他类型的键,JavaScript 会通过toString
方法将其转换为字符串。注意,如果你用object当key,object.toString是一个”[object Object]”,也就是说所有object类型的key默认都是这个字符串,那么也就无法区分key,一定要用object当key,需要使用JSON.stringfy, 例如:
const obj={}
const theObjectKey={a:1}
obj[JSON.stringfy(theObjectKey)]='someValue'
// 不能直接用 obj[theObjectKey]='someValue' ❌Map
的键可以是任意类型,包括函数、对象以及任何原始类型。
键的顺序
Object
不保证键的顺序与插入顺序一致。数字类型的键按升序排列,其他类型的键按插入顺序排列。Map
始终保持键值对的插入顺序,迭代时的顺序与插入顺序一致。
大小
Map
的size
属性可以直接获取元素的数量。Object
需要手动计算大小。
迭代
Map
是 iterable 的,可以直接用于for...of
循环。Object
需要使用Object.keys()
或其他方法来进行迭代。
功能和方法
Map
提供了更多便利的方法,如has
用于检查键是否存在,set
用于设置键值对,get
用于获取键对应的值,以及size
属性用于获取键值对的数量。Object
需要手动实现这些功能。例如,使用hasOwnProperty
来检查属性是否存在。而且,字面量方式创建的Object
会通过原型链自动继承Object
对象,这意味着即使是一个空的Object
,它也并非真正的“空对象”,它包含了如toString
等预定义属性。
底层实现
Map
的底层实现
Map
的核心是哈希表,通过对键进行哈希计算,将其映射到某个桶(bucket)。- 哈希表允许
Map
支持 任意类型的键(包括对象、函数、数字等),而不仅仅是字符串或符号。
Object
的底层实现
Object
是 JavaScript 的核心数据结构,其底层实现是一个优化后的 动态哈希表 或其他类似结构。- 键只能是字符串或符号(非字符串键会自动转换为字符串),这使得
Object
的键范围相较于Map
更加有限。
原型链
Object
默认继承原型链(Object.prototype
),这意味着一些预定义属性(如toString
)可能会与用户定义的键发生冲突,除非使用Object.create(null)
来创建一个没有原型链的对象。
隐藏类
- JavaScript 引擎(如 V8)使用隐藏类来优化对象属性访问。尽管 JavaScript 是动态语言,允许随时添加或删除对象的属性,但这种动态性会影响性能。
- 隐藏类将对象的属性访问转化为类似静态语言的固定内存布局,大大 提高了属性访问的效率。
隐藏类的核心目标是:
- 将对象的属性访问转化为快速的偏移量访问(类似于静态语言中的固定内存布局)。
偏移量访问:
- 每个隐藏类维护一个属性到存储位置的映射表。例如:
obj.a = 1; // 'a' 映射到偏移量 0
obj.b = 2; // 'b' 映射到偏移量 1
共享隐藏类:
- 多个具有相同属性结构的对象会共享同一个隐藏类,从而避免重复生成。例如:
let obj1 = { a: 1, b: 2 };
let obj2 = { a: 3, b: 4 }; // obj1 和 obj2 共享同一隐藏类
隐藏类的演化:
- 隐藏类会根据对象的属性动态生成,并随着属性的变化而演化。
- 初始创建:为一个空对象生成一个基础隐藏类。
- 属性添加:每添加一个新属性,隐藏类就会进化。
- 属性顺序:属性添加顺序会影响隐藏类的生成。
删除属性的影响:
- 当删除属性时,对象可能切换到字典模式,导致性能下降。频繁删除属性会增加存储的稀疏性,并使对象无法回到隐藏类模式,从而略微增加内存开销。
Map
和 Object
的性能差异
动态扩展机制
Map
和 Object
都需要动态扩展存储空间以支持更多键值对,但 Map
的扩展机制更高效:
在扩展时,
Map
会重新分配存储桶并重新哈希键值,这通常是一次性成本。Object
的扩展依赖引擎的优化,但通常为了兼容性,其效率不如专为键值操作优化的Map
。小规模对象:高效存储:在属性数量较少时,
Object
使用隐藏类机制优化属性访问,插入属性时无需重新分配内存。大规模对象:切换为字典模式:当属性数量超出隐藏类所能支持的范围,或属性变化频繁时,
Object
会切换到字典模式,使用散列表存储属性并增加扩展成本。
字符串化开销
- 在
Object
中,所有键必须转换为字符串,这对于大数据量的对象来说可能带来额外的性能开销。相比之下,Map
不需要进行键的字符串化,能够直接处理任何类型的键。
原型链查找
Object
是基于原型链的。如果查找的键不存在于当前对象,JavaScript 会沿着原型链向上查找,这可能导致性能下降。Map
不受原型链的影响,它所有的键值对都直接存储在哈希表中,因此查找性能更加稳定。
键的顺序与迭代效率
Map
的顺序性:Map
按插入顺序存储键值对,避免了额外的排序或查找成本,因此在大数据量下插入和迭代效率更高。Object
的无序性:虽然现代引擎保证Object
在某些情况下保留插入顺序,但数值键和非数值键的存储顺序可能会混淆,迭代Object
键时需要使用额外的方法,效率通常低于Map
。
内存管理
Map
在现代 JavaScript 引擎中有更好的内存分配与管理:
- 使用更紧凑的内存布局,降低存储开销。
- 支持
WeakMap
,通过弱引用自动管理内存,避免内存泄漏。Object
不具备此功能。
总结
特性 | Map |
Object |
---|---|---|
键的类型 | 任意类型 | 字符串或 Symbol |
有序性 | 插入顺序 | 数值键按升序,其他按插入顺序 |
原型链影响 | 无影响 | 受原型链影响 |
性能优化 | 针对键值存储和频繁操作进行了优化 | 为通用性设计,性能可能稍逊 |
迭代 | 支持 for...of 和 .forEach() |
使用 for...in 或 Object.keys() 等方式 |
适用场景 | 需要频繁存取、顺序迭代、多样化键值场景 | 简单键值存储和原型链相关操作 |
Map
适用于性能要求高、需要非字符串键、或频繁进行增删改查、保持键插入顺序的场景;而Object
更适合简单的键值对存储或需要与原型链兼容的场景。
对于大数据量的场景,优先选择 Map
,特别是当你需要高性能或处理非字符串类型键时。而 Object
更适用于 轻量级.
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 HzmBlog!