在使用垃圾回收的编程环境中,开发者通常无须关心内存管理。不过,JavaScript 运行在一个内存管理与垃圾回收都很特殊的环境。分配给浏览器的内存通常比分配给桌面软件的要少很多,分配给移动浏览器的就更少了。这更多出于安全考虑而不是别的,就是为了避免运行大量 JavaScript 的网页耗尽系统内存而导致操作系统崩溃。这个内存限制不仅影响变量分配,也影响调用栈以及能够同时在一个线程中执行的语句数量。
很大一部分前端开发自从有了chrome之后,很少再关心性能的问题了,除非写的代码特别不好或者业务对性能要求特别高(比如交易类系统),针对有性能要求的场景写写这部分内容。
现代垃圾回收程序会基于对 JavaScript 运行时环境的探测来决定何时运行。探测机制因引擎而异,
但基本上都是根据已分配对象的大小和数量来判断的。比如,根据 V8 团队 2016 年的一篇博文的说法:
“在一次完整的垃圾回收之后,V8 的堆增长策略会根据活跃对象的数量外加一些余量来确定何时再次垃
圾回收。”
function createPerson(name){
let localPerson = new Object();
localPerson.name = name;
return localPerson;
}
let globalPerson = createPerson("Nicholas");
// 解除 globalPerson 对值的引用
globalPerson = null;
在上面的代码中,变量 globalPerson 保存着 createPerson()函数调用返回的值。在 createPerson()
内部,localPerson 创建了一个对象并给它添加了一个 name 属性。然后,localPerson 作为函数值
被返回,并被赋值给 globalPerson。localPerson 在 createPerson()执行完成超出上下文后会自
动被解除引用,不需要显式处理。但 globalPerson 是一个全局变量,应该在不再需要时手动解除其
引用,最后一行就是这么做的。
不过要注意,解除对一个值的引用并不会自动导致相关内存被回收。解除引用的关键在于确保相关
的值已经不在上下文里了,因此它在下次垃圾回收时会被回收。
通过 const 和 let 声明提升性能
相比于使用 var,使用这两个新关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存。在块作用域比函数作用域更早终止的情况下,这就有可能发生。
隐藏类和删除操作
function Article() {
this.title = 'Inauguration Ceremony Features Kazoo Band';
}
let a1 = new Article();
let a2 = new Article();
V8 会在后台配置,让这两个类实例共享相同的隐藏类,因为这两个实例共享同一个构造函数和原型。假设之后又添加了下面这行代码:
a2.author = 'Jake';
此时两个 Article 实例就会对应两个不同的隐藏类。根据这种操作的频率和隐藏类的大小,这有可能对性能产生明显影响。当然,解决方案就是避免 JavaScript 的“先创建再补充”(ready-fire-aim)式的动态属性赋值,并在构造函数中一次性声明所有属性,如下所示:
function Article(opt_author) {
this.title = 'Inauguration Ceremony Features Kazoo Band';
this.author = opt_author;
}
let a1 = new Article();
let a2 = new Article('Jake');
这样,两个实例基本上就一样了(不考虑 hasOwnProperty 的返回值),因此可以共享一个隐藏类,
从而带来潜在的性能提升。不过要记住,使用 delete 关键字会导致生成相同的隐藏类片段。看一下这
个例子:
function Article() {
this.title = 'Inauguration Ceremony Features Kazoo Band';
this.author = 'Jake';
}
let a1 = new Article();
let a2 = new Article();
delete a1.author;
在代码结束后,即使两个实例使用了同一个构造函数,它们也不再共享一个隐藏类。动态删除属性与动态添加属性导致的后果一样。最佳实践是把不想要的属性设置为 null。这样可以保持隐藏类不变和继续共享,同时也能达到删除引用值供垃圾回收程序回收的效果。比如:
function Article() {
this.title = 'Inauguration Ceremony Features Kazoo Band';
this.author = 'Jake';
}
let a1 = new Article();
let a2 = new Article();
a1.author = null;
function setName() {
name = 'Jake';
}
定时器
let name = 'Jake';
setInterval(() => {
console.log(name);
}, 100);
闭包
let outer = function() {
let name = 'Jake';
return function() {
return name;
};
};
function addVector(a, b) {
let resultant = new Vector();
resultant.x = a.x + b.x;
resultant.y = a.y + b.y;
return resultant;
}
调用这个函数时,会在堆上创建一个新对象,然后修改它,最后再把它返回给调用者。如果这个
矢量对象的生命周期很短,那么它会很快失去所有对它的引用,成为可以被回收的值。假如这个矢量
加法函数频繁被调用,那么垃圾回收调度程序会发现这里对象更替的速度很快,从而会更频繁地安排
垃圾回收。
该问题的解决方案是不要动态创建矢量对象,比如可以修改上面的函数,让它使用一个已有的矢量对象:
function addVector(a, b, resultant) {
resultant.x = a.x + b.x;
resultant.y = a.y + b.y;
return resultant;
}
使用对象池按需分配
let vectorList = new Array(100);
let vector = new Vector();
vectorList.push(vector);
由于 JavaScript 数组的大小是动态可变的,引擎会删除大小为 100 的数组,再创建一个新的大小为
200 的数组。垃圾回收程序会看到这个删除操作,说不定因此很快就会跑来收一次垃圾。要避免这种动态分配操作,可以在初始化时就创建一个大小够用的数组,从而避免上述先删除再创建的操作。不过,必须事先想好这个数组有多大
最近因为孩子教育问题十分头大,灌自己一碗鸡汤,先干为敬
不能把小孩子的精神世界变成单纯学习知识。如果我们力求使儿童的全部精神力量都专注到功课上去,他的生活就会变得不堪忍受。他不仅应该是一个学生,而且首先应该是一个有多方面兴趣、要求和愿望的人。——苏霍姆林斯基