JavaScript引擎包括Google V8(Chrome,Node)都是专为快速执行大型JavaScript程序而设计的。在开发过程中,如果你在乎内存使用率和性能情况,那么你应该会关心在用户的浏览器中JavaScript引擎背后是怎么样的。无论是V8、SpiderMonkey (Firefox)、Carakan (Opera)、Chakra (IE) 还是其他,有了它们可以帮助你更好的优化应用程序。
我们应该时不时地询问自己:
我还能做些什么使代码更加有效?
主流的JavaScript引擎做了哪些优化?
什么是引擎无法优化的,我能期待利用垃圾回收进行清洁吗?
快速的加载Web网页就如同汽车一样,需要使用特殊工具。
当涉及到编写高效的内存和快速创建代码时总会出现一些常见的弊端,在这篇文章中我们将探索高效编写代码的测试验证方法。
一、JavaScript如何在V8中工作?
如果你对JS引擎没有较深的了解,开发一个大型Web应用也没啥问题,就好比会开车的人也只是看过引擎盖而没有看过车盖内的引擎一样(这里将Web网页比如成汽车)。Chrome浏览器是我的优先选择,这里我将谈下V8的核心组件:
一个基本的编译器,在代码执行前分析JavaScript、生成本地机器代码而非执行字节代码或是简单的解释,该段代码之初不是高度优化的。
V8用对象模型“表述”对象。在JavaScript中,对象是一个关联数组,但是V8中,对象被“表述”为隐藏类,这种隐藏类是V8的内部类型,用于优化后的查找。
运行时分析器监视正在运行的系统并优化“hot”(活跃)函数。(比如,终结运行已久的代码)
通过运行时分析器把优化编译器重新编译和被运行时分析器标识为“hot”的代码 ,这是一种有效的编译优化技术,(例如用被调用者的主体替换函数调用的位置)。
V8支持去优化,也就是说当你发现一些假设的优化代码太过乐观,优化编译器可以退出已生成的代码。
垃圾回收,了解它是如何工作的,如同优化JavaScript一样同等重要。
二、垃圾回收
垃圾回收是内存管理的一种形式,它试图通过将不再使用的对象修复从而释放内存占用率。垃圾回收语言(比如JavaScript)是指在JavaScript这种垃圾回收语言中,应用程序中仍在被引用的对象不会被清除。手动消除对象引用在大多数情况下是没有必要的。通过简单地把变量放在需要它们的地方(理想情况下,尽可能是局部作用域,即它们被使用的函数里而不是函数外层),一切将运作地很好。
垃圾回收清除内存
在JavaScript中强制执行垃圾回收是不可取的,当然,你也不会想这么做,因为垃圾回收进程被运行时控制着,它知道什么时候才是适合清理代码的最好时机。
1. “消除引用”的误解(De-Referencing Misconceptions)
在JavaScript中回收内存在网上引发了许多争论,虽然它可以被用来删除对象(map)中的属性(key),但有部分开发者认为它可以用来强制“消除引用”。建议尽可能避免使用delete,在下面的例子中delete o.x 的弊大于利,因为它改变了o的隐藏类,使它成为通用的慢对象。
var o = { x: 1 };
delete o.x; // true
o.x; // undefined
目的是为了在运行时避免修改活跃对象的结构,JavaScript引擎可以删除类似“hot”对象,并试图对其进行优化。如果该对象的结果没有太大改变,超过生命周期,删除可能会导致其改变。
对于null是如何工作也是有误解的。将一个对象引用设置为null,并没有使对象变“空”,只是将它的引用设置为空而已。使用o.x=null比使用delete会更好些,但可能也不是很必要。
var o = { x: 1 };
o = null;
o; // null
o.x // TypeError
如果这个引用是最后一个引用对象,那么该对象可进行垃圾回收;倘若不是,那么此方法不可行。注意,无论您的网页打开多久,全局变量不能被垃圾回收清理。
var myGlobalNamespace = {};
当你刷新新页面时,或导航到不同的页面,关闭标签页或是退出浏览器,才可进行全局清理;当作用域不存在这个函数作用域变量时,这个变量才会被清理,即该函数被退出或是没有被调用时,变量才能被清理。
经验法则:
为了给垃圾回收创造机会,尽可能早的收集对象,尽量不要隐藏不使用的对象。这一点主要是自动发生,这里有几点需要谨记:
正如之前我们提到的,手动引用在合适的范围内使用变量是个更好的选择,而不是将全局变量清空,只需使用不再需要的局部函数变量。也就是说我们不要为清洁代码而担心。
确保移除不再需要的事件侦听器,尤其是当DOM对象将要被移除时。
如果你正在使用本地数据缓存,请务必清洁该缓存或使用老化机制来避免存储那些不再使用的大量数据。
2. 函数(Functions)
正如我们前面提到的垃圾回收的工作原理是对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。下面的例子能够更好的说明这一点:
function foo() {
var bar = new LargeObject();
bar.someCall();
}
当foo返回时,bar自动指向垃圾回收对象,这是因为没被调用,这里我们将做个对比:
function foo() {
var bar = new LargeObject();
bar.someCall();
return bar;
}
// somewhere else
var b = foo();
这里有个调用对象且被一直调用着直到这个调用交给b(或是超出b范围)。
3. 闭包(Closures)
当你看到一个函数返回到内部函数,该内部函数可以访问外部函数,即使外部函数正在被执行。这基本上是一个封闭的,可以在特定的范围内设置变量的表达式。比如:
function sum (x) {
function sumIt(y) {
return x + y;
};
return sumIt;
}
// Usage
var sumA = sum(4);
var sumB = sumA(3);
console.log(sumB); // Returns 7
在sum调用上下文中生成的函数对象(sumIt)是无法被回收的,它被全局变量(sumA)所引用,并且可以通过sumA(n)调用。
这里有个示例演示如何访问largeStr?
var a = function () {
var largeStr = new Array(1000000).join('x');
return function () {
return largeStr;
};
}();
我们可以通过a():
var a = function () {
var smallStr = 'x';
var largeStr = new Array(1000000).join('x');
return function (n) {
return smallStr;
};
}();
此时,我们不能访问了,因为它是垃圾回收的候选者。
4. 计时器(Timers)
最糟糕的莫过于在循环中泄露,或者在setTimeout()/setInterval()中,但这却是常见的问题之一。
var myObj = {
callMeMaybe: function () {
var myRef = this;
var val = setTimeout(function () {
console.log('Time is running out!');
myRef.callMeMaybe();
}, 1000);
}
};
如果我们运行:
myObj.callMeMaybe();
在计时器开始前,我们看到每一秒“时间已经不多了”,这时,我们将运行:
myObj = null;
三、当心性能陷阱
除非你真正需要,否则永远不要优化代码。在V8中你能轻易的看到一些细微的基准测试显示比如N比M更佳,但是在真实的模块代码中或是在实际的应用程序中测试,这些优化所带来的影响要比你想象中要小的多。
创建一个模块,这里有三点:
采用本地的数据源包含ID数值
绘制一个包含这些数据的表格
添加事件处理程序,当用户点击的任何单元格时切换单元格的css class
如何存储数据?如何高效的绘制表格并追加到DOM?怎样处理表单上的事件?
注意:下面的这段代码,千万不能做:
var moduleA = function () {
return {
data: dataArrayObject,
init: function () {
this.addTable();
this.addEvents();
},
addTable: function () {
for (var i = 0; i < rows; i++) {
$tr = $('<tr></tr>');
for (var j = 0; j < this.data.length; j++) {
$tr.append('<td>' + this.data[j]['id'] + '</td>');
}
$tr.appendTo($tbody);
}
},
addEvents: function () {
$('table td').on('click', function () {
$(this).toggleClass('active');
});
}
};
}();
很简单,但是却能把工作完成的很好。
请注意,直接使用DocumentFragment和本地DOM方法生成表格比使用jQuery更佳,事件委托通常比单独绑定每个td更具备高性能。jQuery一般在内部使用DocumentFragment,但是在这个例子中,通过内循环调用代码append() ,因此,无法在这个例子中进行优化,但愿这不是一个诟病,但请务必将代码进行基准测试。
这里,我们通过opting for documentFragment提高性能,事件代理对简单的绑定是一种改进,可选的DocumentFragment也起到了助推作用。
var moduleD = function () {
return {
data: dataArray,
init: function () {
this.addTable();
this.addEvents();
},
addTable: function () {
var td, tr;
var frag = document.createDocumentFragment();
var frag2 = document.createDocumentFragment();
for (var i = 0; i < rows; i++) {
tr = document.createElement('tr');
for (var j = 0; j < this.data.length; j++) {