Skip to content

V8 对象属性存储:快属性与慢属性

在 JavaScript 中,对象是属性的集合。但 V8 为了优化不同场景下的属性访问,在底层将属性分为了多种存储模式。


一、一句话先记住

决定属性存哪里的,不是访问语法(. 或 []),而是“键的类型”。

  • 数字键 (0, 1, 2...) → 存放在 Elements (索引属性)
  • 字符串键 ("a", "b"...) → 存放在 Properties (命名属性)

二、V8 对象结构图解

V8 将对象的属性分为 Elements (索引属性)Properties (命名属性),并针对它们分别存储。

V8 对象属性存储逻辑

1. 内存里的真相 (以代码为例)

假设我们有如下对象:

javascript
const obj = {
  a: 1,
  b: 2,
  2: "hello"
};
  1. 隐藏类 (Map/HiddenClass)
    • 只记录命名属性:a → offset 0, b → offset 1
    • 不记录数字索引属性。 👉 详见:隐藏类原理
  2. Elements (索引属性)
    • 采用 线性数组 存储:[ , , "hello"]
    • 索引 2 的位置直接存放值。
  3. Properties (命名属性)
    • 采用 线性数组 (快属性模式下) 存储:[1, 2]
    • a 的值 1 在 offset 0,b 的值 2 在 offset 1。

三、访问时到底发生了什么?

访问语法键的类型V8 判定逻辑查找路径
obj.a字符串 "a"命名属性查隐藏类偏移量 → 去 Properties 拿值
obj["b"]字符串 "b"命名属性查隐藏类偏移量 → 去 Properties 拿值
obj[2]数字 2索引属性不看隐藏类 → 直接去 Elements 拿值
obj["2"]字符串 "2"自动转为数字同上,走 Elements 路径

👉 核心点.[] 只是语法糖。真正决定走哪条路的是键的类型。字符串键通常走 Properties,数字键(或可转为数字的字符串)走 Elements。


四、深入理解:快属性 (Fast) vs 慢属性 (Slow)

快属性和慢属性,本质上只有一件事不同:Properties 里的属性是怎么存的。

1. 先忘掉“快 / 慢”,换个直观的名字

官方叫法你可以叫它存储形式是否依赖隐藏类
快属性 (Fast)数组式属性线性数组 [值, 值]✅ 是
慢属性 (Slow)字典式属性哈希表 {"a": 1}❌ 否

2. 快属性 (数组式) —— 到底“快”在哪?

1️⃣ 前提条件

  • 属性数量不多。
  • 没有使用 delete
  • 属性添加顺序稳定。 👉 满足以上条件,V8 就会优先使用快属性。

2️⃣ 内存里长什么样?

  • HiddenClass: 记录 a → offset 0, b → offset 1
  • Properties: 存储为一个纯数组 [1, 2]

3️⃣ 访问逻辑

当执行 obj.a 时:

  1. 找到 obj.map (隐藏类)。
  2. 查表得知 a 的偏移量是 0。
  3. 直接取 properties[0]。 ✅ 结论:像数组下标访问一样快,不需要字符串比较。这就是“快”的真正含义。

3. 慢属性 (字典式) —— 到底“慢”在哪?

1️⃣ 什么时候会变成慢属性?

  • 使用 delete:删除非末尾属性会破坏隐藏类的线性结构。
  • 属性太多:当属性数量超过一定阈值(通常是 30 个)。
  • 属性名不固定:频繁使用动态键名。 👉 此时,隐藏类保不住了,V8 会将其降级。

2️⃣ 内存里长什么样?

  • V8 会放弃隐藏类,直接将 Properties 转换为一个 哈希表 (字典)

3️⃣ 访问逻辑

当执行 obj.a 时:

  1. 计算字符串 "a" 的 Hash 值。
  2. 去哈希表中查找对应的 Key。
  3. 拿到 Value。 ❌ 结论:有 Hash 计算、有冲突处理、比数组慢得多。这就是“慢”的真正含义。

4. 正确的“内存心智模型”

text
JSObject
├── map → HiddenClass (快属性时有用)
├── elements → [ 索引属性 ]
└── properties 
      ├── 快属性模式 → [值, 值, 值] (线性数组)
      └── 慢属性模式 → { "a": 值, "b": 值 } (哈希字典)

快属性依赖隐藏类 | ✅ 慢属性不依赖隐藏类


5. 为什么 V8 不一直用快属性?

JavaScript 是极其动态的,如果强行对成千上万个属性或频繁 delete 的对象使用快属性,会导致:

  1. 隐藏类爆炸:每个对象都可能产生一堆中间态隐藏类。
  2. 内存浪费:线性数组扩容和维护成本高。 👉 V8 的策略是:能快就快,快不了就果断变慢。

五、为什么要区分 Elements 和 Properties?

主要原因在于 属性遍历顺序 的规范要求:

  1. ECMAScript 规范要求:
    • 所有的索引属性按升序排列。
    • 所有的命名属性按添加顺序排列。
  2. V8 的策略: 为了满足规范并保持高效,V8 将它们分开存储。索引属性存储在 Elements 数组中,命名属性存储在 Properties 数组或对象内。

六、关联知识

为了更深入地理解 V8 属性访问优化,建议阅读以下文档:


七、高频面试题

1. 解释 V8 中的 Elements 和 Properties 有什么区别?

回答

  • Elements: 专门存储数组索引属性。为了满足升序排列的要求,V8 使用线性数组存储,通过索引直接定位,效率极高。
  • Properties: 存储非索引的命名属性。它们又分为对象内属性(最快)、快属性(线性存储)和慢属性(哈希存储)。

2. 为什么在 JS 中属性的遍历顺序有时是固定的,有时又不是?

回答:根据规范,索引属性(Elements)总是按数值升序遍历,而命名属性(Properties)按添加顺序遍历。V8 底层将它们分开存储就是为了高效地实现这一规范要求。

3. 什么情况下对象会从“快模式”进入“慢模式”?

回答

  • 大量添加属性: 当命名属性数量超过一定阈值。
  • 使用 delete: 删除非最后添加的属性会破坏隐藏类的线性结构。
  • 属性名不固定: 频繁使用动态键名。 一旦进入慢模式(字典模式),属性访问将变慢,且难以恢复到快模式。

4. 如何优化对象属性的访问性能?

回答

  • 构造函数初始化: 在构造函数中一次性初始化所有属性。
  • 顺序一致: 保持不同实例的属性添加顺序一致,以复用隐藏类。
  • 避免 delete: 如果需要移除属性,建议将其设为 nullundefined
  • 使用数组存储索引数据: 尽量不要将索引作为普通对象的键,而是使用真正的数组。
最近更新