编写快速、高效的JavaScript代码

414 查看

许多Javascript引擎都是为了快速运行大型的JavaScript程序而特别设计的,例如Google的V8引擎(Chrome浏览器,Node均使用该引擎)。在开发过程中,如果你关心你程序的内存和性能的话,你应该了解并意识到,在你的代码背后,浏览器的JavaScript引擎中到底发生了什么事情。

不论的V8,SpiderMonkey(Firefox)、Carakan(Opera)、Chakra(IE)或者其它类型的引擎。了解引擎背后的一些运行机制可以帮助你更好的优化你的应用程序。这并不是意味着你只为一种浏览器或者一种引擎进行程序的优化,而且,永远不要这样做。

然而,你应该问自己下面这些问题:

  • 我应该做点什么才能让我的代码更高效地运行。
  • 流行的JavaScript引擎(通常)是怎么进行优化的。
  • 有什么是引擎无法进行优化的,还有,垃圾回收器是不是按照我预想的那样,回收了我不需要的内存空间。

fast_memory

在我们编写高效、快速的代码的时候,有许多常见的陷阱。在这篇文章当中,我们会去探索一些方法,让你的代码拥有更加良好的性能,我们也会为这些代码提供测试样例。

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的隐藏类,并且把它转化成一般的对象,而这些一般对象会更慢。

也就是说,在现在流行的JavaScript库中,你几乎肯定能找到delete删除引用的身影——它也确实存在这个语言目的。这里提出来的主旨是,让大家尽量避免在运行期改变热点对象(hotobjects)的结构。JavaScript引擎可以检测出这种的“热点”对象并尝试去优化它们,如果在对象的生命期中没有遇到重大的结构改变,引擎的检测和优化过程会来得更加容易,而使用delete则会触发对象结构上的这种改变。

不少人对null的使用上也存在误解。将一个对象的引用设为null,并不是意味着“清空”该对象,而是将该引用指向null。用o.x=null比用delete要好,但这甚至可能不是必要的。

如果被删除的引用是指向对象的最后一个引用,那么该对象就满足了垃圾回收的资格。如果该引用不是指向对象的最后一个引用,那么该对象仍然可以被获取,而不会被垃圾回收。

另外要重点注意的是,要意识到,在你页面的生命期中,全局变量不会被垃圾回收器所清理。只要你的页面保持打开状态,JavaScript运行期中的全局对象就会常驻在内存当中。

只有当你刷新页面,导航到不同的页面,关闭选项卡,或关闭你的浏览器,全局变量才会被清理。当函数作用域变量超出作用域范围,它就会被清理。当函数完全结束,并且再没有任何引用指向其中的变量,函数中的变量会被清理。

经验法则

为了给垃圾回收器尽早,尽量多地回收对象的机会,不要保留你不再需要的对像。这种情况大多会自动发生;这里有几件事是要谨记的:

● 就像之前所说的那样,一个比手动解除引用更好的选择是,在恰当的作用域中使用变量。也就是说,用可以自动从作用域中剔除的函数局部变量,去取代要手动清空的全局变量。这意味着你的代码会更加的整洁且要担忧的事情会更少。

● 确保要及时注销掉你不再需要的监听事件。特别是对那些必然要删除的DOM对象。

● 如果你正在使用本地数据缓存的话,确保要清除数据缓存或者使用老化机制(agingmechanism),以免保存了大量你不大可能复用的数据。

函数

接下来,让我们看看函数。正如我们所说的,垃圾回收是通过重新分配已经无法通过引用获得的内存块(对象)来工作的。为了更好地说明这一点,这里有一些例子。

当foo函数结束的时候,bar指向的对象就会自动地被垃圾回器所获取,因为已经没有任何引用指向该对象了。

对比以下代码:

现在我们有了一个指向该对象的引用,这个引用会在该次调用中保留下来,直到调用者将b赋值给其他东西(或者b超出了作用域范围)。

闭包

现在我们来看看一个返回内部函数的函数,那个内部函数可以访问到更外层的作用域,即使外部函数已经执行完毕。这基本上就是一个闭包——一种可以使用设置在特殊上下文中的变量的表现。例如:

在sum运行上下文中创造的函数对象不会被垃圾回收,因为它被一个全局变量所指向,仍然非常容易被访问到。它可以通过sumA(n)来运行。

让我们来看另外一个例子。这里,我们可以访问到largeStr吗?