写出快速,节省内存的Javascript代码(一)

写出快速,节省内存的Javascript代码(一)

这是一篇翻译文章,原文为:Writing Fast, Memory-Efficient JavaScript

像google V8(Chrome和Node.js)这样的JavaScript解析引擎是为大型JavaScript应用能够快速执行而设计的.在你开发的过程中,如果你关心内存使用和性能,你就应该了解用户浏览器下的JavaScript解析引擎的原理.

不管是V8还是 SpiderMonkey (Firefox), Carakan(Opera), Chakra (IE)或者其他的解析引擎, 知道他们的原理可以更好地优化你的程序.

但是这并不是说你就要针对一个浏览器或者一个解析引擎来优化,别做傻事了...

你应该这样思考:

  • 我的代码里是否有更高效的方法?
  • 那些流行的JavaScript引擎都采用怎样的优化方法?
  • 哪些是引擎无法优化的?哪些内容是垃圾回收机制可以帮我释放的?

car_engine

能够快速加载的网站和能够高速行驶的跑车一样,需要使用专业的工具.

有很多的陷阱使你无法写出高效节省内存的Javascript代码,本文我们将要去探索那些已经被测试为能够提升代码效率的方法.

V8引擎是怎样解析JavaScript的?

尽管你不需要了解JavaScript解析引擎的详细内容就可以构建一个大型的JavaScript应用程序,但是作为一个好司机差不多都会对他爱车的发动机看上一眼吧.由于Chrome是我的使用的浏览器,我准备说一点它的JavaScript解析引擎.V8由几个核心部件构成:

  • 一个基础的编译器:和其他的编译器不同的是它并不是执行字节码或者简化解释器,而是在执行前将JavaScript代码解析后直接转换为机器码.不过这段代码这个时候并不是被高度优化的.
  • V8使用对象模型来描述你的对象.在JavaScript中对象被描述为相关联的数组,而在V8中它们被描述为hidden classes.hidden classes是高效查找的内部类型系统.
  • 运行时分析器检测运行的系统并标记出"热"方法(例如一段运行了很长时间的代码)
  • 一个优化编译器重新编译并优化被运行时分析器标记为"热"方法的代码,执行诸如代码嵌入的优化(例如将函数调用替换为回调函数)
  • V8支持逆向优化,即当优化编译器发现它之前优化的代码是被过度优化,它会释放之前产生的代码.
  • 它有一个垃圾回收器.理解它是如何工作的能有助于优化JavaScript代码.

垃圾回收器

垃圾回收机制是内存管理的一个方式.它被用在当一个对象不再被使用时来回收这个对象占用的内存.在像JavaScript这样的具有垃圾回收机制的语言中,当对象还在被引用着是不会被释放的.

在大部分常见下并不需要手动取消引用.仅仅通过注意变量的位置(作用域越小越好,例如在方法需要的变量在内部定义而不是外部)就可以了.

garbage_collection

垃圾回收器尝试回收内存

在JavaScript中是无法强制垃圾回收.你不能这样做是因为:垃圾回收进程是被运行时控制,它知道什么时间来清理垃圾.

清除错误的引用

在网上相当多的关于JavaScript的回收内存的讨论中很多是关于delete关键字.尽管它只是被用来从map中移除keys,一些开发者却认为它可以强制取消引用.尽量避免使用delete关键字.在下边的例子中,delete o.x这段代码有很多害处,因为它改变了对象o的hidden class,让它变成一个相对慢的对象.

1    var o = { x: 1 }; 
2    delete o.x; // true 
3    o.x; // undefined

不过,你肯定可以在许多流行的JavaScript库中看的delete--它确实有它的用途.这里不使用的原因是为了避免修改运行时中的"热"对象的数据结构.JavaScript引擎可以侦测到这种"热"对象并且尝试去优化它们.如果这个对象的结构在它的生命周期内没有太大的改变这种侦测就很简单,但是delete可以却这种改变让侦测变得复杂.

关于null的使用也有误区.把一个对象设为null,并没有把对象清空,而是把对象的引用设为空.使用o.x = null比使用delete好,但是这可能并不必要.

1    var o = { x: 1 }; 
2    o = null;
3    o; // null
4    o.x // TypeError

如果这个引用是这个对象的最后一次引用,那么这个对象会被回收,如果这次引用不是最后一个引用,这个对象仍然可以获取而不会被释放.

另外一个要注意的是全局变量并不会在你页面的生命周期内被释放.不过这个页面被打开多久,全局对象的作用域在JavaScript运行时中始终留存.

1    var myGlobalNamespace = {};

全局变量只有在你刷新页面、跳转到另一个页面、关闭标签或者退出你的浏览器时才会被清除.函数作用域中的变量会在函数结束时释放.当函数已经退出并且它们不再被引用时变量才会被释放.

经验法则

为了让垃圾回收器能够尽早回收更多的对象,别保留你不在需要的对象.这经常在无意中发生;记住下边的要点:
* 像前边说的,最好在最近位置使用变量.例如,当一个全局变量在外部为空时用函数级的变量这样就会在不使用时释放.
* 确保在事件响应listeners不在需要时移除它们,特别是当这些listeners绑定的DOM对象将要被移除的时候.
* 如果你使用了局部的数据缓存,确保删除它们或者通过一定的塌陷机制来避免大量的不被重用的数据被存储.

函数

接下来我们看看函数.我们前边说过,垃圾回收机制作用在那些不再能够获取到的内存(对象)中.为了更好的举例,这儿有一个例子:

1    function foo() {
2      var bar = new LargeObject();
3      bar.someCall();
4    }

foo函数返回后,bar所指向的对象会被垃圾回收器释放,因为没有任何对象再引用它了.

对比下边的代码:

1    function foo() {
2      var bar = new LargeObject();
3      bar.someCall();
4      return bar;
5    }
6
7    // somewhere else
8    var b = foo();

现在我们有了一个对该对象的引用,并且这个引用会一直保持到b被赋值为其他对象(或者b超出作用域)

闭包

当你看到一个方法返回一个内部方法,这个内部方法就能够被外部作用域内的变量引用,即便它已经超出了被调用方法的作用域.
这就是闭包:能够将变量设置到特殊的上下文环境中.例如:

 1    function sum (x) {
 2      function sumIt(y) {
 3        return x + y;
 4      };
 5      return sumIt;
 6    }
 7
 8    // Usage
 9    var sumA = sum(4);
10    var sumB = sumA(3);
11    console.log(sumB); // Returns 7

在调用sum函数的上下文环境中创建的函数对象不会被回收,因为它被一个全局变量引用仍然可以使用.它仍然可以通过sumA来执行.

我们看看另一个例子,在这个例子中我们可以使用largeStr吗?

1    var a = function () {
2      var largeStr = new Array(1000000).join('x');
3      return function () {
4        return largeStr;
5      };
6    }();

是的,我们仍然可以通过a()获取到largeStr,但是如果这样呢:

1    var a = function () {
2      var smallStr = 'x';
3      var largeStr = new Array(1000000).join('x');
4      return function (n) {
5      return smallStr;
6    };
7    }();

这样就无法获取largeStr,它就会被当做垃圾来回收.

计时器

内存泄露最厉害的地方是在循环上,或者在setTimeout()/setInterval()方法上.但这很常见.

看一下下边的例子:

 1    var myObj = {
 2      callMeMaybe: function () {
 3        var myRef = this;
 4        var val = setTimeout(function () { 
 5        console.log('Time is running out!'); 
 6          myRef.callMeMaybe();
 7        }, 1000);
 8      }
 9    };

如果我们执行:

1    myObj.callMeMaybe();

来开始计时,我们每秒钟都会看的打印出来的'Time is running out!'.即便是我们执行:

1    myObj=null;

计时器仍然在运行.myObj不会被回收是因为传入到setTimeout方法中的闭包一直在保持着.也就是说,它因为使用了myRef而一直保持着对myObj的引用.如果我们把一个闭包传给任何一个别的函数,这个函数就会保持着对这个闭包的引用.
还有一点需要注意的是在setTimeout/setInterval方法内部所引用的函数等需要执行完毕后才会被释放.

Powered by Engin & toto

comments powered by Disqus