前言

在 JavaScript 中,ObjectMap 都是用于存储键值对的集合,常常被混用,例如在数据缓存和快速查找的场景中。两者各有优势,但在不同的使用场景下选择合适的结构非常重要。那么,如何判断在什么情况下使用哪种结构更合适呢?

ObjectMap 的关键区别

键的类型

  • 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 始终保持键值对的插入顺序,迭代时的顺序与插入顺序一致。

大小

  • Mapsize 属性可以直接获取元素的数量。
  • 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 共享同一隐藏类

隐藏类的演化:

  • 隐藏类会根据对象的属性动态生成,并随着属性的变化而演化。
    • 初始创建:为一个空对象生成一个基础隐藏类。
    • 属性添加:每添加一个新属性,隐藏类就会进化。
    • 属性顺序:属性添加顺序会影响隐藏类的生成。

删除属性的影响:

  • 当删除属性时,对象可能切换到字典模式,导致性能下降。频繁删除属性会增加存储的稀疏性,并使对象无法回到隐藏类模式,从而略微增加内存开销。

MapObject 的性能差异

动态扩展机制

MapObject 都需要动态扩展存储空间以支持更多键值对,但 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...inObject.keys() 等方式
适用场景 需要频繁存取、顺序迭代、多样化键值场景 简单键值存储和原型链相关操作
  • Map 适用于性能要求高、需要非字符串键、或频繁进行增删改查、保持键插入顺序的场景;而 Object 更适合简单的键值对存储或需要与原型链兼容的场景。

对于大数据量的场景,优先选择 Map,特别是当你需要高性能或处理非字符串类型键时。而 Object 更适用于 轻量级.