前言
本文章源自《JavaScript知识小册》专栏,感兴趣的话还请关注点赞收藏.
上一篇文章:《JavaScript DOM property和attribute的区别》
普通属性
在JavaScript
中定义对象时,普通的以字符串为键
定义的属性为普通属性
,特点是在遍历对象属性时根据创建时的顺序排序
arduino
const obj = {
'name': 'Bruse',
'age': 16,
}
for (const key in obj) {
console.log(key)
}
输出顺序为
name
age
排序属性
排序属性则是以数字为键
定义的属性,特点是按照索引值的大小进行升序排序
,优先级优于普通属性。
arduino
const obj = {
'name': 'Bruse', // 普通属性
'age': 16, // 普通属性
2: 'obj2' // 排序属性
}
obj[1] = 'obj1' // 排序属性
obj[99] = 'obj99' // 排序属性
obj[10] = 'obj10' // 排序属性
for (const key in obj) {
console.log(key)
}
输出
1
2
10
99
name
age
可以看到在使用for ... in
遍历obj
的属性时,排序属性
遍历顺序优先于普通属性
,同时排序属性
也是按照键值进行升序排序
的,键值越小,遍历顺序越往前
。
字符串数字作为键
在obj
中新建一个键为'3'
,值为112
的属性,遍历obj
属性名并输出
arduino
const obj = {
'name': 'Bruse', // 普通属性
'age': 16, // 普通属性
2: 'obj2', // 排序属性
'3': 112 // 转换,也是排序属性
}
obj[1] = 'obj1' // 排序属性
obj[99] = 'obj99' // 排序属性
obj[10] = 'obj10' // 排序属性
for (const key in obj) {
console.log(key)
}
输出
1
2
3
10
99
name
age
可以看到即便在定义属性键时,是字符串类型的数字3,也会做一下转换,将其转换为排序属性
排序属性 VS 普通属性
存储方式
首先排序属性
和普通属性
在对象中的存储方式不一样
可以简单地理解为obj
对象中,分别有着elements
和properties
两个内置的属性,elements
可以理解为是一个数组,而数字键则是属性在该数组中的下标,也就是内存偏移量。通过下标访问数组中的某个元素是很快的。当我们要访问obj.1
时,会先从obj
对象中内置的elements
属性中通过1
这个下标进行查询,最终找到obj[1]
的值为obj1
。
而properties
可以理解为是一个Map
,而字符串键则是该Map
中的key
,value
则是对应的属性值。当访问obj.name
时,会先从elements
中进行查找,结果是找不到,然后再从properties
中进行查找,但从properties
中查找则没有elements
中查找快,因为elements
可以通过计算偏移量来进行访问,但是properties
要hash计算访问。
对象内属性
其实上图还并不是全貌,因为即便有着elements
和properties
分别存放排序属性和普通属性,但是无论访问排序属性和普通属性[排序属性访问速度优于普通属性]
,都是要先访问obj
这个对象的elements
或properties
内置属性,然后再通过属性键访问到具体的属性值,其实就相当于obj.name
= obj.properties.name
,还存在优化空间,这个时候对象内属性
就出场了。
对象内属性
其实就是被保存到对象自身的常规属性
,也就是真正意义上的让obj.name
不再等于obj.properties.name
,而是真正的所见即所得obj.name
代码测试
为了方便理解,把之前的代码稍作修改
javascript
class People {
constructor() {
this.name = 'Bruse'
this.age = 16
this[2] = 'obj2'
}
}
const obj = new People()
obj[1] = 'obj1' // 排序属性
obj[99] = 'obj99' // 排序属性
obj[10] = 'obj10' // 排序属性
debugger // 避免执行过快,导致还来不及生成内存快照,obj对象就已被回收
for (const key in obj) {
console.log(key)
}
F12
打开浏览器开发者工具,当debugger
阻塞住代码往下执行时,点击Memory
,生成内存快照
可以看到obj
这个对象中存在内置属性elements
和一些内属性name
、[10]
、[99]
、[1]
、[2]
,暂时并没有内置属性properties
首先因为obj
属性并不多,此时对象内属性的数量还比较少
,所以此时并不需要内置属性properties
来存放普通属性,而是将name
等普通属性当做是对象的内置属性存放即可,访问速度比访问elements
和properties
更快。
Tips:不要看到elements中10、99排在1、2前面,就以为elements不是按照数字升序排列的,因为输出之后你会发现其实还是1、2、10、99这样的顺序,至于为什么在调试工具里看起来顺序有点不太一致,我也不知道...
添加更多的排序属性
接下来给obj
塞入更多的排序属性
javascript
class People {
constructor() {
this.name = 'Bruse'
this.age = 16
}
}
const obj = new People()
for (let i = 0; i < 20; i++) {
obj[i] = `obj${i}`
}
debugger
for (const key in obj) {
console.log(key)
}
再看看看obj
的变化,首先elements
属性中的元素变多了
同时也并没有出现内置属性properties
因为只是增加了更多的排序属性
,并没有突破内置属性(内置常规属性)的数量限制
添加更多的普通属性
接下来再塞入更多的普通属性
javascript
class People {
constructor() {
this.name = 'Bruse'
this.age = 16
for (let i = 0; i < 20; i++) {
this[`obj${i}`] = i
}
}
}
const obj = new People()
debugger
for (const key in obj) {
console.log(key)
}
可以看到在这个时候obj
的内置属性properties
终于出现了
只不过貌似并不像elements
那样方便预览其中的属性...后来经过一番查找和尝试,终于是找到了解决办法... 也很简单,就是生成内存快照时多做一步操作,勾上"在快照中添加数字值"
再次内存分析,好吧...因为生成20个常规变量的缘故,貌似也还没达到内置属性的限制...
将数量调整到50个,可以看到elements
和properties
都有了相应存放的变量
css
for (let i = 0; i < 50; i++) {
this[i] = `obj${i}`
this[`obj${i}`] = i
}
困惑
单纯在Chrome上进行内存分析的话,貌似即便超出了内属性数量限制,那多出来的一部分普通属性,也非常直白地展示在properties
之外,这个暂时不太清楚...
总结
把上述排序属性``普通属性``内属性
结合到一块来看,那么JavaScript
中的对象属性存放结构应该如下图所示
首先是为了提高访问速度,对象obj
本身就会有一定空间存放内属性
,在访问内属性
时,可以直接跳过访问elements
或properties
这一步,直接访问到该属性的值。
但是内属性
是有一定数量限制的,所以当超出了限制后,剩下的普通属性
会被存放到properties
中,而properties
有点像Map
,在进行属性访问的时候,需要计算出键的hash值,然后才能访问到具体的属性值。
而elements
则是存放排序属性
用的,有点像Array
,数字键即数组中的下标,所有元素按数字升序进行排序,访问属性值时则是通过下标进行访问,访问速度会比properties
要快一些。
properties
和elements
两种存储方式之间的VS,本质上就像是数据结构中的Map
和Array
之间的VS。