javascript代码性能优化

在这篇文章叙述的关于Javascript代码优化,其实并没有什么新意,关于这方面的优化,一直都是Nicholas C. Zakas的专利。我在这里将要说的是对当前众多关于Javascript代码优化的方式一个小总结,并尝试去归类各种不同的优化方式,方便于记忆众说纷纭的优化技巧。本人归类的结果如下图所示:

Javascript代码优化无非主要围绕:DOM操作、循环、闭包、对象重复出现、对象的声明方式、作用域链、字符串操作、类的声明方式等等。循环、闭包、对象重复出现是从作用域链的角度去优化的;DOM操作主要围绕HTMLCollection、NodeList等来优化;对象的声明方式主要是对象(Object)、数组(Array)、字符串(String)、函数(Function)、正则(RegExp)等内置的对象使用字面量的方式来声明,这个比使用new来实例化相应的对象在性能上要强很多;字符串操作的优化方式主要是通过数组的push和join方法;类的声明方式优化方式主要是分清属性和方法的声明的方式,方法使用prototype的方式来声明;Javascript语言本身的流程操作语句的优化(if、switch、with、eval等等)。

因此,我将Javascript代码优化主要分为六类:DOM“真空”空间缩短作用域链字面量声明方式字符串操作类声明方式流程操作语句。下面将逐个叙述:

DOM“真空”空间

DOM“真空”空间这个词或许有些迷惑,举个例子就明白了:当使用removeChild将一个DOM元素从DOM树中删除之后,这个DOM元素并没有彻底消失,它就存在于DOM“真空”空间中,可以通过引用重新调用这个元素,并且对它的操作不会影响DOM文档树。

所以很多对DOM的HTMLCollection、NodeList操作的优化,都是利用了这个“真空”空间,先将元素从DOM树中剥离开来,再对这个元素进行一系列的操作之后,最后再通过appendChild或者insertBefore插入原来位置的DOM树中。这样就使得页面的reflow的次数最小化。比如下面优化的例子:  

/**
* Remove an element and provide a function that inserts it into its original position
* @param element {Element} The element to be temporarily removed
* @return {Function} A function that inserts the element into its original position
**/
//removeToInsertLater函数的作用是将element从DOM树中删除,保存于DOM“真空”空间里,同时返回一个闭包函数,用于在对element操作之后将其插回原来的位置。
function removeToInsertLater(element) {
  var parentNode = element.parentNode;
  var nextSibling = element.nextSibling;
  parentNode.removeChild(element);
  return function() {
    if (nextSibling) {
      parentNode.insertBefore(element, nextSibling);
    } else {
      parentNode.appendChild(element);
    }
    nextSibling=null; //因为处于闭包中,需要设置为null,断开作用域链,下同。
    parentNode=null;
  };
}
//接下来我们就可以对element进行一系列的操作,最后插回去
function updateAllAnchors(element, anchorClass) {
  var insertFunction = removeToInsertLater(element);
  var anchors = element.getElementsByTagName('a');
  for (var i = 0, length = anchors.length; i < length; i ++) {
    anchors[i].className = anchorClass;
  }
  insertFunction();
}

如果所示,将需要操作的DOM元素先从DOM树中剥离开来,对其进行操作,再安插回去,减少了页面reflow的次数,还有一种方式是将HTMLCollection、NodeList对象转换成数组的形式来进行操作,这都是不错的优化方式,可以应用在很多类似的优化案例中。当然了,对DOM的操作,DOM本身提供的API方法也都存在性能问题,使用nextSibling比childNodes快多了、使用item比使用普通的索引慢多了、不同的循环的方式对操作HTMLCollection、NodeList也都存在不同的性能:《雷人的优化HTMLCollection对象的循环操作技巧》,这个将会在下面的循环里叙述。

缩短作用域链

缩短作用域链,说简单点就是尽量使用局部变量来储存外部的对象。对于在循环中重复出现的对象,这个尤其具有优化效果;对于在闭包函数外的对象,当需要在闭包内使用的时候,可以在闭包内声明一个局部变量来储存该对象;还有对DOM元素的length属性、或者在DOM操作的循环中单个DOM元素使用局部变量来储存,也十分具有优化效果。目的就是使得在数据存取的过程中从作用域链中能最快的取出来,而最快的取出来的前提就是需要对象或者变量在作用域链的顶部。经过测试表明:局部变量的存取速度是最快的。所以将作用域外的对象局部引用化,也是不错的优化方式。循环也算是缩短作用域链的一个方面,但是某些方面也不全是,这个跟循环内部的比较和判断有关,具体请浏览《对循环操作的几种优化》。下面举几个例子:

//例子一:将NodeList的length属性缓存起来,这样就可以避免每次重新计算length属性,重新查询一遍DOM树。
var d = document.getElementsByTagName("div");
for(var i=0, l = d.length; i<l; i++){
  //code here...
}
//=====================================
//例子二:将每一个DOM元素局部化,因为对每一个DOM元素的操作,都需要在NodeList中去查询这个DOM元素。
var d = document.getElementsByTagName("div");
for(var i=0, l = d.length; i<l; i++){
  var item = d[i];
  item.className="active";
  item.title = "active item";
  //......
}
//=====================================
//例子三:储存重复出现的对象或者方法,特别是在循环中。将document.body缓存起来,这样在循环里读取的速度就非常快了。
var b = document.body;
for(var i=0;i<10;i++){
  b.appendChild(document.createTextNode("some text"));
}
//=====================================
//例子四:对闭包外的对象或者变量在闭包内局部化。
//msg在setTimeout闭包参数的作用域外
function setupAlertTimeout() {
  var msg = 'Message to alert';
window.setTimeout(function() { alert(msg); }, 100);
}
//下面这个更快,将变量声明放到闭包内,使得读取速度更快。
function setupAlertTimeout() {
  window.setTimeout(function() {
    var msg = 'Message to alert';
    alert(msg);
  }, 100);
}
//再来个更快的,这下避免使用闭包来作为setTimeout的参数,使得程序运行更快。
function alertMsg() {
  var msg = 'Message to alert';
  alert(msg);
}
function setupAlertTimeout() {
  window.setTimeout(alertMsg, 100);
}

对于上面例子四中关于闭包的使用,在这里解释一下:闭包非常强大而且使用,但是它也有很多缺陷,比如:是造成很多内存泄漏的罪魁祸首;声明一个闭包比声明一个不是闭包的内部函数更慢,比声明一个静态函数就更慢了。因此得出了上面的结论。

还有,对于缩短作用域链的一个更直白的解释例子如下:

var a = 'a';
function createFunctionWithClosure() {
  var b = 'b';
  return function () {
    var c = 'c';
    a;
    b;
    c;
  };
}
var f = createFunctionWithClosure();
f();

如上所示:当函数f调用的时候,读取a比读取b慢,比读取c又更慢。这就是作用域链的存取速度导致的性能问题,在HTMLCollection、NodeList中更甚。

字面量声明方式

这个字面量的声明方式的优化方式比较简单易懂,就是使用Javascript这门语言所独有的可以使用字面量的形式来声明对象的特点,这些比使用new来实例化响应的内置的对象将消耗更多的性能和时间,而且也不够灵活。比如下面的声明方式:

var obj = {};
var obj2 = {
  "name":"supersha"
}
var arr = [];
var arr2 = [1,2];
var reg = /abc/gi;
var fn = function(){
  alert("传说中的Hello world");
}

字符串操作

字符串中的优化已经算是不值得多说了,主要使用数组的push、join方法来时优化。现在来说说使用传统的“+”或者“+=”运算符为啥低效:当使用“+”或者“+=”运算符的时候,内存中先会创建一个变量副本来储存这个连接的结果,之后把结果赋值给“=”号左边的变量或者对象属性,最后销毁这个变量副本,当在循环中通过“+”或者“+=”来连接字符串的时候,就不得不忍受副本变量的创建、赋值、销毁这一连串的操作,如果需要连接的两个字符串比较大的时候,就更难以忍受了。所以,对它的优化,就被无可奈何的提出来了。举个例子:

//例子一:
var veryLongMessage = [
'This is a long string that due to our strict line length limit of',
' characters per line must be wrapped. ',
'% of engineers dislike this rule. The line length limit is for ',
' style purposes, but we do not want it to have a performance impact.',
' So the question is how should we do the wrapping?'
].join();
//=====================================
//例子二:
var strBuilder = ['First 20 fibonacci numbers:'];
for (var i = 0; i < 20; i++) {
  strBuilder.push(i, ' = ', fibonacci(i));
}
var fibonacciStr = strBuilder.join('');

类声明方式

类声明方式的优化,主要是利用了prototype原型链的优点,使得类在实例化的时候不需要给每一个实例都声明相同名称的方法或者属性,而是通过prototype原型链来声明方法或者属性,使得每一个类的实例都从原型链共享这个方法或者属性(有些不知道怎么表达,暂且这样吧)。比如下面的例子:

//下面声明类的方式比较糟糕
var Car = function(color,model,owner){
  this.color=color;
  this.model=model;
  this.owner=owner;
  this.setColor=function(c){
    this.color=c;
  }
  //....
}
//改进一下下,好多了……
var Car = function(color,model,owner){
  this.color=color;
  this.model=model;
  this.owner=owner;
}
Car.prototype.setColor=function(c){
  this.color=c;
}

流程操作语句

这里所说的流程操作语句主要是if、switch、do while、while、for、try catch,也有with、eval等等。if语句和switch语句之间的衡量是经常会碰到的优化问题:《对Duff策略优化数组操作的疑虑》;循环的优化在上面已经叙述过了;我相信很多叙述有关Javascript性能的文章都建议避免使用with、eval,甚至是try catch语句,因为catch语句会增加作用域链的深度,造成数据读取速度下降的性能问题,更多的细节可以阅读《高性能网站建设进阶指南》和Nicholas C.Zakas著的《High Performance JavaScript》中更详细的叙述。

总结

上面例举了Javascript代码优化的五个基本类别的方式。当然了,Javascript代码优化还远不止上面例子中出现的情况,大到设计模式、Ajax应用,小到还有对if、switch、with、eval等等的优化等等,无处不存在着代码性能优化这些细节问题,但是细化下来,也基本在上面五个类别里了。案例是丰富多彩的,但是本质不变。目的只有一个:让你的代码跑的更快

延伸阅读

 原文地址: javascript代码性能优化

XeonWell Studio