在项目中,后端返回的数据中有一个字段是属性,不是数组
在vue中使用v-for
遍历,但是结果显示的内容和上图的排序完全不一样
一开始以为是v-for
的key使用了index
的原因,后面给每个内容加上了id,但是发现还是没效果,经过一番百度,终于知道了是ECMAScript 规范规定的原因。
ECMAScript 规范中定义了数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列。
JavaScript 中的对象是由一组组属性和值的集合,从 JavaScript 语言的角度来看,JavaScript 对象像一个字典,字符串作为键名,任意对象可以作为键值,可以通过键名读写键值。然而在 V8 实现对象存储时,并没有完全采用字典的存储方式,这主要是出于性能的考量。因为字典是非线性的数据结构,查询效率会低于线性的数据结构,V8 为了提升存储和查找效率,采用了一套复杂的存储策略。线性结构和非线性结构
线性结构和非线性结构
在开始之前,我们先来了解什么是对象中的常规属性和排序属性,你可以先参考下面这样一段代码:
function Foo() {
this[100] = 'test-100'
this[1] = 'test-1'
this["B"] = 'bar-B'
this[50] = 'test-50'
this[9] = 'test-9'
this[8] = 'test-8'
this[3] = 'test-3'
this[5] = 'test-5'
this["A"] = 'bar-A'
this["C"] = 'bar-C'
}
var bar = new Foo()
for(key in bar){
console.log(`index:${key} value:${bar[key]}`)
}
输出结果如下:
可以看到遍历bar
这个对象,输出的顺序是:先按照数字大小排列,属于字符串属性的,排列顺序为
改属性创建时的顺序,从Foo
函数里可以看到,B A C
先后被创建,故输出的顺序也是按照如此。
之所以出现这样的结果,是因为在 ECMAScript 规范中定义了数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列。
在这里我们把对象中的数字属性称为排序属性,在 V8 中被称为 elements,字符串属性就被称为常规属性,在 V8 中被称为 properties。
在 V8 内部,为了有效地提升存储和访问这两种属性的性能,分别使用了两个线性数据结构来分别保存排序属性和常规属性,具体结构如下图所示:
V8内部的对象构造
通过上图我们可以发现,bar 对象包含了两个隐藏属性:elements 属性和 properties 属性,elements 属性指向了 elements 对象,在 elements 对象中,会按照顺序存放排序属性,properties 属性则指向了 properties 对象,在 properties 对象中,会按照创建时的顺序保存了常规属性。
分解成这两种线性数据结构之后,如果执行索引操作,那么 V8 会先从 elements 属性中按照顺序读取所有的元素,然后再在 properties 属性中读取所有的元素,这样就完成一次索引操作。
将不同的属性分别保存到 elements 属性和 properties 属性中,无疑简化了程序的复杂度,但是在查找元素时,却多了一步操作,比如执行 bar.B
这个语句来查找 B 的属性值,那么在 V8 会先查找出 properties 属性所指向的对象 properties,然后再在 properties 对象中查找 B 属性,这种方式在查找过程中增加了一步操作,因此会影响到元素的查找效率。
基于这个原因,V8 采取了一个权衡的策略以加快查找属性的效率,这个策略是将部分常规属性直接存储到对象本身,我们把这称为对象内属性 (in-object properties)。对象在内存中的展现形式你可以参看下图:
对象内属性
采用对象内属性之后,常规属性就被保存到 bar 对象本身了,这样当再次使用bar.B
来查找 B 的属性值时,V8 就可以直接从 bar 对象本身去获取该值就可以了,这种方式减少查找属性值的步骤,增加了查找效率。
不过对象内属性的数量是固定的,默认是 10 个,如果添加的属性超出了对象分配的空间,则它们将被保存在常规**属性存储(即element
字符串属性)**中。虽然属性存储多了一层间接层,但可以自由地扩容。
通常,我们将保存在线性数据结构中的属性称之为**“快属性”,因为线性数据结构中只需要通过索引即可以访问到属性,虽然访问线性结构的速度快,但是如果从线性结构中添加或者删除大量的属性时,则执行效率会非常低,这主要因为会产生大量时间和内存开销**。(个人认为,这也是不推荐使用delete方式删除对象属性的原因)
因此,如果一个对象的属性过多时,V8 就会采取另外一种存储策略,那就是**“慢属性”策略,但慢属性的对象内部会有独立的非线性数据结构 (词典)** 作为属性存储容器。所有的属性元信息不再是线性存储的,而是直接保存在属性字典中。
慢属性是如何存储的
现在我们知道了 V8 是怎么存储对象的了,接下来我们来结合 Chrome 中的内存快照,来看看对象在内存中是如何布局的?
你可以打开 Chrome 开发者工具,先选择控制台标签,然后在控制台中执行以下代码查看内存快照:
function Foo(property_num,element_num) {
//添加可索引属性
for (let i = 0; i < element_num; i++) {
this[i] = `element${i}`
}
//添加常规属性
for (let i = 0; i < property_num; i++) {
let ppt = `property${i}`
this[ppt] = ppt
}
}
var bar = new Foo(10,10)
你可以在搜索框里面输入构造函数 Foo,Chrome 会列出所有经过构造函数 Foo 创建的对象,如下图所示:
观察上图,我们搜索出来了所有经过构造函数 Foo 创建的对象,点开 Foo 的那个下拉列表,第一个就是刚才创建的 bar 对象,我们可以看到 bar 对象有一个 elements 属性,这里面就包含我们创造的所有的排序属性,那么怎么没有常规属性对象呢?
这是因为只创建了 10 个常规属性,所以 V8 将这些常规属性直接做成了 bar 对象的对象内属性。
所以这时候的数据内存布局是这样的:
- 10 个常规属性作为对象内属性,存放在 bar 函数内部;
- 10 个排序属性存放在 elements 中。
接下来我们可以将创建的对象属性的个数调整到 20 个,你可以在控制台执行下面这段代码:
var bar2 = new Foo(20,10)