JS 是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。基本思路很简单:确定哪些变量不会再使用,然后释放它占用的内存,这个过程是周期性的,即垃圾回收程序每隔一定时间执行一次。在浏览器的发展史上,有两种变量标记策略:标记清理(常用)和引用计数(不常用)。
垃圾回收程序会周期运行,如果内存中分配了很多变量,则可能造成性能损失,尤其在内存有限的移动端设备上,垃圾回收可能明显地拖慢渲染速度和帧速率。开发者不知道什么时候会运行垃圾回收程序,因此最好的办法是在写代码时做到,无论什么时候收集垃圾,都能让它尽快结束工作。
现代垃圾回收程序会基于JS运行环境的探测来决定何时运行,探测机制因引擎而异,但基本上都是根据已分配对象的大小和数量来判断的。
1.通过 const 和 let 声明提升性能
let 和 const 都以块为作用域,所以相比于使用 var,使用这两个新关键字可能会更早的让垃圾回收程序介入,尽早回收应该回收的内存。
2. 隐藏类和删除操作
以 V8 引擎为例,它将 js 代码编译成机器码时会利用'隐藏类'。如果你的代码非常注重性能,那么这一点对你可能非常有用。
运行期间,V8 会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类的对象会更好。比如下面的这段代码:
function Eg(){
this.title = '标题'
}
const a1 = new Eg()
const a2 = new Eg()
V8会在后台配置,让 a1 和 a2 共享相同的隐藏类,因为它们共享相同的构造函数和原型。假设又添加下面这行代码
a2.author = 'heyu'
这时 a1 和 a2 会对应两个不同的隐藏类。这里的解决方案是避免“先创建再补充”式的动态属性赋值,在构造函数中一次性声明所有属性。如下所示:
function Eg(name){
this.title = '标题'
this.author = name
}
const a1 = new Eg('heyu')
const a2 = new Eg()
这样,两个实例基本上一样了,因此可以共享一个隐藏类。如果接下来使用 delete 关键字删除某个属性,那么将使实例不再共享同一个隐藏类
delete a1.title
动态删除属性和动态添加属性导致的结果一样,最佳实践是将不想要的属性设置为null,这样可以保存隐藏类不变,同时也能达到删除引用值供垃圾回收程序回收的效果。
3. 静态分配
为了提升 js 性能,最后要考虑的一点是榨干浏览器了。这里,一个关键的问题是如何减少浏览器执行垃圾回收的次数。开发者无法直接控制什么时候开始收集垃圾,但可以间接控制触发垃圾回收的条件。理论上,如果能够合理使用分配的内存,同时避免多余的垃圾回收,就可以保住因释放内存而损失的性能。
对象更替的速度是浏览器决定何时垃圾回收的一个标准,如果有很多对象被创建,又一下子超出了作用域,那么浏览器会采取更激进的方式调度垃圾回收程序运行,这会降低性能。看下面这个例子
function addVector(a,b){
const result = new Vector()
result.x = a.x + b.x
result.y = a.y + b.y
return result
}
上述函数会创建一个新对象,然后修改它,最后将其返回。如果这个矢量对象的生命周期很多,那么它会很快失去引用,成为可以被回收的值。假如该函数被频繁调用,那么垃圾回收程序将发现这里对象更替的速度很快,从而频繁地安排垃圾回收。改造方式如下
function addVector(a,b,result){
result.x = a.x + b.x
result.y = a.y + b.y
return result
}
函数的行为没有变,只是需要在其他地方创建矢量参数。一个策略是使用对象池,在初始化的某个时刻创建一个对象池,用来管理一组可回收的对象,当需要使用对象的时候,程序可以向对象池请求一个对象,设置属性,使用它,等操作完成后再把它还给对象池。由于没有发生对象初始化,垃圾回收程序不会发现对象更替,因此它不会那么频繁地运行。
静态分配是优化的一种极端方式,被垃圾回收严重拖后腿这种情况并不多见。