前言
最近看到了一个关于优化的面试题
const obj1 = { | const obj1 = {
a: 1, | a: 1,
b: 2 | b: 2
} | }
|
const obj2 = { | const obj2 = {
a: 1, | b: 2,
b: 2 | a: 1
} | }
左右两边都是定义obj1
和obj2
对象,右边的obj2
的a
属性和b
属性换了位置,那么左右两边有什么区别呢?看到这道题确实很疑惑,这里是跟性能优化相关的,考察的知识是v8的隐藏类
隐藏类(Hidden Classes)
因为Javascript是一种动态编程语言,这意味着对象在初始化后仍然可以对其属性进行增删操作,才会有了隐藏类的概念,其实隐藏类的作用是为了优化属性的访问速度,下面通过例子来看看隐藏类做了些什么
class A{
constructor(name, age){
this.name = name
this.age = age
}
}
const a = new A('zs', 19)
- 当
V8
在执行第一行代码的时候会生成一个Hidden class 0
的类
Hidden class 0{
}
在此时还没有声明任何的属性,所以Hidden class 0
现在为空
- 当
V8
在执行第二行代码的时候会生成一个Hidden class 1
的类,在Hidden class 0
中添加内存偏移量指针,指向Hidden class 1
Hidden class 1{
public string name;
}
↑
|
|
Hidden class 0{
if add 'name' transition to class 1
}
- 当
V8
在执行第三行代码的时候会生成一个Hidden class 2
的类,在Hidden class 1
中添加内存偏移量指针,指向Hidden class 2
,当前的Hidden class 2
则代表最终被实例化出来的对象
Hidden class 2{
public string age;
}
↑
|
|
Hidden class 1{
public string name;
if add 'age' transition to class 2
}
- 他们可以彼此间间隔固定的偏移量储存在一段连续的内存空间中,最终的关系:
Hidden class 2{
public string age;
}
↑
|
|
Hidden class 1{
public string name;
if add 'age' transition to class 2
}
↑
|
|
Hidden class 0{
if add 'name' transition to class 1
}
通过循环实例化
那么隐藏类的好处在哪里呢,哪里实现了性能优化?下面我们通过一个for循环创建实例对象
for(let i = 0; i < 1000; i++){
new A(`user${i}`, i)
}
当第一次循环,会创建出class 0
class 1
class 2
,当进入到第二次循环,V8
会去查找有没有能代表当前代码的隐藏类,如果有则直接使用,那么这个循环无论循环多少次,都只会创建class 0
class 1
class 2
者这三个隐藏类,可以一直复用
什么情况下会重新创建隐藏类
我们通过改造class A
class A{
constructor(name, age){
this[name] = name
this.age = age
}
}
for(let i = 0; i < 1000; i++){
new A(`user${i}`, i)
}
当执行第一次循环时,会生成一个空的Hidden class 0
Hidden class 0{
}
但是执行到第二行代码时,因为属性名是动态变化的,这个时候Hidden class 1
生成的属性名为user0
Hidden class 1{
public string user0;
}
↑
|
|
Hidden class 0{
if add 'user0' transition to class 1
}
当执行第三行代码时,还是会创建一个Hidden class 2
Hidden class 2{
public string age;
}
↑
|
|
Hidden class 1{
public string user0;
if add 'age' transition to class 2
}
↑
|
|
Hidden class 0{
if add 'user0' transition to class 1
}
到目前为止和方案一消耗的性能是一致的,都是创建了三个隐藏类,但是走第二次循环时,就不一样了
Hidden class 2{
public string age;
}
↑
|
|
Hidden class 1{
public string user1;
if add 'age' transition to class 2
}
↑
|
|
Hidden class 0{
if add 'user1' transition to class 1
}
因为属性名发生了变化,V8
找不到能代表当前代码的隐藏类所以需要重新生成隐藏类,那么依次类推,我们循环了多少次,就要生成n个隐藏类,相互之间起不到复用的效果,所以两个例子所消耗的性能差别是很大的
优化建议
- 保证以相同的顺序实例化对象属性,这样可以保证它们共享相同的隐藏类。
- 在初始化属性时不要使用动态属性名,推荐使用
this.name = name
这种方式
面试题解答
同样都是生成obj1
和obj2
,但是左边的结构在创建obj2
时复用了执行obj1
生成的隐藏类,右侧结构因为a
和b
的属性定义顺序不相同,则导致了找不到对应的隐藏类,从而要创建出一个新的隐藏类,所以结论就是左侧的代码性能会好于右侧的代码