深入浅出 JavaScript

作者介绍

子弈(感兴趣的同学可以点击查看子弈的掘金主页哈,内部有大量的优秀文章呀),专有钉钉前端团队成员,负责专有钉钉 PC 客户端的工程化、端上应用、端上模块插件化的开发。

背景

本文主要阐述 JavaScript 语言的运行特性,有助于深入理解 JavaScript。通过此文也可以指导代码的设计原则,提升项目的运行性能。本文主要分为以下几个部分:

  • 隐藏类
  • 元素类型
  • 命名属性

温馨提示: 《Chrome V8 编译浅谈》 是本文的理论支撑,如果对 JavaScript 编译原理不清晰,则可以优先阅读此文。

调试示例

如果想了解 JavaScript 在 V8 中的编译时和运行时信息,可以使用调试工具 D8。 D8 是 V8 引擎的命令行 Shell,可以查看 AST 生成、中间代码 ByteCode、优化代码、反优化代码、优化编译器的统计数据、代码的 GC 等信息。D8 的安装方式很多,包括:

  • 方法一:根据 V8 官方文档 Using d8 以及 Building V8 with GN 进行工具链的下载和编译
  • 方法二:使用别人已经编译好的 D8 工具,可能版本会有滞后性,例如 Mac 版
  • 方法三:使用 JavaScript 引擎版本管理工具,例如 jsvu

本文使用方法三安装 D8 工具,安装完成后执行 v8-debug --help 可以查看具体的命令信息:

# 执行 help 命令查看支持的参数
v8-debug --help

Synopsis:
  shell [options] [--shell] [<file>...]
  d8 [options] [-e <string>] [--shell] [[--module|--web-snapshot] <file>...]

  -e        execute a string in V8
  --shell   run an interactive JavaScript shell
  --module  execute a file as a JavaScript module
  --web-snapshot  execute a file as a web snapshot

SSE3=1 SSSE3=1 SSE4_1=1 SSE4_2=1 SAHF=1 AVX=1 AVX2=1 FMA3=1 BMI1=1 BMI2=1 LZCNT=1 POPCNT=1 ATOM=0
The following syntax for options is accepted (both '-' and '--' are ok):
  --flag        (bool flags only)
  --no-flag     (bool flags only)
  --flag=value  (non-bool flags only, no spaces around '=')
  --flag value  (non-bool flags only)
  --            (captures all remaining args in JavaScript)

Options:
# 打印生成的字节码
--print-bytecode (print bytecode generated by ignition interpreter)
        type: bool  default: --noprint-bytecode

	
# 跟踪被优化的信息
 --trace-opt (trace optimized compilation)
        type: bool  default: --notrace-opt
--trace-opt-verbose (extra verbose optimized compilation tracing)
        type: bool  default: --notrace-opt-verbose
--trace-opt-stats (trace optimized compilation statistics)
        type: bool  default: --notrace-opt-stats

# 跟踪去优化的信息
--trace-deopt (trace deoptimization)
        type: bool  default: --notrace-deopt
--log-deopt (log deoptimization)
        type: bool  default: --nolog-deopt
--trace-deopt-verbose (extra verbose deoptimization tracing)
        type: bool  default: --notrace-deopt-verbose
--print-deopt-stress (print number of possible deopt points)

	
# 查看编译生成的 AST
--print-ast (print source AST)
        type: bool  default: --noprint-ast

# 查看编译生成的代码
--print-code (print generated code)
        type: bool  default: --noprint-code

# 查看优化后的代码
--print-opt-code (print optimized code)
        type: bool  default: --noprint-opt-code

# 允许在源代码中使用 V8 提供的原生 API 语法
--allow-natives-syntax (allow natives syntax)
        type: bool  default: --noallow-natives-syntax
复制代码

在使用 jsvu 安装 v8-debug 之后,新建 index.js 文件,并写入如下代码:

// 需要给代码的结尾加上分号,否则编译会出错
class Test {
    constructor(length) {
      	// 命名属性
        for (let i = 0; i < length; i++) { this[`string${i}`] = `string${i}`; } 
      	// 元素属性  
      	for (let i = 0; i < length; i++) { this[i] = `number${i}`; }
    }
}
const test = new Test(15);

// V8 提供的内置 API,需要通过 --allow-natives-syntax 才能使用
%DebugPrint(test);
复制代码

温馨提示:%DebugPrint 是 V8 提供的内置 API,关于 V8 内置 API 的介绍可以查看 Built-in functions。除此之外,如果想了解更多的内置 API,则可以查看 src/runtime/runtime.h

执行编译命令 v8-debug --allow-natives-syntax ./index.js 后,输出如下信息:

DebugPrint: 0x2cd20810a655: [JS_OBJECT_TYPE]
 // 隐藏类指针
 - map: 0x2cd2082c7e61 <Map(HOLEY_ELEMENTS)> [FastProperties]
 // 原型属性指针
 - prototype: 0x2cd20810a589 <Test map = 0x2cd2082c7e89>
 // 元素属性指针  
 - elements: 0x2cd20810ad61 <FixedArray[17]> [HOLEY_ELEMENTS]
 // 命名属性指针
 - properties: 0x2cd20810ad05 <PropertyArray[6]>
   
   
 - All own properties (excluding elements): {
    // 对象内属性详细信息
    // 注意查看最后: location: in-object
    0x2cd2082936d5: [String] in OldSpace: #string0: 0x2cd20810a69d <String[7]: "string0"> (const data field 0), location: in-object
    0x2cd20829370d: [String] in OldSpace: #string1: 0x2cd20810a6fd <String[7]: "string1"> (const data field 1), location: in-object
    0x2cd208293731: [String] in OldSpace: #string2: 0x2cd20810a74d <String[7]: "string2"> (const data field 2), location: in-object
    0x2cd208293755: [String] in OldSpace: #string3: 0x2cd20810a7a9 <String[7]: "string3"> (const data field 3), location: in-object
    0x2cd208293779: [String] in OldSpace: #string4: 0x2cd20810a811 <String[7]: "string4"> (const data field 4), location: in-object
    0x2cd20829379d: [String] in OldSpace: #string5: 0x2cd20810a885 <String[7]: "string5"> (const data field 5), location: in-object
    0x2cd2082937c1: [String] in OldSpace: #string6: 0x2cd20810a905 <String[7]: "string6"> (const data field 6), location: in-object
    0x2cd2082937e5: [String] in OldSpace: #string7: 0x2cd20810a991 <String[7]: "string7"> (const data field 7), location: in-object
    0x2cd208293809: [String] in OldSpace: #string8: 0x2cd20810aa29 <String[7]: "string8"> (const data field 8), location: in-object
    0x2cd20829382d: [String] in OldSpace: #string9: 0x2cd20810aad9 <String[7]: "string9"> (const data field 9), location: in-object
    // 命名属性详细信息
    // 注意查看最后:location: properties[0]
    0x2cd208293851: [String] in OldSpace: #string10: 0x2cd20810ab01 <String[8]: "string10"> (const data field 10), location: properties[0]
    0x2cd208293875: [String] in OldSpace: #string11: 0x2cd20810abdd <String[8]: "string11"> (const data field 11), location: properties[1]
    0x2cd208293899: [String] in OldSpace: #string12: 0x2cd20810ac05 <String[8]: "string12"> (const data field 12), location: properties[2]
    0x2cd2082938bd: [String] in OldSpace: #string13: 0x2cd20810acf1 <String[8]: "string13"> (const data field 13), location: properties[3]
    0x2cd2082938e1: [String] in OldSpace: #string14: 0x2cd20810ad39 <String[8]: "string14"> (const data field 14), location: properties[4]
 }
 // 元素属性详细信息
 - elements: 0x2cd20810ad61 <FixedArray[17]> {
           0: 0x2cd20810ad4d <String[7]: "number0">
           1: 0x2cd20810adad <String[7]: "number1">
           2: 0x2cd20810adc1 <String[7]: "number2">
           3: 0x2cd20810ae09 <String[7]: "number3">
           4: 0x2cd20810ae1d <String[7]: "number4">
           5: 0x2cd20810ae31 <String[7]: "number5">
           6: 0x2cd20810ae45 <String[7]: "number6">
           7: 0x2cd20810ae59 <String[7]: "number7">
           8: 0x2cd20810ae6d <String[7]: "number8">
           9: 0x2cd20810ae81 <String[7]: "number9">
          10: 0x2cd20810ae95 <String[8]: "number10">
          11: 0x2cd20810aea9 <String[8]: "number11">
          12: 0x2cd20810aebd <String[8]: "number12">
          13: 0x2cd20810aed1 <String[8]: "number13">
          14: 0x2cd20810aee5 <String[8]: "number14">
       15-16: 0x2cd20800242d <the_hole>
 }
       
// 隐藏类详细信息
0x2cd2082c7e61: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 52
 - inobject properties: 10
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 1
 - enum length: invalid
 - stable_map
 - back pointer: 0x2cd2082c7e39 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x2cd208293705 <Cell value= 1>
 - instance descriptors (own) #15: 0x2cd20810ac19 <DescriptorArray[15]>
 - prototype: 0x2cd20810a589 <Test map = 0x2cd2082c7e89>
 - constructor: 0x2cd20810a569 <JSFunction Test (sfi = 0x2cd208293469)>
 - dependent code: 0x2cd2080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 6
复制代码

通过上述的打印信息可以将 V8 编译的对象属性结构进行简单整理,如下图所示:

JavaScript 的对象是属性和值的集合,看起来很跟字典类似,但是在实际存储的过程中会出于对性能的考虑而采用多种存储结构来完成对象属性的存储。如上图所示,JavaScript 对象在存储的过程中包含:

扫描二维码关注公众号,回复: 13670990 查看本文章
  • Elements:数组索引的属性通常被保存在独立的 Elements 中( array-indexed properties are stored in a separate elements store),被存储在线性的数据结构中,主要通过 Array.prototype 提供的方法(例如 popslice)来高效访问连续地址的属性值。需要注意的是在某些情况下存储的结构可能转换成字典形式,例如突然出现超大数组索引的稀疏数组,从而可以节省连续的存储带来的内存开销
  • Properties:命名的属性通常被存储在 Properties 中(named properties are stored in the properties store),跟 elements 不同,不能简单的通过健 key 来推断它们的属性在 properties 数组中的位置,往往需要一些额外的元信息。在 V8 中,会通过隐藏类(hidden class)来关联对象的命名属性以及对应值的形状(shape),在复杂的情况下会采用字典方式而不是简单的数组方式来存储数据

温馨提示:图中展示的 map 和 proto 分别是隐藏类(hidden class)和原型对象属性。Elements 和 Properties 的存储都可以是数组或者字典(elements and properties can either be arrays or dictionaries.),更多关于存储方式请自行了解线性和非线性的存储结构,包括链表、栈、队列、树以及图等。

隐藏类

在 C++ 语言等静态语言中声明一个对象需要定义该对象的具体形状(属性结构),代码在执行之前往往需要进行编译,编译后由于对象的形状固定,可以通过属性的固定指针偏移量来快速查询属性值,这是静态语言执行效率高的原因之一。在 JavaScript 动态语言中,对象在执行的过程中可以动态增删属性,一旦对象的形状被修改,往往无法通过偏移量来查找对象的属性值,因此对象属性值的查找会变得更加复杂。V8 在设计的过程中使用隐藏类将 JavaScript 的对象预置为静态对象来处理(假设对象是静态的,对象创建完之后不会添加新的属性,也不会进行属性的删除操作),并为每个对象属性设置值的指针偏移信息,从而可以快速查找对象的属性值。除此之外,我们知道对象的属性描述符中有很多信息,包括可读写(Writable)、可枚举(Enumerable)、可配置(Configurable)等,往往这些信息是固定不变的,使用隐藏类之后不需要为每个属性值描述这些信息,从而在语言的设计上一定程度节省了内存空间。

从调试示例中可以看出,隐藏类主要保存了对象相关的元信息,包括对象的属性数、原型属性信息以及属性描述符数组的指针信息等。隐藏类可以理解为 V8 中对象形状的标识符,是 V8 优化编译器 TurboFan 以及内联缓存的重要组成部分。隐藏类的描述符数组中包含了命名属性(注意不是元素属性)的信息,包括属性的名称以及属性值相对于对象本身的指针偏移量。属性的指针地址偏移信息有助于 V8 通过隐藏类对属性的值进行快速查找,从而提升查找对象命名属性的效率。最终,优化编译器 TurboFan 会直接访问内联属性,从而可以通过隐藏类来确保对象的结构兼容,可根据结构固定的属性偏移指针快速查找对象的属性值,并可以用于确定执行优化后的机器代码从而提升运行性能。

添加命名属性

为了更好的理解隐藏类本身的运行特性,这里将 index.js 中的代码进行调整,如下所示:

const obj1 = { a: 100, b: 200 };
const obj2 = { a: 100, b: 200 };
const obj3 = { a: 100, b: 200, c: 300 };

%DebugPrint(obj1);
%DebugPrint(obj2);
%DebugPrint(obj3);

obj1.c = 300;
%DebugPrint(obj1);

obj2.d = 400;
%DebugPrint(obj2);
复制代码

执行编译命令后打印的信息如下:

// 执行 index.js 脚本
v8-debug --allow-natives-syntax  ./index.js

// obj1 的打印信息
DebugPrint: 0x146c0810a53d: [JS_OBJECT_TYPE]
 // obj1 的隐藏类指针信息:0x146c082c7b91
 - map: 0x146c082c7b91 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x146c08284245 <Object map = 0x146c082c21b9>
 - elements: 0x146c0800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x146c0800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x146c0808fea5: [String] in ReadOnlySpace: #a: 100 (const data field 0), location: in-object
    0x146c0808ff41: [String] in ReadOnlySpace: #b: 200 (const data field 1), location: in-object
 }
0x146c082c7b91: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x146c082c7b69 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x146c082044fd <Cell value= 1>
 - instance descriptors (own) #2: 0x146c0810a56d <DescriptorArray[2]>
 - prototype: 0x146c08284245 <Object map = 0x146c082c21b9>
 - constructor: 0x146c08283e59 <JSFunction Object (sfi = 0x146c0820b0f1)>
 - dependent code: 0x146c080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

// obj2 的打印信息
DebugPrint: 0x146c0810a595: [JS_OBJECT_TYPE]
 // obj2 的隐藏类指针信息:0x146c082c7b91(和 obj1 的指针相同)
 - map: 0x146c082c7b91 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x146c08284245 <Object map = 0x146c082c21b9>
 - elements: 0x146c0800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x146c0800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x146c0808fea5: [String] in ReadOnlySpace: #a: 100 (const data field 0), location: in-object
    0x146c0808ff41: [String] in ReadOnlySpace: #b: 200 (const data field 1), location: in-object
 }
0x146c082c7b91: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x146c082c7b69 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x146c082044fd <Cell value= 1>
 - instance descriptors (own) #2: 0x146c0810a56d <DescriptorArray[2]>
 - prototype: 0x146c08284245 <Object map = 0x146c082c21b9>
 - constructor: 0x146c08283e59 <JSFunction Object (sfi = 0x146c0820b0f1)>
 - dependent code: 0x146c080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

// obj3 的打印信息
DebugPrint: 0x146c0810a5a9: [JS_OBJECT_TYPE]
 // obj3 的隐藏类指针信息:0x146c082c7c31(和 obj1、obj2 的指针不同)
 - map: 0x146c082c7c31 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x146c08284245 <Object map = 0x146c082c21b9>
 - elements: 0x146c0800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x146c0800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x146c0808fea5: [String] in ReadOnlySpace: #a: 100 (const data field 0), location: in-object
    0x146c0808ff41: [String] in ReadOnlySpace: #b: 200 (const data field 1), location: in-object
    0x146c082933f5: [String] in OldSpace: #c: 300 (const data field 2), location: in-object
 }
0x146c082c7c31: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 24
 - inobject properties: 3
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x146c082c7c09 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x146c082044fd <Cell value= 1>
 - instance descriptors (own) #3: 0x146c0810a605 <DescriptorArray[3]>
 - prototype: 0x146c08284245 <Object map = 0x146c082c21b9>
 - constructor: 0x146c08283e59 <JSFunction Object (sfi = 0x146c0820b0f1)>
 - dependent code: 0x146c080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

// obj1 添加属性 c 后的打印信息
DebugPrint: 0x146c0810a53d: [JS_OBJECT_TYPE]
 // obj1 的隐藏类指针信息:0x146c082c7c59(和 obj1 原有的隐藏类指针地址不同)
 - map: 0x146c082c7c59 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x146c08284245 <Object map = 0x146c082c21b9>
 - elements: 0x146c0800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x146c0810a66d <PropertyArray[3]>
 - All own properties (excluding elements): {
    0x146c0808fea5: [String] in ReadOnlySpace: #a: 100 (const data field 0), location: in-object
    0x146c0808ff41: [String] in ReadOnlySpace: #b: 200 (const data field 1), location: in-object
    0x146c082933f5: [String] in OldSpace: #c: 300 (const data field 2), location: properties[0]
 }
0x146c082c7c59: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 2
 - enum length: invalid
 - stable_map
 // obj1 新的隐藏类的尾指针指向了 obj1 旧的隐藏类的指针地址 0x146c082c7b91
 - back pointer: 0x146c082c7b91 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x146c08293601 <Cell value= 0>
 - instance descriptors (own) #3: 0x146c0810a639 <DescriptorArray[3]>
 - prototype: 0x146c08284245 <Object map = 0x146c082c21b9>
 - constructor: 0x146c08283e59 <JSFunction Object (sfi = 0x146c0820b0f1)>
 - dependent code: 0x146c080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

// obj2 添加属性 d 后的打印信息
DebugPrint: 0x146c0810a595: [JS_OBJECT_TYPE]
 // obj1 的隐藏类指针信息:0x146c082c7c81(和 obj2 原有的隐藏类指针地址不同)
 - map: 0x146c082c7c81 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x146c08284245 <Object map = 0x146c082c21b9>
 - elements: 0x146c0800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x146c0810a6b5 <PropertyArray[3]>
 - All own properties (excluding elements): {
    0x146c0808fea5: [String] in ReadOnlySpace: #a: 100 (const data field 0), location: in-object
    0x146c0808ff41: [String] in ReadOnlySpace: #b: 200 (const data field 1), location: in-object
    0x146c0829341d: [String] in OldSpace: #d: 400 (const data field 2), location: properties[0]
 }
0x146c082c7c81: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 2
 - enum length: invalid
 - stable_map
 // obj2 新的隐藏类的尾指针指向了 obj2 旧的隐藏类的指针地址 0x146c082c7b91
 - back pointer: 0x146c082c7b91 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x146c08293601 <Cell value= 0>
 - instance descriptors (own) #3: 0x146c0810a681 <DescriptorArray[3]>
 - prototype: 0x146c08284245 <Object map = 0x146c082c21b9>
 - constructor: 0x146c08283e59 <JSFunction Object (sfi = 0x146c0820b0f1)>
 - dependent code: 0x146c080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0
复制代码

观察上述对象的运行打印信息,可以发现 obj1 和 obj 2 的隐藏类表现大致如下图所示:

温馨提示:图中的 HC N 指代第 N 个隐藏类的指针地址信息,箭头指代两个 HC 之间的关联关系,例如 HC 0 指向了 HC 1,说明 HC 1 的 back pointer(隐藏类链表中上一个隐藏类的地址) 为 HC 0 的指针地址 (0x146c082c7b69)。

通过上图并结合打印信息,我们最终可以得出如下结论信息:

  • 每个 JavaScript 对象都有相应的隐藏类来记录对象的形状信息
  • 相同形状的 JavaScript 对象可以共用同一个隐藏类(节省隐藏类的存储空间和创建次数)
  • 每次给对象添加新的属性时,都会创建新的隐藏类,新隐藏类的尾指针会指向之前的隐藏类
  • V8 会创建一个将所有隐藏类链接在一起的转换树(Transition Tree),从而节省隐藏类的信息结构数据

删除命名属性

在真正设计代码的过程中,我们往往可能对 obj1 以及 obj2 进行属性删除操作,例如:

const obj1 = { a: 100, b: 200 };
%DebugPrint(obj1);

obj1.c = 300;
%DebugPrint(obj1);

delete obj1.b
%DebugPrint(obj1);
复制代码

执行编译后的打印信息如下:

v8-debug --allow-natives-syntax  ./index.js

// obj1 的打印信息
DebugPrint: 0x3e900810a4cd: [JS_OBJECT_TYPE]
 - map: 0x3e90082c7b91 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x3e9008284245 <Object map = 0x3e90082c21b9>
 - elements: 0x3e900800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x3e900800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x3e900808fea5: [String] in ReadOnlySpace: #a: 100 (const data field 0), location: in-object
    0x3e900808ff41: [String] in ReadOnlySpace: #b: 200 (const data field 1), location: in-object
 }
0x3e90082c7b91: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x3e90082c7b69 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x3e90082044fd <Cell value= 1>
 - instance descriptors (own) #2: 0x3e900810a4fd <DescriptorArray[2]>
 - prototype: 0x3e9008284245 <Object map = 0x3e90082c21b9>
 - constructor: 0x3e9008283e59 <JSFunction Object (sfi = 0x3e900820b0f1)>
 - dependent code: 0x3e90080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

// obj1 增加属性 c 后的打印信息
DebugPrint: 0x3e900810a4cd: [JS_OBJECT_TYPE]
 - map: 0x3e90082c7bb9 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x3e9008284245 <Object map = 0x3e90082c21b9>
 - elements: 0x3e900800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x3e900810a559 <PropertyArray[3]>
 - All own properties (excluding elements): {
    0x3e900808fea5: [String] in ReadOnlySpace: #a: 100 (const data field 0), location: in-object
    0x3e900808ff41: [String] in ReadOnlySpace: #b: 200 (const data field 1), location: in-object
    0x3e90082933ed: [String] in OldSpace: #c: 300 (const data field 2), location: properties[0]
 }
0x3e90082c7bb9: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 2
 - enum length: invalid
 - stable_map
 - back pointer: 0x3e90082c7b91 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x3e900829354d <Cell value= 0>
 - instance descriptors (own) #3: 0x3e900810a525 <DescriptorArray[3]>
 - prototype: 0x3e9008284245 <Object map = 0x3e90082c21b9>
 - constructor: 0x3e9008283e59 <JSFunction Object (sfi = 0x3e900820b0f1)>
 - dependent code: 0x3e90080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

// obj1 删除属性 b 后的打印信息
DebugPrint: 0x3e900810a4cd: [JS_OBJECT_TYPE]
 - map: 0x3e90082c56d9 <Map(HOLEY_ELEMENTS)> [DictionaryProperties]
 - prototype: 0x3e9008284245 <Object map = 0x3e90082c21b9>
 - elements: 0x3e900800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x3e900810a56d <NameDictionary[29]>
 - All own properties (excluding elements): {
   a: 100 (data, dict_index: 1, attrs: [WEC])
   c: 300 (data, dict_index: 3, attrs: [WEC])
 }
0x3e90082c56d9: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 12
 - inobject properties: 0
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - dictionary_map
 - may_have_interesting_symbols
 - back pointer: 0x3e90080023b5 <undefined>
 - prototype_validity cell: 0x3e90082044fd <Cell value= 1>
 - instance descriptors (own) #0: 0x3e90080021c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
 - prototype: 0x3e9008284245 <Object map = 0x3e90082c21b9>
 - constructor: 0x3e9008283e59 <JSFunction Object (sfi = 0x3e900820b0f1)>
 - dependent code: 0x3e90080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0
复制代码

重新来构建一下隐藏类的关系图,如下所示:

从上图可以发现,删除属性会创建新的隐藏类信息,创建的新的隐藏类与之前的隐藏类没有任何关联关系,所以对属性进行删除操作,可能会影响代码的运行性能。需要注意,如果 obj1 删除的是刚刚添加的属性 c,则会回退到 0x3e90082c7b91 指向的隐藏类(这里利用 back pointer 地址进行回退隐藏类的操作),并不会完全创建新的隐藏类。

设置命名属性的值为 null

如果不对属性进行删除操作,而是将属性值置为 null:

const obj1 = { a: 100, b: 200 };
%DebugPrint(obj1);

obj1.b = null;
%DebugPrint(obj1);
复制代码

此时输出的打印信息如下所示:

.jsvu % v8-debug --allow-natives-syntax  ./index.js

DebugPrint: 0x21f60810a4ad: [JS_OBJECT_TYPE]
 - map: 0x21f6082c7b91 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x21f608284245 <Object map = 0x21f6082c21b9>
 - elements: 0x21f60800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x21f60800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x21f60808fea5: [String] in ReadOnlySpace: #a: 100 (const data field 0), location: in-object
    0x21f60808ff41: [String] in ReadOnlySpace: #b: 200 (const data field 1), location: in-object
 }
0x21f6082c7b91: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x21f6082c7b69 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x21f6082044fd <Cell value= 1>
 - instance descriptors (own) #2: 0x21f60810a4dd <DescriptorArray[2]>
 - prototype: 0x21f608284245 <Object map = 0x21f6082c21b9>
 - constructor: 0x21f608283e59 <JSFunction Object (sfi = 0x21f60820b0f1)>
 - dependent code: 0x21f6080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

DebugPrint: 0x21f60810a4ad: [JS_OBJECT_TYPE]
 - map: 0x21f6082c7b91 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x21f608284245 <Object map = 0x21f6082c21b9>
 - elements: 0x21f60800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x21f60800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x21f60808fea5: [String] in ReadOnlySpace: #a: 100 (const data field 0), location: in-object
    0x21f60808ff41: [String] in ReadOnlySpace: #b: 0x21f608002235 <null> (data field 1), location: in-object
 }
0x21f6082c7b91: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x21f6082c7b69 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x21f6082044fd <Cell value= 1>
 - instance descriptors (own) #2: 0x21f60810a4dd <DescriptorArray[2]>
 - prototype: 0x21f608284245 <Object map = 0x21f6082c21b9>
 - constructor: 0x21f608283e59 <JSFunction Object (sfi = 0x21f60820b0f1)>
 - dependent code: 0x21f6080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0
复制代码

从上述打印信息可以发现将命名属性的字段设置为 null 不会改变原有的隐藏类,所以如果要对属性进行重置操作,将属性设置为 null 相对于删除属性而言可以提升 JavaScript 的运行性能。

添加元素属性

如果给对象操作的属性不是命名属性,而是元素属性,例如:

const obj1 = { a: 100, b: 200 };
%DebugPrint(obj1);

obj1[0] = 300;
obj1[1] = 400;
%DebugPrint(obj1);
复制代码
v8-debug --allow-natives-syntax  ./index.js

DebugPrint: 0x3d260810a4bd: [JS_OBJECT_TYPE]
 - map: 0x3d26082c7b91 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x3d2608284245 <Object map = 0x3d26082c21b9>
 - elements: 0x3d260800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x3d260800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x3d260808fea5: [String] in ReadOnlySpace: #a: 100 (const data field 0), location: in-object
    0x3d260808ff41: [String] in ReadOnlySpace: #b: 200 (const data field 1), location: in-object
 }
0x3d26082c7b91: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x3d26082c7b69 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x3d26082044fd <Cell value= 1>
 - instance descriptors (own) #2: 0x3d260810a4ed <DescriptorArray[2]>
 - prototype: 0x3d2608284245 <Object map = 0x3d26082c21b9>
 - constructor: 0x3d2608283e59 <JSFunction Object (sfi = 0x3d260820b0f1)>
 - dependent code: 0x3d26080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

DebugPrint: 0x3d260810a4bd: [JS_OBJECT_TYPE]
 - map: 0x3d26082c7b91 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x3d2608284245 <Object map = 0x3d26082c21b9>
 - elements: 0x3d260810a515 <FixedArray[17]> [HOLEY_ELEMENTS]
 - properties: 0x3d260800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x3d260808fea5: [String] in ReadOnlySpace: #a: 100 (const data field 0), location: in-object
    0x3d260808ff41: [String] in ReadOnlySpace: #b: 200 (const data field 1), location: in-object
 }
 - elements: 0x3d260810a515 <FixedArray[17]> {
           0: 300
           1: 400
        2-16: 0x3d260800242d <the_hole>
 }
0x3d26082c7b91: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x3d26082c7b69 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x3d26082044fd <Cell value= 1>
 - instance descriptors (own) #2: 0x3d260810a4ed <DescriptorArray[2]>
 - prototype: 0x3d2608284245 <Object map = 0x3d26082c21b9>
 - constructor: 0x3d2608283e59 <JSFunction Object (sfi = 0x3d260820b0f1)>
 - dependent code: 0x3d26080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0
复制代码

通过输出的打印信息可以发现,添加元素属性不会为对象创建新的隐藏类信息。

使用类或工厂函数创建对象

假设在开发的过程中使用工厂函数来创建对象,例如:

function createFactory(givenName, familyName) {
    // 这里可以做一些逻辑,但是要确保最终对象的命名属性的顺序和名称相同
    return {
        givenName,
        familyName,
        name: givenName + ' ' + familyName
    }
}

const benny = createFactory('jack',  'Benny');
%DebugPrint(benny);
const bruce = createFactory('jack', ' Bruce');
%DebugPrint(bruce);
const black = createFactory('jack', ' Black');
%DebugPrint(black);
复制代码
v8-debug --allow-natives-syntax  ./index.js

DebugPrint: 0xdb10810a769: [JS_OBJECT_TYPE]
 - map: 0x0db1082c7be1 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x0db108284245 <Object map = 0xdb1082c21b9>
 - elements: 0x0db10800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x0db10800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0xdb1082933e1: [String] in OldSpace: #givenName: 0x0db108293425 <String[4]: #jack> (const data field 0), location: in-object
    0xdb1082933f9: [String] in OldSpace: #familyName: 0x0db108293435 <String[5]: #Benny> (const data field 1), location: in-object
    0xdb108004dfd: [String] in ReadOnlySpace: #name: 0x0db10810a80d <String[10]: "jack Benny"> (const data field 2), location: in-object
 }
0xdb1082c7be1: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 24
 - inobject properties: 3
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x0db1082c7bb9 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x0db1082044fd <Cell value= 1>
 - instance descriptors (own) #3: 0x0db10810a7c5 <DescriptorArray[3]>
 - prototype: 0x0db108284245 <Object map = 0xdb1082c21b9>
 - constructor: 0x0db108283e59 <JSFunction Object (sfi = 0xdb10820b0f1)>
 - dependent code: 0x0db1080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

DebugPrint: 0xdb10810a825: [JS_OBJECT_TYPE]
 - map: 0x0db1082c7be1 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x0db108284245 <Object map = 0xdb1082c21b9>
 - elements: 0x0db10800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x0db10800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0xdb1082933e1: [String] in OldSpace: #givenName: 0x0db108293425 <String[4]: #jack> (const data field 0), location: in-object
    0xdb1082933f9: [String] in OldSpace: #familyName: 0x0db108293475 <String[6]: # Bruce> (const data field 1), location: in-object
    0xdb108004dfd: [String] in ReadOnlySpace: #name: 0x0db10810a851 <String[11]: "jack  Bruce"> (const data field 2), location: in-object
 }
0xdb1082c7be1: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 24
 - inobject properties: 3
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x0db1082c7bb9 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x0db1082044fd <Cell value= 1>
 - instance descriptors (own) #3: 0x0db10810a7c5 <DescriptorArray[3]>
 - prototype: 0x0db108284245 <Object map = 0xdb1082c21b9>
 - constructor: 0x0db108283e59 <JSFunction Object (sfi = 0xdb10820b0f1)>
 - dependent code: 0x0db1080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

DebugPrint: 0xdb10810a869: [JS_OBJECT_TYPE]
 - map: 0x0db1082c7be1 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x0db108284245 <Object map = 0xdb1082c21b9>
 - elements: 0x0db10800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x0db10800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0xdb1082933e1: [String] in OldSpace: #givenName: 0x0db108293425 <String[4]: #jack> (const data field 0), location: in-object
    0xdb1082933f9: [String] in OldSpace: #familyName: 0x0db10829349d <String[6]: # Black> (const data field 1), location: in-object
    0xdb108004dfd: [String] in ReadOnlySpace: #name: 0x0db10810a895 <String[11]: "jack  Black"> (const data field 2), location: in-object
 }
0xdb1082c7be1: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 24
 - inobject properties: 3
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x0db1082c7bb9 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x0db1082044fd <Cell value= 1>
 - instance descriptors (own) #3: 0x0db10810a7c5 <DescriptorArray[3]>
 - prototype: 0x0db108284245 <Object map = 0xdb1082c21b9>
 - constructor: 0x0db108283e59 <JSFunction Object (sfi = 0xdb10820b0f1)>
 - dependent code: 0x0db1080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0
复制代码

通过输出的打印信息可以发现,使用工厂函数生成的多个对象都指向了同一个隐藏类,说明在生成对象的过程中,采用相同的对象字面量初始化命名属性(相同的属性名称、相同的属性个数、相同的属性添加顺序),只生成了一个隐藏类。

代码实践指南

  • 尽量使用对象字面量一次性初始化完整的对象属性(只创建一个隐藏类,而不是形成隐藏类链条)
  • 尽量不要使用 delete 进行命名属性的删除操作,可以改用设置 null 的方式重置命名属性
  • 如果有很多相同的或者类似的对象需要生成,尽可能保证对象结构的一致性,推荐使用工厂函数,可以减少隐藏类的生成个数

命名属性

JavaScript 出于性能和内存考虑,在不同的情况下会对对象的属性采用不同的存储方式,深入了解对象属性如何根据使用方式来进行优化,有助于我们提高对象读写性能的优化,考虑以下数据声明:

  class Foo {
    constructor(length) {
      for (let i = 0; i < length; i++) { this[i] = `number${i}` } 
      for (let i = 0; i < length; i++) { this[`string${i}`] = `string${i}` } 
    }
  }
  const foo = new Foo(15)
复制代码

在隐藏类和元素属性的说明中,我们都采用了 v8 进行调试,其实如果觉得 jsuv 的安装相对比较麻烦,也可以通过 Chrome(81.0.4044.138)开发者工具 的 Memory 进行内存快照捕获,然后通过搜索找出 Foo 构造函数查看详情信息(可能没有 v8 调试的信息更详细),如下图所示:

命名属性通过 properties 进行访问时,往往需要进行二次访问。因为对象需要先索引 properties ,然后根据 properties 进行第二次索引。为了加快访问速度,V8 支持了一种对象内属性(In-object properties),这些属性直接存储在对象本身上,例如上图中的属性(string0 ~ string9)。需要注意的是,对象内属性的数量由对象的初始大小预先确定,通过 Chrome DevTools 调试的结果可以发现,上图中的数量是 10 个(大多数情况),而 string10 ~ string 14 存储在了 properties 中。

除此之外,从 string10 ~ string14 的存储结构可以看出,当数据量比较少的时候,默认采用连续地址的线性存储结构,往往会通过隐藏类来维护对象属性的信息结构。如果对对象进行大量的添加或删除属性操作,此时需要产生大量的时间和内存开销来维护属性描述符和隐藏类信息,因此 V8 会将存储的方式更改为字典存储,此时属性的元信息不会维护在对象属性的隐藏类信息结构中,而是直接保存在字典中。需要注意的是,内联缓存不适用于字典属性,因此存储在字典中的性能通常比线性存储的性能低,官方将采用字典存储方式(非线性存储)的属性称为慢属性,而线性存储结构的属性称为快属性。由于采用慢属性会降低代码的运行性能,因此 V8 会尽量避免采用慢属性进行属性值存储。

命名属性的三种类型

命名属性可以分为对象内属性和普通属性,对象内属性(In-object properties)不需要进行二次访问,直接存储于对象本身,访问的速度最快,但是存储的个数有限,往往根据对象的初始大小确定(基本上大部分开发可以满足这个大小需求)。而普通属性需要先所索引 properties,因此需要二次访问,性能相对于对象内属性会低一些。除此之外,普通属性又可以区分为快属性(Fast properties)和慢属性(Slow properties)。快属性采用线性的存储结构,并且属性值的指针偏移量地址等属性元信息存储在隐藏类的描述符数组中(往往是相同结构的对象共享元信息)。慢属性采用非线性的字典存储方式,所有属性的元信息不再保存在隐藏类中,而是各自单独保存在属性字节里。

添加大量属性

命名属性添加的个数较少时,会采用隐藏类会维护属性的元信息,此时属性都是快属性(FastProperties),采用的是线性的存储结构:

const obj = { a: 1, b: 2};
%DebugPrint(obj);

for(let i=0; i<10; i++) {
    obj[`string${i}`] = i;
}
%DebugPrint(obj);
复制代码
.jsvu % v8-debug --allow-natives-syntax ./index.js     

DebugPrint: 0xf7a0810a4cd: [JS_OBJECT_TYPE]
 // FastProperties 快属性
 - map: 0x0f7a082c7b91 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x0f7a08284245 <Object map = 0xf7a082c21b9>
 - elements: 0x0f7a0800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x0f7a0800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0xf7a0808fea5: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
    0xf7a0808ff41: [String] in ReadOnlySpace: #b: 2 (const data field 1), location: in-object
 }
0xf7a082c7b91: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x0f7a082c7b69 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x0f7a082044fd <Cell value= 1>
 - instance descriptors (own) #2: 0x0f7a0810a4fd <DescriptorArray[2]>
 - prototype: 0x0f7a08284245 <Object map = 0xf7a082c21b9>
 - constructor: 0x0f7a08283e59 <JSFunction Object (sfi = 0xf7a0820b0f1)>
 - dependent code: 0x0f7a080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

DebugPrint: 0xf7a0810a4cd: [JS_OBJECT_TYPE]
 - map: 0x0f7a082c7d21 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x0f7a08284245 <Object map = 0xf7a082c21b9>
 - elements: 0x0f7a0800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x0f7a0810a961 <PropertyArray[12]>
 - All own properties (excluding elements): {
    // location: in-object obj.a 是对象内属性
    0xf7a0808fea5: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
    // location: in-object obj.b 是对象内属性
    0xf7a0808ff41: [String] in ReadOnlySpace: #b: 2 (const data field 1), location: in-object
    0xf7a08293545: [String] in OldSpace: #string0: 0 (const data field 2), location: properties[0]
    0xf7a08293561: [String] in OldSpace: #string1: 1 (const data field 3), location: properties[1]
    0xf7a08293585: [String] in OldSpace: #string2: 2 (const data field 4), location: properties[2]
    0xf7a082935a9: [String] in OldSpace: #string3: 3 (const data field 5), location: properties[3]
    0xf7a082935cd: [String] in OldSpace: #string4: 4 (const data field 6), location: properties[4]
    0xf7a082935f1: [String] in OldSpace: #string5: 5 (const data field 7), location: properties[5]
    0xf7a08293615: [String] in OldSpace: #string6: 6 (const data field 8), location: properties[6]
    0xf7a08293639: [String] in OldSpace: #string7: 7 (const data field 9), location: properties[7]
    0xf7a0829365d: [String] in OldSpace: #string8: 8 (const data field 10), location: properties[8]
    0xf7a08293681: [String] in OldSpace: #string9: 9 (const data field 11), location: properties[9]
 }
0xf7a082c7d21: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 2
 - enum length: invalid
 - stable_map
 - back pointer: 0x0f7a082c7cf9 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x0f7a08293559 <Cell value= 0>
 - instance descriptors (own) #12: 0x0f7a0810a8ad <DescriptorArray[12]>
 - prototype: 0x0f7a08284245 <Object map = 0xf7a082c21b9>
 - constructor: 0x0f7a08283e59 <JSFunction Object (sfi = 0xf7a0820b0f1)>
 - dependent code: 0x0f7a080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0
复制代码

当命名属性添加的个数相对较多时,此时属性会从快属性转变成慢属性(字典属性),同时仔细观察隐藏类信息中的 instance descriptors,此时发现数量为 0,说明命名属性的元信息已经不在隐藏类中进行存储。转变为慢属性之后命名属性采用非线性的字典存储方式:

const obj = { a: 1, b: 2};
%DebugPrint(obj);
for(let i=0; i<100; i++) {
    obj[`string${i}`] = i;
}
%DebugPrint(obj);
复制代码
v8-debug --allow-natives-syntax ./index.js

DebugPrint: 0x19ac0810a4cd: [JS_OBJECT_TYPE]
 - map: 0x19ac082c7b91 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x19ac08284245 <Object map = 0x19ac082c21b9>
 - elements: 0x19ac0800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x19ac0800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x19ac0808fea5: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
    0x19ac0808ff41: [String] in ReadOnlySpace: #b: 2 (const data field 1), location: in-object
 }
0x19ac082c7b91: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x19ac082c7b69 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x19ac082044fd <Cell value= 1>
 - instance descriptors (own) #2: 0x19ac0810a4fd <DescriptorArray[2]>
 - prototype: 0x19ac08284245 <Object map = 0x19ac082c21b9>
 - constructor: 0x19ac08283e59 <JSFunction Object (sfi = 0x19ac0820b0f1)>
 - dependent code: 0x19ac080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

DebugPrint: 0x19ac0810a4cd: [JS_OBJECT_TYPE]
 // DictionaryProperties 字典属性
 - map: 0x19ac082c56d9 <Map(HOLEY_ELEMENTS)> [DictionaryProperties]
 - prototype: 0x19ac08284245 <Object map = 0x19ac082c21b9>
 - elements: 0x19ac0800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x19ac0810bc35 <NameDictionary[773]>
 - All own properties (excluding elements): {
   string24: 24 (data, dict_index: 27, attrs: [WEC])
   string57: 57 (data, dict_index: 60, attrs: [WEC])
   string94: 94 (data, dict_index: 97, attrs: [WEC])
   string22: 22 (data, dict_index: 25, attrs: [WEC])
   string6: 6 (data, dict_index: 9, attrs: [WEC])
   string2: 2 (data, dict_index: 5, attrs: [WEC])
   string85: 85 (data, dict_index: 88, attrs: [WEC])
   string15: 15 (data, dict_index: 18, attrs: [WEC])
   string38: 38 (data, dict_index: 41, attrs: [WEC])
   string59: 59 (data, dict_index: 62, attrs: [WEC])
   string66: 66 (data, dict_index: 69, attrs: [WEC])
   string3: 3 (data, dict_index: 6, attrs: [WEC])
   string13: 13 (data, dict_index: 16, attrs: [WEC])
   string84: 84 (data, dict_index: 87, attrs: [WEC])
   string62: 62 (data, dict_index: 65, attrs: [WEC])
   string91: 91 (data, dict_index: 94, attrs: [WEC])
   string8: 8 (data, dict_index: 11, attrs: [WEC])
   string43: 43 (data, dict_index: 46, attrs: [WEC])
   string34: 34 (data, dict_index: 37, attrs: [WEC])
   string5: 5 (data, dict_index: 8, attrs: [WEC])
   string27: 27 (data, dict_index: 30, attrs: [WEC])
   string80: 80 (data, dict_index: 83, attrs: [WEC])
   string72: 72 (data, dict_index: 75, attrs: [WEC])
   string97: 97 (data, dict_index: 100, attrs: [WEC])
   string55: 55 (data, dict_index: 58, attrs: [WEC])
   string78: 78 (data, dict_index: 81, attrs: [WEC])
   string75: 75 (data, dict_index: 78, attrs: [WEC])
   string69: 69 (data, dict_index: 72, attrs: [WEC])
   string53: 53 (data, dict_index: 56, attrs: [WEC])
   string73: 73 (data, dict_index: 76, attrs: [WEC])
   string42: 42 (data, dict_index: 45, attrs: [WEC])
   string48: 48 (data, dict_index: 51, attrs: [WEC])
   string81: 81 (data, dict_index: 84, attrs: [WEC])
   string68: 68 (data, dict_index: 71, attrs: [WEC])
   string46: 46 (data, dict_index: 49, attrs: [WEC])
   string88: 88 (data, dict_index: 91, attrs: [WEC])
   string95: 95 (data, dict_index: 98, attrs: [WEC])
   string28: 28 (data, dict_index: 31, attrs: [WEC])
   string51: 51 (data, dict_index: 54, attrs: [WEC])
   string63: 63 (data, dict_index: 66, attrs: [WEC])
   string23: 23 (data, dict_index: 26, attrs: [WEC])
   string32: 32 (data, dict_index: 35, attrs: [WEC])
   string9: 9 (data, dict_index: 12, attrs: [WEC])
   string98: 98 (data, dict_index: 101, attrs: [WEC])
   string44: 44 (data, dict_index: 47, attrs: [WEC])
   string11: 11 (data, dict_index: 14, attrs: [WEC])
   string30: 30 (data, dict_index: 33, attrs: [WEC])
   string76: 76 (data, dict_index: 79, attrs: [WEC])
   string36: 36 (data, dict_index: 39, attrs: [WEC])
   string18: 18 (data, dict_index: 21, attrs: [WEC])
   string0: 0 (data, dict_index: 3, attrs: [WEC])
   string25: 25 (data, dict_index: 28, attrs: [WEC])
   string49: 49 (data, dict_index: 52, attrs: [WEC])
   string37: 37 (data, dict_index: 40, attrs: [WEC])
   string61: 61 (data, dict_index: 64, attrs: [WEC])
   string14: 14 (data, dict_index: 17, attrs: [WEC])
   string21: 21 (data, dict_index: 24, attrs: [WEC])
   string45: 45 (data, dict_index: 48, attrs: [WEC])
   string77: 77 (data, dict_index: 80, attrs: [WEC])
   string58: 58 (data, dict_index: 61, attrs: [WEC])
   string71: 71 (data, dict_index: 74, attrs: [WEC])
   string89: 89 (data, dict_index: 92, attrs: [WEC])
   string16: 16 (data, dict_index: 19, attrs: [WEC])
   string33: 33 (data, dict_index: 36, attrs: [WEC])
   string4: 4 (data, dict_index: 7, attrs: [WEC])
   string19: 19 (data, dict_index: 22, attrs: [WEC])
   string99: 99 (data, dict_index: 102, attrs: [WEC])
   string65: 65 (data, dict_index: 68, attrs: [WEC])
   string74: 74 (data, dict_index: 77, attrs: [WEC])
   string56: 56 (data, dict_index: 59, attrs: [WEC])
   string60: 60 (data, dict_index: 63, attrs: [WEC])
   string7: 7 (data, dict_index: 10, attrs: [WEC])
   string86: 86 (data, dict_index: 89, attrs: [WEC])
   string39: 39 (data, dict_index: 42, attrs: [WEC])
   string40: 40 (data, dict_index: 43, attrs: [WEC])
   string54: 54 (data, dict_index: 57, attrs: [WEC])
   string83: 83 (data, dict_index: 86, attrs: [WEC])
   string92: 92 (data, dict_index: 95, attrs: [WEC])
   string93: 93 (data, dict_index: 96, attrs: [WEC])
   string64: 64 (data, dict_index: 67, attrs: [WEC])
   string79: 79 (data, dict_index: 82, attrs: [WEC])
   string10: 10 (data, dict_index: 13, attrs: [WEC])
   string52: 52 (data, dict_index: 55, attrs: [WEC])
   string20: 20 (data, dict_index: 23, attrs: [WEC])
   string50: 50 (data, dict_index: 53, attrs: [WEC])
   string70: 70 (data, dict_index: 73, attrs: [WEC])
   string17: 17 (data, dict_index: 20, attrs: [WEC])
   string12: 12 (data, dict_index: 15, attrs: [WEC])
   string26: 26 (data, dict_index: 29, attrs: [WEC])
   string90: 90 (data, dict_index: 93, attrs: [WEC])
   string31: 31 (data, dict_index: 34, attrs: [WEC])
   string96: 96 (data, dict_index: 99, attrs: [WEC])
   string29: 29 (data, dict_index: 32, attrs: [WEC])
   a: 1 (data, dict_index: 1, attrs: [WEC])
   string87: 87 (data, dict_index: 90, attrs: [WEC])
   string35: 35 (data, dict_index: 38, attrs: [WEC])
   string67: 67 (data, dict_index: 70, attrs: [WEC])
   string82: 82 (data, dict_index: 85, attrs: [WEC])
   string41: 41 (data, dict_index: 44, attrs: [WEC])
   string47: 47 (data, dict_index: 50, attrs: [WEC])
   b: 2 (data, dict_index: 2, attrs: [WEC])
   string1: 1 (data, dict_index: 4, attrs: [WEC])
 }
0x19ac082c56d9: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 12
 - inobject properties: 0
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - dictionary_map
 - may_have_interesting_symbols
 - back pointer: 0x19ac080023b5 <undefined>
 - prototype_validity cell: 0x19ac082044fd <Cell value= 1>
 // 隐藏类信息中属性描述符数组的个数为 0 
 - instance descriptors (own) #0: 0x19ac080021c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
 - prototype: 0x19ac08284245 <Object map = 0x19ac082c21b9>
 - constructor: 0x19ac08283e59 <JSFunction Object (sfi = 0x19ac0820b0f1)>
 - dependent code: 0x19ac080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0
复制代码

温馨提示:这上述这个示例中,测试添加超过 15 个命名属性后,属性会从快属性类型转变为字典类型,具体的临界值在源码内部应该是有一套算法规则的,这里没有细看,感兴趣的同学可以研究一下。

删除命名属性

在命名属性是快属性的情况下,删除命名属性和添加命名属性类似,会产生大量时间和内存开销来维护隐藏类和描述符数组,此时 V8 会将快属性降级为慢属性进行处理,此时由于无需更新隐藏类的信息,因此慢属性的删除和添加往往比较高效,但是访问的速度则比快属性和对象内属性慢:

const obj = { a: 1, b: 2};
%DebugPrint(obj);

for(let i=0; i<10; i++) {
    obj[`string${i}`] = i;
}
%DebugPrint(obj);

delete obj['string1'];
%DebugPrint(obj);
复制代码
v8-debug --allow-natives-syntax ./index.js

DebugPrint: 0x38e80810a4f5: [JS_OBJECT_TYPE]
 - map: 0x38e8082c7b91 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x38e808284245 <Object map = 0x38e8082c21b9>
 - elements: 0x38e80800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x38e80800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x38e80808fea5: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
    0x38e80808ff41: [String] in ReadOnlySpace: #b: 2 (const data field 1), location: in-object
 }
0x38e8082c7b91: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x38e8082c7b69 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x38e8082044fd <Cell value= 1>
 - instance descriptors (own) #2: 0x38e80810a525 <DescriptorArray[2]>
 - prototype: 0x38e808284245 <Object map = 0x38e8082c21b9>
 - constructor: 0x38e808283e59 <JSFunction Object (sfi = 0x38e80820b0f1)>
 - dependent code: 0x38e8080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

DebugPrint: 0x38e80810a4f5: [JS_OBJECT_TYPE]
 - map: 0x38e8082c7d21 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x38e808284245 <Object map = 0x38e8082c21b9>
 - elements: 0x38e80800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x38e80810a989 <PropertyArray[12]>
 - All own properties (excluding elements): {
    0x38e80808fea5: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
    0x38e80808ff41: [String] in ReadOnlySpace: #b: 2 (const data field 1), location: in-object
    0x38e80829356d: [String] in OldSpace: #string0: 0 (const data field 2), location: properties[0]
    0x38e8082933ed: [String] in OldSpace: #string1: 1 (const data field 3), location: properties[1]
    0x38e808293599: [String] in OldSpace: #string2: 2 (const data field 4), location: properties[2]
    0x38e8082935bd: [String] in OldSpace: #string3: 3 (const data field 5), location: properties[3]
    0x38e8082935e1: [String] in OldSpace: #string4: 4 (const data field 6), location: properties[4]
    0x38e808293605: [String] in OldSpace: #string5: 5 (const data field 7), location: properties[5]
    0x38e808293629: [String] in OldSpace: #string6: 6 (const data field 8), location: properties[6]
    0x38e80829364d: [String] in OldSpace: #string7: 7 (const data field 9), location: properties[7]
    0x38e808293671: [String] in OldSpace: #string8: 8 (const data field 10), location: properties[8]
    0x38e808293695: [String] in OldSpace: #string9: 9 (const data field 11), location: properties[9]
 }
0x38e8082c7d21: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 2
 - enum length: invalid
 - stable_map
 - back pointer: 0x38e8082c7cf9 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x38e808293581 <Cell value= 0>
 - instance descriptors (own) #12: 0x38e80810a8d5 <DescriptorArray[12]>
 - prototype: 0x38e808284245 <Object map = 0x38e8082c21b9>
 - constructor: 0x38e808283e59 <JSFunction Object (sfi = 0x38e80820b0f1)>
 - dependent code: 0x38e8080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

DebugPrint: 0x38e80810a4f5: [JS_OBJECT_TYPE]
 // 删除属性操作后将快属性降级为字典属性(慢属性)
 - map: 0x38e8082c56d9 <Map(HOLEY_ELEMENTS)> [DictionaryProperties]
 - prototype: 0x38e808284245 <Object map = 0x38e8082c21b9>
 - elements: 0x38e80800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x38e80810a9c1 <NameDictionary[101]>
 - All own properties (excluding elements): {
   string2: 2 (data, dict_index: 5, attrs: [WEC])
   string8: 8 (data, dict_index: 11, attrs: [WEC])
   string5: 5 (data, dict_index: 8, attrs: [WEC])
   string7: 7 (data, dict_index: 10, attrs: [WEC])
   string0: 0 (data, dict_index: 3, attrs: [WEC])
   string6: 6 (data, dict_index: 9, attrs: [WEC])
   string9: 9 (data, dict_index: 12, attrs: [WEC])
   a: 1 (data, dict_index: 1, attrs: [WEC])
   string3: 3 (data, dict_index: 6, attrs: [WEC])
   string4: 4 (data, dict_index: 7, attrs: [WEC])
   b: 2 (data, dict_index: 2, attrs: [WEC])
 }
0x38e8082c56d9: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 12
 - inobject properties: 0
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - dictionary_map
 - may_have_interesting_symbols
 - back pointer: 0x38e8080023b5 <undefined>
 - prototype_validity cell: 0x38e8082044fd <Cell value= 1>
 - instance descriptors (own) #0: 0x38e8080021c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
 - prototype: 0x38e808284245 <Object map = 0x38e8082c21b9>
 - constructor: 0x38e808283e59 <JSFunction Object (sfi = 0x38e80820b0f1)>
 - dependent code: 0x38e8080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0
复制代码

当我们不进行删除命名属性的操作,而是改用将属性值设置为 null 或者 undefined 处理,则不会改变属性的存储方式。

代码最佳实践

  • 尽量不要对命名属性进行删除操作,可以将属性值设置为 null 或者 undefined 处理
  • 尽量在分配对象的时候进行细粒度的切分(不要在一个对象上添加大量属性),确保对象内属性(In-object properties)可以满足开发的诉求,从而提升运行性能

元素属性

元素属性和命名属性各自存储在独立的数据结构中,因此两者在添加、删除以及访问属性时的性能会有差异。元素(文中的数组可以理解为元素)可以使用各种 Array.prototype 方法(例如 popslice),并且这些方法一般都适用于访问连续地址的属性,因此在 V8 内部一般将元素采用线性的数组存储模式。线性结构的存储模式可以使得属性的访问通过一对一的线性关系快速查找到属性值的指针地址,因此元素属性通常不需要像命名属性那样需要通过隐藏类来维护属性的指针偏移地址信息。当然,因为元素的种类很多,并不是所有的元素都采用线性的存储结构,需要根据具体的元素种类进行具体分析。

温馨提示:这里所说的线性结构是指元素和数据之间存在一对一的线性关系,例如数组(顺序存储结构)、链表(链式存储结构)、队列以及栈。非线性结构是指元素和数据之间存在一对多的关系,例如二维或多维数组、树、图、HashMap(数组和链表的结合,外围采用数组,而内部使用链表) 、HashTable 等。后续在 JavaScript 中讲解的非线性结构基本上都是采用 Hash 存储,如果你对 Hash 不了解则可以查看 漫画:什么是 HashMap?

元素种类

我们来看一段示例代码:

const array = [1, 2, 3];
%DebugPrint(array);

array.push(4.56);
%DebugPrint(array);

array.push('x');
%DebugPrint(array);

// 转变为稀疏数组
array[10] = 10;
%DebugPrint(array);

// 转变为密集数组
delete array[10];
%DebugPrint(array);
复制代码

查看打印信息如下:

v8-debug --allow-natives-syntax ./index.js

DebugPrint: 0x1f660810a519: [JSArray]
 - map: 0x1f66082c3a41 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x1f660828c139 <JSArray[0]>
 - elements: 0x1f660829345d <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
 - length: 3
 - properties: 0x1f660800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x1f6608004bb5: [String] in ReadOnlySpace: #length: 0x1f6608204255 <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x1f660829345d <FixedArray[3]> {
           0: 1
           1: 2
           2: 3
 }
0x1f66082c3a41: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 // const array = [1, 2, 3];
 // 元素种类:PACKED_SMI_ELEMENTS
 - elements kind: PACKED_SMI_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x1f66080023b5 <undefined>
 - prototype_validity cell: 0x1f66082044fd <Cell value= 1>
 - instance descriptors #1: 0x1f660828c5ed <DescriptorArray[1]>
 - transitions #1: 0x1f660828c609 <TransitionArray[4]>Transition array #1:
     0x1f66080057c9 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x1f66082c3ab9 <Map(HOLEY_SMI_ELEMENTS)>

 - prototype: 0x1f660828c139 <JSArray[0]>
 - constructor: 0x1f660828bed5 <JSFunction Array (sfi = 0x1f6608210501)>
 - dependent code: 0x1f66080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

DebugPrint: 0x1f660810a519: [JSArray]
 - map: 0x1f66082c3ae1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
 - prototype: 0x1f660828c139 <JSArray[0]>
 - elements: 0x1f660810a589 <FixedDoubleArray[22]> [PACKED_DOUBLE_ELEMENTS]
 - length: 4
 - properties: 0x1f660800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x1f6608004bb5: [String] in ReadOnlySpace: #length: 0x1f6608204255 <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x1f660810a589 <FixedDoubleArray[22]> {
           0: 1
           1: 2
           2: 3
           3: 4.56
        4-21: <the_hole>
 }
0x1f66082c3ae1: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 // array.push(4.56);
 // 元素种类:PACKED_DOUBLE_ELEMENTS
 - elements kind: PACKED_DOUBLE_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x1f66082c3ab9 <Map(HOLEY_SMI_ELEMENTS)>
 - prototype_validity cell: 0x1f66082044fd <Cell value= 1>
 - instance descriptors #1: 0x1f660828c5ed <DescriptorArray[1]>
 - transitions #1: 0x1f660828c639 <TransitionArray[4]>Transition array #1:
     0x1f66080057c9 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x1f66082c3b09 <Map(HOLEY_DOUBLE_ELEMENTS)>

 - prototype: 0x1f660828c139 <JSArray[0]>
 - constructor: 0x1f660828bed5 <JSFunction Array (sfi = 0x1f6608210501)>
 - dependent code: 0x1f66080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

DebugPrint: 0x1f660810a519: [JSArray]
 - map: 0x1f66082c3b31 <Map(PACKED_ELEMENTS)> [FastProperties]
 - prototype: 0x1f660828c139 <JSArray[0]>
 - elements: 0x1f660810a641 <FixedArray[22]> [PACKED_ELEMENTS]
 - length: 5
 - properties: 0x1f660800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x1f6608004bb5: [String] in ReadOnlySpace: #length: 0x1f6608204255 <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x1f660810a641 <FixedArray[22]> {
           0: 0x1f660810a6c5 <HeapNumber 1.0>
           1: 0x1f660810a6b9 <HeapNumber 2.0>
           2: 0x1f660810a6ad <HeapNumber 3.0>
           3: 0x1f660810a6a1 <HeapNumber 4.56>
           4: 0x1f66082933f1 <String[1]: #x>
        5-21: 0x1f660800242d <the_hole>
 }
0x1f66082c3b31: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 // array.push('x');
 // 元素种类: PACKED_ELEMENTS
 - elements kind: PACKED_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x1f66082c3b09 <Map(HOLEY_DOUBLE_ELEMENTS)>
 - prototype_validity cell: 0x1f66082044fd <Cell value= 1>
 - instance descriptors #1: 0x1f660828c5ed <DescriptorArray[1]>
 - transitions #1: 0x1f660828c669 <TransitionArray[4]>Transition array #1:
     0x1f66080057c9 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_ELEMENTS) -> 0x1f66082c3b59 <Map(HOLEY_ELEMENTS)>

 - prototype: 0x1f660828c139 <JSArray[0]>
 - constructor: 0x1f660828bed5 <JSFunction Array (sfi = 0x1f6608210501)>
 - dependent code: 0x1f66080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

DebugPrint: 0x1f660810a519: [JSArray]
 - map: 0x1f66082c3b59 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1f660828c139 <JSArray[0]>
 - elements: 0x1f660810a641 <FixedArray[22]> [HOLEY_ELEMENTS]
 - length: 11
 - properties: 0x1f660800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x1f6608004bb5: [String] in ReadOnlySpace: #length: 0x1f6608204255 <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x1f660810a641 <FixedArray[22]> {
           0: 0x1f660810a6c5 <HeapNumber 1.0>
           1: 0x1f660810a6b9 <HeapNumber 2.0>
           2: 0x1f660810a6ad <HeapNumber 3.0>
           3: 0x1f660810a6a1 <HeapNumber 4.56>
           4: 0x1f66082933f1 <String[1]: #x>
         5-9: 0x1f660800242d <the_hole>
          10: 10
       11-21: 0x1f660800242d <the_hole>
 }
0x1f66082c3b59: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 // array[10] = 10;
 // 元素种类:HOLEY_ELEMENTS
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x1f66082c3b31 <Map(PACKED_ELEMENTS)>
 - prototype_validity cell: 0x1f66082044fd <Cell value= 1>
 - instance descriptors (own) #1: 0x1f660828c5ed <DescriptorArray[1]>
 - prototype: 0x1f660828c139 <JSArray[0]>
 - constructor: 0x1f660828bed5 <JSFunction Array (sfi = 0x1f6608210501)>
 - dependent code: 0x1f66080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

DebugPrint: 0x1f660810a519: [JSArray]
 - map: 0x1f66082c3b59 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1f660828c139 <JSArray[0]>
 - elements: 0x1f660810a641 <FixedArray[22]> [HOLEY_ELEMENTS]
 - length: 11
 - properties: 0x1f660800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x1f6608004bb5: [String] in ReadOnlySpace: #length: 0x1f6608204255 <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x1f660810a641 <FixedArray[22]> {
           0: 0x1f660810a6c5 <HeapNumber 1.0>
           1: 0x1f660810a6b9 <HeapNumber 2.0>
           2: 0x1f660810a6ad <HeapNumber 3.0>
           3: 0x1f660810a6a1 <HeapNumber 4.56>
           4: 0x1f66082933f1 <String[1]: #x>
        5-21: 0x1f660800242d <the_hole>
 }
0x1f66082c3b59: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 // delete array[10];
 //  元素种类:HOLEY_ELEMENTS
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x1f66082c3b31 <Map(PACKED_ELEMENTS)>
 - prototype_validity cell: 0x1f66082044fd <Cell value= 1>
 - instance descriptors (own) #1: 0x1f660828c5ed <DescriptorArray[1]>
 - prototype: 0x1f660828c139 <JSArray[0]>
 - constructor: 0x1f660828bed5 <JSFunction Array (sfi = 0x1f6608210501)>
 - dependent code: 0x1f66080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0
复制代码

从打印的信息可以发现代码中已经出现了如下几种元素类型:

  • PACKED_SMI_ELEMENTS:小整数(Small Intergers)
  • PACKED_DOUBLE_ELEMENTS:双精度浮点数(熟悉 C 语言的同学应该非常清楚 float 和 double)
  • PACKED_ELEMENTS:常规元素(不能表示任一一种其他类型)
  • HOLEY_ELEMENTS:稀疏的常规元素

除此之外, V8 实际上有 21 种元素类型,对于不同的种类的元素进行的优化手段可能不相同。从上述打印信息还可以发现:

  • Chrome V8 会为每个元素分配一个元素种类
  • 数组的种类可以分为稀疏数组(PACKED)和密集数组(HOLEY)
  • 元素种类在运行的过程中可以改变,但是只能从特定种类转换为一般种类,转换的过程不可逆

例如上述的稀疏数组经过 delete array[10] 操作后又变成了密集数组,但是元素种类仍然是 HOLEY_ELEMENTS,并不会回退为 PACKED_ELEMENTS。HOLEY 相对于 PACKED 更为特定,同样的 SMI 相对于 DOUBLE 更为特定。

密集(PACKED)元素、稀疏(HOLEY)元素和字典(DICTIONARY)元素

稀疏元素的相对于密集元素的执行效率会更低,例如以下示例代码:

const array = [1, 2, 3];
%DebugPrint(array);

array[6] = 10000;
%DebugPrint(array);
复制代码
v8-debug --allow-natives-syntax ./index.js

DebugPrint: 0x25970810a4a9: [JSArray]
 - map: 0x2597082c3a41 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x25970828c139 <JSArray[0]>
 - elements: 0x25970829344d <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
 - length: 3
 - properties: 0x25970800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x259708004bb5: [String] in ReadOnlySpace: #length: 0x259708204255 <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x25970829344d <FixedArray[3]> {
           0: 1
           1: 2
           2: 3
 }
0x2597082c3a41: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - elements kind: PACKED_SMI_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x2597080023b5 <undefined>
 - prototype_validity cell: 0x2597082044fd <Cell value= 1>
 - instance descriptors #1: 0x25970828c5ed <DescriptorArray[1]>
 - transitions #1: 0x25970828c609 <TransitionArray[4]>Transition array #1:
     0x2597080057c9 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x2597082c3ab9 <Map(HOLEY_SMI_ELEMENTS)>

 - prototype: 0x25970828c139 <JSArray[0]>
 - constructor: 0x25970828bed5 <JSFunction Array (sfi = 0x259708210501)>
 - dependent code: 0x2597080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

DebugPrint: 0x25970810a4a9: [JSArray]
 - map: 0x2597082c3ab9 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x25970828c139 <JSArray[0]>
 - elements: 0x25970810a4b9 <FixedArray[26]> [HOLEY_SMI_ELEMENTS]
 - length: 7
 - properties: 0x25970800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x259708004bb5: [String] in ReadOnlySpace: #length: 0x259708204255 <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x25970810a4b9 <FixedArray[26]> {
           0: 1
           1: 2
           2: 3
   			 // 3-5 是稀疏的,浪费内存空间
         3-5: 0x25970800242d <the_hole>
           6: 10000
        7-25: 0x25970800242d <the_hole>
 }
0x2597082c3ab9: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - elements kind: HOLEY_SMI_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x2597082c3a41 <Map(PACKED_SMI_ELEMENTS)>
 - prototype_validity cell: 0x2597082044fd <Cell value= 1>
 - instance descriptors #1: 0x25970828c5ed <DescriptorArray[1]>
 - transitions #1: 0x25970828c621 <TransitionArray[4]>Transition array #1:
     0x2597080057c9 <Symbol: (elements_transition_symbol)>: (transition to PACKED_DOUBLE_ELEMENTS) -> 0x2597082c3ae1 <Map(PACKED_DOUBLE_ELEMENTS)>

 - prototype: 0x25970828c139 <JSArray[0]>
 - constructor: 0x25970828bed5 <JSFunction Array (sfi = 0x259708210501)>
 - dependent code: 0x2597080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0
复制代码

因为 HOLEY 类型的稀疏元素在遍历时会对原型链进行额外的检查和昂贵的查找,并且大家可以发现从密集元素变化为稀疏元素后隐藏类保持了链式的结构。当稀疏的跨度较大时,Chrome V8 会更改数组元素的存储方式,将线性的存储结构转变为非线性的存储结构,从而节省连续地址的存储带来的内存消耗(例如上述的 3-5 就是浪费的内存空间),例如:

const array = [1, 2, 3];
%DebugPrint(array);

array[10000] = 10000;
%DebugPrint(array);
复制代码
v8-debug --allow-natives-syntax ./index.js

DebugPrint: 0x30cc0810a4ad: [JSArray]
 - map: 0x30cc082c3a41 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x30cc0828c139 <JSArray[0]>
 - elements: 0x30cc0829344d <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
 - length: 3
 - properties: 0x30cc0800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x30cc08004bb5: [String] in ReadOnlySpace: #length: 0x30cc08204255 <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x30cc0829344d <FixedArray[3]> {
           0: 1
           1: 2
           2: 3
 }
0x30cc082c3a41: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - elements kind: PACKED_SMI_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x30cc080023b5 <undefined>
 - prototype_validity cell: 0x30cc082044fd <Cell value= 1>
 - instance descriptors #1: 0x30cc0828c5ed <DescriptorArray[1]>
 - transitions #1: 0x30cc0828c609 <TransitionArray[4]>Transition array #1:
     0x30cc080057c9 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x30cc082c3ab9 <Map(HOLEY_SMI_ELEMENTS)>

 - prototype: 0x30cc0828c139 <JSArray[0]>
 - constructor: 0x30cc0828bed5 <JSFunction Array (sfi = 0x30cc08210501)>
 - dependent code: 0x30cc080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

DebugPrint: 0x30cc0810a4ad: [JSArray]
 - map: 0x30cc082c7b69 <Map(DICTIONARY_ELEMENTS)> [FastProperties]
 - prototype: 0x30cc0828c139 <JSArray[0]>  
 - elements: 0x30cc0810a505 <NumberDictionary[28]> [DICTIONARY_ELEMENTS]
 - length: 10001
 - properties: 0x30cc0800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x30cc08004bb5: [String] in ReadOnlySpace: #length: 0x30cc08204255 <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 // 字典存储模式
 - elements: 0x30cc0810a505 <NumberDictionary[28]> {
   - max_number_key: 10000
   0: 1 (data, dict_index: 0, attrs: [WEC])
   2: 3 (data, dict_index: 0, attrs: [WEC])
   10000: 10000 (data, dict_index: 0, attrs: [WEC])
   1: 2 (data, dict_index: 0, attrs: [WEC])
 }
0x30cc082c7b69: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - elements kind: DICTIONARY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x30cc082c3b59 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x30cc082044fd <Cell value= 1>
 - instance descriptors (own) #1: 0x30cc0828c5ed <DescriptorArray[1]>
 - prototype: 0x30cc0828c139 <JSArray[0]>
 - constructor: 0x30cc0828bed5 <JSFunction Array (sfi = 0x30cc08210501)>
 - dependent code: 0x30cc080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0
复制代码

可以发现元素的种类从 PACKED_SMI_ELEMENTS 转变为 DICTIONARY_ELEMENTS,存储模式从线性的连续地址存储更改为非线性的存储模式,并且重新创建了隐藏类,和原有的密集型元素的隐藏类没有任何关联关系。事实上,元素可以分为快元素(特定种类的元素)和慢元素(字典元素),上述中的的字典元素会存储元素的健、值以及描述符信息(需要注意此时不是存储在隐藏类的属性元信息中),在上面这个例子中健是 '10000' 的元素属性会建立默认的描述符属性,因此如果使用自定义描述符去定义元素属性,那么 V8 会将元素降级为慢元素。

需要注意的是,两种存储模式的访问性能需要依据元素的个数而定,当元素数量较大时,采用线性的存储模式如果要读取第 N 个索引的数据,需要要运算 N 次,而采用 Hash 存储模式下,可能运算的次数小于 N 次,尽管它需要进行二次查找(假设哈希表中的数据分布相对均匀)。当然在数据量较小的情况下,Hash 因为需要进行二次查找,且第一次查找要计算哈希索引从而导致性能不如只需要一次线性查找的性能来的快。

使用描述符定义元素属性

示例代码如下所示:

const array = [1, 2, 3];
%DebugPrint(array);

Object.defineProperty(array, '4', {
    value: 4,
    enumerable:false,
    configurable: false,
    writable: false
});
%DebugPrint(array);
复制代码
v8-debug --allow-natives-syntax ./index.js

DebugPrint: 0x2dd0810a511: [JSArray]
 - map: 0x02dd082c3a41 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x02dd0828c139 <JSArray[0]>
 - elements: 0x02dd08293489 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
 - length: 3
 - properties: 0x02dd0800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x2dd08004bb5: [String] in ReadOnlySpace: #length: 0x02dd08204255 <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x02dd08293489 <FixedArray[3]> {
           0: 1
           1: 2
           2: 3
 }
0x2dd082c3a41: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - elements kind: PACKED_SMI_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x02dd080023b5 <undefined>
 - prototype_validity cell: 0x02dd082044fd <Cell value= 1>
 - instance descriptors #1: 0x02dd0828c5ed <DescriptorArray[1]>
 - transitions #1: 0x02dd0828c609 <TransitionArray[4]>Transition array #1:
     0x02dd080057c9 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x02dd082c3ab9 <Map(HOLEY_SMI_ELEMENTS)>

 - prototype: 0x02dd0828c139 <JSArray[0]>
 - constructor: 0x02dd0828bed5 <JSFunction Array (sfi = 0x2dd08210501)>
 - dependent code: 0x02dd080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

DebugPrint: 0x2dd0810a511: [JSArray]
 - map: 0x02dd082c7c09 <Map(DICTIONARY_ELEMENTS)> [FastProperties]
 - prototype: 0x02dd0828c139 <JSArray[0]>
 - elements: 0x02dd0810a63d <NumberDictionary[28]> [DICTIONARY_ELEMENTS]
 - length: 5
 - properties: 0x02dd0800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x2dd08004bb5: [String] in ReadOnlySpace: #length: 0x02dd08204255 <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x02dd0810a63d <NumberDictionary[28]> {
   - requires_slow_elements
   2: 3 (data, dict_index: 0, attrs: [WEC])
   0: 1 (data, dict_index: 0, attrs: [WEC])
   1: 2 (data, dict_index: 0, attrs: [WEC])
   4: 4 (data, dict_index: 0, attrs: [___])
 }
0x2dd082c7c09: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - elements kind: DICTIONARY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x02dd082c3b59 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x02dd082044fd <Cell value= 1>
 - instance descriptors (own) #1: 0x02dd0828c5ed <DescriptorArray[1]>
 - prototype: 0x02dd0828c139 <JSArray[0]>
 - constructor: 0x02dd0828bed5 <JSFunction Array (sfi = 0x2dd08210501)>
 - dependent code: 0x02dd080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0
复制代码

可以发现通过属性描述符来定义元素属性会使得元素的存储结构发生变化,从线性结构退化为非线性的存储结构,并且会创建新的隐藏类信息。

温馨提示:想象一下 Vue 2 中使用 Object.defineProperty 对数组元素带来的副作用。

代码实践指南

由于精力有限,没有再过多研究元素属性的种类转换特性,这里根据上述的表现形式,做如下指导:

  • 避免创建稀疏数组,这会导致遍历的时候产生原型链搜索的性能损耗,而且稀疏数组永远会被标记为 HOLEY_?_ELEMENTS,这会对 V8 的优化带来不利影响
  • 尽量避免元素的种类转换,尽量保证元素种类为特定种类,例如 PACKED_SMI_ELEMENTS,这有利于 V8 做更好的性能优化,例如之前说描述的类型反馈(TypeFeedback)技术以及之后会描述的内联缓存(Inline Cache)技术
  • 一些密集的类数组对象,可以转换成真正的数组进行处理 Array.prototype.slice.call(arrayLike, 0)。在真正调用 Array.prototype.forEach 等内置的方法时,V8 会对元素做高度优化。

猜你喜欢

转载自juejin.im/post/7054363717403836424