对象与数组
V8 中对象与数组的存储
上世纪九十年代,随着网景浏览器的发行,

在
[ class / map ] -> ... ; 指向内部类
[ properties] -> [empty array]
[ elements] -> [empty array] ; 数值类型名称的属性
[ reserved #1 ] -\
[ reserved #2 ]|
[ reserved #3 ]}- in object properties,即预分配的内存空间
...............|
[ reserved #N ] -/
在创建新的对象时,this.field = expr
{ a: 1, b: 2, c: 3, d: 4}
对象的存储方式可能如下:
[ class ] -> [a: in obj #1, b: in obj #2, c: out obj #1, d: out obj #2]
[ properties] -> [3][4] ; this is linear array
[ elements]
[ 1 ]
[ 2 ]
随着属性数目的增加,
[ class ] -> [ OBJECT IS IN DICTIONARY MODE ]
[ properties] -> [a: 1, b: 2, c: 3, d: 4, e: 5] ; this is classical hash table
[elements]
Property Name | 属性名
作为动态语言,
obj.prop
obj["prop"]
参照
obj[1]; //
obj["1"]; // names for the same property
obj[1.0]; //
const o = {
toString: function () {
return "-1.5";
},
};
obj[-1.5]; // also equivalent
obj[o]; // since o is converted to string
而length
属性的对象而已,length
会返回当前最大下标加一的结果
const a = new Array();
a[100] = "foo";
a.length;//101
a[undefined] = 'a';
a.length; //0
Function
本质上也是对象,只不过length
属性会返回参数的长度而已:
> a = ()=>{}
[Function: a]
> a.length
0
> a = (b)=>{}
[Function: a]
> a.length
1
In-Object Properties & Fast Property Access | 对象内属性与访问优化
作为动态类型语言,
const PROPERTIES = 10000000;
const o = {};
const start = +new Date();
for (const i = 0; i < PROPERTIES; i++) {
o[i] = i;
}
console.log(+new Date() - start);
function O(size) {
for (const i = 0; i < size; i++) {
this[i] = null;
}
}
const o = new O(PROPERTIES);
const start = +new Date();
for (const i = 0; i < PROPERTIES; i++) {
o[i] = i;
}
console.log(+new Date() - start);
class OClass {
constructor(size) {
for (const i = 0; i < size; i++) {
this[i] = null;
}
}
}
const o = new OClass(PROPERTIES);
const start = +new Date();
for (const i = 0; i < PROPERTIES; i++) {
o[i] = i;
}
console.log(+new Date() - start);
该程序的执行结果如下:
// Babel 下结果
385
37
49
// Chrome 下结果
416
32
31
第一种实现中,每次为对象 o
设置新的属性时,
function Point(x, y) {
this.x = x;
this.y = y;
}
当我们执行new Point(x,y)
语句时,Point
对象。创建的过程中,C0
的隐藏内部类,因为尚未为对象添加任何属性,此时隐藏类还是空的

接下来调用首个赋值语句this.x = x;
为当前Point
对象创建了新的属性x
,此时C0
创建另一个隐藏类C1
来替换C0
,然后在C1
中存放对象属性x
的内存位置信息

这里从C0
到C1
的变化称为转换this.y = y
这一条语句,会为Point
对象创建新的属性。此时
- 基于
C1
创建另一个隐藏类C1
,并且将关于属性y
的位置信息写入到C2
中。 - 更新
C1
为其添加转换信息,即当为Point
对象添加属性y
时,应该转换到隐藏类C2
。

整个过程的伪代码描述如下:
<Point object is allocated>
Class C0
"x": TRANSITION to C1 at offset 0
this.x = x;
Class C1
"x": FIELD at offset 0
"y": TRANSITION to C2 at offset 1
this.y = y;
Map C2
"x": FIELD at offset 0
"y": FIELD at offset 1
Reused Hidden Class | 重复使用的隐藏类
我们在上文中提及,如果每次添加新的属性时都创建新的隐藏类无疑是极大的性能浪费,实际上当我们再次创建新的Point
对象时,
- 初始化新的
Point
对象,并将隐藏类指向C0
。 - 添加
x
属性时,遵循隐藏类的转换原则指向到C1
, 并且根据C1
指定的偏移地址写入x
。 - 添加
y
属性时,遵循隐藏类的转换原则指向到C2
,并且根据C2
指定的偏移地址写入y
。
另外我们在上文以链表的方式描述转换,实际上真实场景中
function Point(x, y, reverse) {
if (reverse) {
this.x = x;
this.y = y;
} else {
this.y = x;
this.x = y;
}
}
Methods & Prototypes: 方法与原型
distance
方法可以被看做Point
的普通属性之一,不过其并非原始类型的数据,而是指向了另一个函数:
function Point(x, y) {
this.x = x;
this.y = y;
this.distance = PointDistance;
}
function PointDistance(p) {
const dx = this.x - p.x;
const dy = this.y - p.y;
return Math.sqrt(dx * dx + dy * dy);
}
如果我们像上文介绍的普通的distance
属性,那么无疑会带来较大的内存浪费,毕竟每个对象都要存放一段外部函数引用
<Point object is allocated>
Class C0
"x": TRANSITION to C1 at offset 0
this.x = x;
Class C1
"x": FIELD at offset 0
"y": TRANSITION to C2 at offset 1
this.y = y;
Class C2
"x": FIELD at offset 0
"y": FIELD at offset 1
"distance": TRANSITION to C3 <PointDistance>
this.distance = PointDistance;
Class C3
"x": FIELD at offset 0
"y": FIELD at offset 1
"distance": CONSTANT_FUNCTION <PointDistance>
注意,在这里如果我们将PointDistance
重定义指向了其他函数,那么这个转换也会自动失效,Prototype
属性,该属性会自动成为对象的原型链上的一环,上面的例子可以改写为以下方式:
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.distance = function (p) {
const dx = this.x - p.x;
const dy = this.y - p.y;
return Math.sqrt(dx * dx + dy * dy);
};
// ...
const u = new Point(1, 2);
const v = new Point(3, 4);
const d = u.distance(v);
Dictionary Mode
对于复杂属性的对象,undefined
,当插入新的数据时,计算得出的键名的哈希值的低位会被当做初始的存储索引地址。如果此地址已经被占用了,
// 插入
insert(table, key, value):
table = ensureCapacity(table, length(table) + 1)
code = hash(key)
n = capacity(table)
index = code (mod n)
while getKey(table, index) is not undefined:
index += 1 (mod n)
set(table, index, key, value)
//查找
lookup(table, key):
code = hash(key)
n = capacity(table)
index = code (mod n)
k = getKey(table, index)
while k is not null or undefined
and k != key:
index += 1 (mod n)
k = getKey(table, index)
if k == key:
return getValue(table, index)
else:
return undefined
尽管计算键名哈希值与比较的速度会比较快,但是每次读写属性的时候都进行这么多步骤无疑会大大拉低速度,因此
Fast Elements | 数值下标的属性
(0、1、2…… )
的属性称为
- Fast small integers
- Fast doubles
- Fast values
根据标准,
new Array(100)
,但实际上这仅仅针对Array
构造函数有用。如果你将值存在一个不存在的下标上,undefined
。当然,
// 这会大大降低大数组的性能
function copy(a) {
const b = new Array();
for (const i = a.length - 1; i >= 0; i--) b[i] = a[i];
return b;
}
由于普通的属性和数字式属性分开存放,即使数组退化为字典模式,也不会影响到其他属性的访问速度