闭包如何产生内存消耗及性能消耗
在《高性能JavaScript》这本书中,对“闭包如何产生内存消耗及性能消耗”这个专题做了比较清晰的解释。如下(内容摘自此书的第二章数据访问中的“闭包,作用域,和内存”)——
1.先说标识符识别性能——
标识符识别不是免费的,事实上没有哪种电脑操作可以不产生性能开销。在运行期上下文的作用域链中,一个标识符所处的位置越深,它的读写速度就越慢。所以,函数中局部变量的访问速度总是最快的,而全局变量通常是最慢的(优化的JavaScript引擎在某些情况下可以改变这种状况)。请记住,全局变量总是处于运行期上下文作用域链的最后一个位置,所以总是最远才能触及的。图2-4和2-5显示了作用域链上不同深度标识符的识别速度,深度为1表示一个局部变量。
图2-4 写操作的标识符识别速度
图2-5 读操作的标识符识别速度
总的趋势是,对所有浏览器来说,一个标识符所处的位置越深,读写它的速度就越慢。采用优化的JavaScript引擎的浏览器,如Safari 4,访问域外标识符时没有这种性能损失,而Internet Explorer,Safari 3.2,和其他浏览器则有较大幅度的影响。值得注意的是,早期浏览器如Internet Explorer 6和Firefox 2,有令人难以置信的陡峭斜坡,如果此图包含它们的数据,曲线高点将超出图表边界。
通过以上信息,在没有优化JavaScript引擎的浏览器中,最好尽可能使用局部变量。一个好的经验法则是:用局部变量存储本地范围之外的变量值,如果它们在函数中的使用多于一次。
2.引入正题,即闭包,作用域,和内存
闭包是JavaScript最强大的一个方面,它允许函数访问局部范围之外的数据。闭包的使用通过Douglas Crockford的著作流行起来,当今在最复杂的网页应用中无处不在。不过,有一种性能影响与闭包有关。
为了解与闭包有关的性能问题,考虑下面的例子:
function assignEvents() { var id = "xdi9592"; document.getElementByIdx("save-btn").onclick = function(event) { saveDocument(id); }; }assignEvents()函数为一个DOM元素指定了一个事件处理句柄。此事件处理句柄是一个闭包,当assignEvents()执行时创建,可以访问其范围内部的id变量。用这种方法封闭对id变量的访问,必须创建一个特定的作用域链。
当assignEvents()被执行时,一个激活对象被创建,并包含了一些应有的内容,其中包括id变量。它将成为运行期上下文作用域链上的第一个对象,全局对象是第二个。当闭包创建时,[[Scope]]属性与这些对象一起被初始化(见图2-7)。
图2-7 assignEvents()运行期上下文的作用域链和闭包
由于闭包的[[Scope]]属性包含与运行期上下文作用域链相同的对象引用,会产生副作用。通常,一个函数的激活对象与运行期上下文一同销毁。当涉及闭包时,激活对象就无法销毁了,因为引用仍然存在于闭包的[[Scope]]属性中。这意味着脚本中的闭包与非闭包函数相比,需要更多内存开销。在大型网页应用中,这可能是个问题,尤其在Internet Explorer中更被关注。IE使用非本地JavaScript对象实现DOM对象,闭包可能导致内存泄露。
当闭包被执行时,一个运行期上下文将被创建,它的作用域链与[[Scope]]中引用的两个相同的作用域链同时被初始化,然后一个新的激活对象为闭包自身被创建(参见图2-8)。
图2-8 闭包运行
注意闭包中使用的两个标识符,id和saveDocument,存在于作用域链第一个对象之后的位置上。这是闭包最主要的性能关注点:你经常访问一些范围之外的标识符,每次访问都导致一些性能损失。
在脚本中最好是小心地使用闭包,内存和运行速度都值得被关注。但是,你可以通过本章早先讨论过的关于域外变量的处理建议,减轻对运行速度的影响:将常用的域外变量存入局部变量中,然后直接访问局部变量。
3.总结
千言万语不如一张图——