编写快速、高效的JavaScript代码(中文)
英文原文:Writing Fast,Memory-Efficient JavaScript,编译:伯乐在线——戴嘉华 许多Javascript引擎都是为了快速运行大型的JavaScript程序而特别设计的,例如Google的V8引擎(Chrome浏览器,Node均使用该引擎)。在你的开发过程中,如果你关心你程序的内存和性能的话,你应该了解并意识到,在你的代码背后,浏览器的JavaScript引擎中到底发生了什么事情。
不论的V8,SpiderMonkey(Firefox),Carakan(Opera),Chakra(IE)或者其它类型的引擎。了解引擎背后的一些运行机制可以帮助你更好的优化你的应用程序。这并不是意味着你只为一种浏览器或者一种引擎进行程序的优化,而且,永远不要这样做。
然而,你应该问自己下面这些问题:
● 我应该做点什么才能让我的代码更高效地运行。 ● 流行的JavaScript引擎(通常)是怎么进行优化的。
● 有什么是引擎无法进行优化的,还有,垃圾回收器是不是按照我预想的那样,回收了我不需要的内存空间。
在我们编写高效、快速的代码的时候,有许多常见的陷阱。在这篇文章当中,我们会去探索一些方法,让你的代码拥有更加良好的性能,我们也会为这些代码提供测试样例。
JavaScript在V8引擎中是如何工作的?
虽然在没有彻底了解JavaScript引擎的情况下,开发出大型的应用程序是有可能的,这就像车主开过车却没有看过引擎盖背后的东西一样。把我选择的Chrome浏览器作为例子,我将会谈谈它的JavaScript引擎的工作机制。V8引擎,是由几个核心的部分组成的。
● 一个基本的编译器(basecompiler),在你的代码运行之前,它会分析你的JavaScript代码并且生成本地的机器码,而不是通过字节码的方式来运行,也不是简单地解释它。这种机器码起初是没有被高度优化的。
● V8通过对象模型(objectmodel)来表达你的对象。对象是在JavaScript中是以关联数组的方式呈现的,但是在V8引擎中,它们是通过隐藏类(hiddenclasses)的方式来表示的。这是一种可以优化查找的内部类型机制(internaltypesystem)。
● 一个运行期剖析器(runtimeprofiler),它会监视正在运行的系统,并且标识出―热点‖函数(―hot‖function),也就是那些最后会花费大量运行时间的代码。
● 一个优化编译器(optimizingcompiler),重新编译并优化运行期剖析器所标识―热点‖代码,然后执行优化,例如,把代码进行内联化(inlining)(也就是在函数被调用的地方用函数主体去取代)。
● V8引擎支持逆优化(deoptimization),意味着如果优化编译器发现在某些假定的情况下,把一些已经优化的代码进行了过度的优化,它就会把它门从生成的代码中抽离出来。
● V8拥有垃圾回收器。理解它是如何运作的和理解如何优化你的JavaScript代码同等重要。
垃圾回收
垃圾回收是一种内存管理机制。垃圾回收器的概念是,它会尝试去重新分配已经不需要的对象所占据的内存空间。在如JavaScript拥有垃圾回收机制的语言中,如果你的程序中仍然存在指向一个对象的引用,那么该对象将不会被回收。
在大多数的情况下,我们没有必要去手动得解除对象的引用(de-referencing)。只要简单地把变量放在它们应该的地方(在理想的情况下,变量应该尽量为局部变量,也就是说,在它们被使用的函数中声明它们,而不是在更外层的作用域),垃圾就能正确地被回收。
在JavaScript中强制进行垃圾回收是一件不可能的事情,而且你也不会想这样做。因
为垃圾回收的过程是由运行期所控制的,回收器通常知道垃圾回收的最佳时机在什么时候。
关于解除引用的误解
在网上不少关于JavaScript的内存分配问题的讨论中,关键字delete被频繁提出。虽然它本意是用来删除映射(map)中的键(keys),但是不少的开发者认为也可以使用它来强制解除引用。在可能的情况下,尽量避免使用delete。在下面的例子中,删除o.x在的代码背后会发生一些弊大于利的事情,因为它会改变o的隐藏类,并且把它转化成一般的对象,而这些一般对象会更慢。
varo = { x: 1 }; 1
deleteo.x;// true 2
o.x;// undefined 3
也就是说,在现在流行的JavaScript库中,你几乎肯定能找到delete删除引用的身影——它也确实存在这个语言目的。这里提出来的主旨是,让大家尽量避免在运行期改变热点对象(hotobjects)的结构。JavaScript引擎可以检测出这种的―热点‖对象并尝试去优化它们,如果在对象的生命期中没有遇到重大的结构改变,引擎的检测和优化过程会来得更加容易,而使用delete则会触发对象结构上的这种改变。
不少人对null的使用上也存在误解。将一个对象的引用设为null,并不是意味着―清空‖该对象,而是将该引用指向null。用o.x=null比用delete要好,但这甚至可能不是必要的。 varo = { x: 1 }; 1
o =null; 2
o;// null 3
o.x// TypeError 4
如果被删除的引用是指向对象的最后一个引用,那么该对象就满足了垃圾回收的资格。如果该引用不是指向对象的最后一个引用,那么该对象仍然可以被获取,而不会被垃圾回收。
另外要重点注意的是,要意识到,在你页面的生命期中,全局变量不会被垃圾回收器所清理。只要你的页面保持打开状态,JavaScript运行期中的全局对象就会常驻在内存当中。 varmyGlobalNamespace = {}; 1
只有当你刷新页面,导航到不同的页面,关闭选项卡,或关闭你的浏览器,全局变量才会被清理。当函数作用域变量超出作用域范围,它就会被清理。当函数完全结束,并且再没有任何引用指向其中的变量,函数中的变量会被清理。
经验法则
为了给垃圾回收器尽早,尽量多地回收对象的机会,不要保留你不再需要的对像。这种情况大多会自动发生;这里有几件事是要谨记的:
● 就像之前所说的那样,一个比手动解除引用更好的选择是,在恰当的作用域中使用变量。也就是说,用可以自动从作用域中剔除的函数局部变量,去取代要手动清空的全局变量。这意味着你的代码会更加的整洁且要担忧的事情会更少。
● 确保要及时注销掉你不再需要的监听事件。特别是对那些必然要删除的DOM对象。 ● 如果你正在使用本地数据缓存的话,确保要清除数据缓存或者使用老化机制(agingmechanism),以免保存了大量你不大可能复用的数据。
函数
接下来,让我们看看函数。正如我们所说的,垃圾回收是通过重新分配已经无法通过引用获得的内存块(对象)来工作的。为了更好地说明这一点,这里有一些例子。 functionfoo() { 1
varbar =newLargeObject(); 2
bar.someCall(); 3 } 4
当foo函数结束的时候,bar指向的对象就会自动地被垃圾回器所获取,因为已经没有任何引用指向该对象了。
对比以下代码: functionfoo() { 1
varbar =newLargeObject(); 2
bar.someCall(); 3
returnbar; 4 } 5 6
// somewhere else 7
varb = foo(); 8
现在我们有了一个指向该对象的引用,这个引用会在该次调用中保留下来,直到调用者将b赋值给其他东西(或者b超出了作用域范围)。
闭包
现在我们来看看一个返回内部函数的函数,那个内部函数可以访问到更外层的作用域,即使外部函数已经执行完毕。这基本上就是一个闭包——一种可以使用设置在特殊上下文中的变量的表现。例如: 1
functionsum (x) { 2
functionsumIt(y) { 3
returnx + y; 4
}; 5
returnsumIt; 6 } 7 8
// Usage 9
varsumA = sum(4); 1
varsumB = sumA(3); 0
console.log(sumB);// Returns 7 1 1
在sum运行上下文中创造的函数对象不会被垃圾回收,因为它被一个全局变量所指向,仍然非常容易被访问到。它可以通过sumA(n)来运行。
让我们来看另外一个例子。这里,我们可以访问到largeStr吗?

