继续我们的 Javascript 优化计划, 上期 已经做到怎么尽可能的缩小 Javascript 脚本的文件体积便于传输。不过这样做仅仅是不够的,因为 Javascript 代码的速度被分割成两部分:下载时间(取决于文件的大小)和执行速度(取决于代码算法)。

当客户端载入 Javascript 脚本以后,真正的之行速度就取决于代码本身是否最优化了。这篇就是讲述如何优化代码本身的执行速度(听起来非常有技术的样子)。

关注作用域

浏览器中,Javascript 默认的变量范围是 window,也就是全局变量。在 window 中的变量只在页面从浏览器关闭以后才释放。而 Javascript 同时也有局部变量(私有变量)的概念,通常它在容器(比如 function)中执行完毕就会被释放。

所以很容易理解当调用某变量时,解释器就会自下(容器)由上(window)寻找变量,寻找的变量本身也是需要一点时间的。所以,解释器在作用树( 《Javascript 高级程序设计》 中称为「范围树」)中遍历的范围越短,那么脚本运行就会越快。

本人不擅长施教,下面的代码请自行理解

var country = "China";

function fn1() {
    alert(country);
}

function fn2() {
    var province = "Zhejiang";
    fn1();
}

function fn3() {
    var city = "Hangzhou";
    fn2();
}

fn3();

扩展阅读请点击 这里这里

使用局部变量

理解了上述的细节以后,接下来就非常可以理解了。使用局部变量可以带来更快的执行速度,因为解释器无需因为搜索变量而离开当前执行范围。同时,局部变量让允许完毕就会被释放,所以它们不会一直占用内存。

这里要注意的是,使用闭包会打破这一规则,详细信息可以参看 这里 和以前我做的 一道题目

避免使用 with 语句

搜索变量范围越小,运行速度越快,所以就很很容易理解避免使用 with 语句的原因。比如

alert(document.title);
alert(document.body.tagName);
alert(document.location);

可以写成

with (document) {
    alert(title);
    alert(body.tagName);
    alert(location);
}

虽然代码缩减的程度,并且也非常的容易理解。但是使用 with 语句的同时,要强制解释器不仅在作用树(范围树)内查找局部变量,还强制检测每个变量及指定的对象,看其是否有此变量或者属性。

因此,最好避免使用 with 语句。最短的代码并不一定总是最高效的。

选择正确的算法

这似乎就是废话,所有的程序员都明白正确的算法对于之行效率是多么的重要。这里就不多解释,可以参考 这篇这篇 。我始终相信,好的经验都是在实际 coding 中获得的。

循环的花招

Javascript 和大部分的程序语言一样,循环都会花费大量的执行时间,所以保持循环的高效可以减少执行时间。下面有几个花招,也是从 那本书 中获得的,照本宣科一下。

反转循环

有一个很有趣的例子,比如一个典型的循环会是这样写

for (var i = 0; i < element.length; i++) {
    // ...
}

但写成下面这个样子就有助于降低算法的复杂度,因为它用常数(O)作为条件循环以减少执行时间

for (var i = element.length - 1; i >= 0; i--) {
    // ...
}

书中的解释可能无法理解,那么我重新将其写成

var element_length = element.length;
for (var i = 0; i < element_length; i++) {
    // ...
}

可能会更好理解一些,因为它不会重复在循环中获取 element 的 length 属性,但书中的更改方法少了一个变量。

翻转循环

用 do...while 来替代 while 语句可以进一步的减少执行时间。比如

var i = 0;
while (i < element.length) {
   // ...
   i++;
}

可以改写为 do...while 语句为这个样子

var i = 0;
do {
    // ...
    i++;
} while (i < element.length);

当然,按照上一条的花招我们还可以优化成这个样子

var i = element.length - 1;
do {
    // ...
} while (--i >= 0);

这是因为 do...while 语句事先将循环体载入以后再做条件判断。不过本人认为还是保持程序的逻辑优先。

条件判断

优化 if 语句

用 if 和多个 else 语句时,将就有可能的情况放在最先,依次类推。同时尽量减少 else 和 if 的数量,将条件按照二叉树的方式进行排列。例如

if (i > 0 && i < 10) {
    alert('between 0 and 10');
} else if (i > 9 && i < 20) {
    alert('between 10 and 20');
} else if (i > 19 && i < 30) {
    alert('between 19 and 30');
} else {
    alert('out of range');
}

可以将这段代码写成

if (i > 0) {
    if (i < 10) {
        alert('between 0 and 10');
    } else {
        if (i < 20) {
            alert('between 10 and 20');
        } else {
            if (i < 30) {
                alert('between 20 and 30');
            } else {
                alert('Greater than or equal 30');
            }
        }
    }
} else {
    alert('less than or equal 0');
}

这个样子。虽然看上去非常的复杂,但是它已经考虑了很多代码潜在的条件判断情况,所以执行得更快。

switch 和 if

用 switch 还是 if 已经是老生常谈的问题了。一般来说,超过两个 if...else 判断的时候,最好是使用 switch 语句。这样做可以使代码更加清晰并且效率更高。同时,case 条件也可以使用任何类型的值。

语句瘦身

其实非常可以容易理解,脚本中的语句越少,执行所需的时间越短(听起来与上述观点有矛盾)。有很多方法可以将代码中的语句缩短,比如下面的一些花招。

定义多个变量

很明显,一条语句可以定义多个变量。这样做不仅可以缩小代码体积,还可以减少语句数量以减少执行时间。比如下面的代码

var webSite   = "www.gracecode.com";
var haveLunch = function () {...}; 

就可以精简为

var webSite = "www.gracecode.com", haveLunch = function () {...};

相信这样的语句也不会给阅读带来多大的障碍。

迭代因子

使用迭代因子,尽可能的合并语句。比如

var girlFriend = girl[i];
i++;

这样的语句可以使用

var girlFriend = girl[i++];

替代。不过建议特别小心 i++ 和 ++i 的区别 (该死的 C 语言后遗症)。

使用数组和对象字面量

这点其实在 上一篇 的时候就提到过,在这里就不复述。比如

var mySite = new Object;
mySite.author = "feelinglucky";
mySite.location = "http://www.gracecode.com";

就可以精简到

var mySite = {author:"feeinglucky", location:"http://www.gracecode.com"};

这样子。

其他的花招

优先使用内置方法

比如

function power(number, n) {
   var result = number;
   
   for (var i = 1; i < n; i++) {
       result *= number;
   }

   return result;
}

这样的函数,完全就可以使用 Math.pow 来完成。Javascript 已经有很多现成的内置方法,只要允许最好使用它们。

存储常用的值

当多次用到同一个值的时候,可以先将其存储在局部变量中,以便快速访问。这个就不复述了,偷个懒不好意思。

DOM 操作

节约 DOM 操作

Javascript 对 DOM 的处理,可能是最耗费时间的操作之一。每次 Javascript 对 DOM 的操作,浏览器都会改变页面的表现、重新渲染页面,从而有明显的时间损耗。比较环保的做法就是尽可能不在 DOM 中进行 DOM 操作。

请看下面的例子,为 ul 添加 10 个条目

var oUl = document.getElementById("ulItem");

for (var i = 0; i < 10; i++) {
    var oLi = document.createElement("li");
    oUl.appendChild(oLi);
    oLi.appendChild(document.createTextNode("Item " + i);
}

乍看起来似乎无懈可击,但是这段代码的确有问题。首先是循环中的 oUl.appendChild(oLi); 的调用,每次执行过后浏览器就会重新渲染页面;其次,给列表添加添加文本节点(oLi.appendChind(document.createTextNode("Item " + i);),也这会造成页面重新渲染。每次运行都会造成两次页面重新渲染,总计 20 次。

要解决这个问题就如上面所言的,减少 DOM 操作,将列表项目在添加好文本节点以后再添加。下面的代码就可以与上述的代码完成同样的任务。

var oUl   = document.getElementById("ulItem");
var oTemp = document.createDocumentFragment();

for (var i = 0; i < 10; i++) {
    var oLi = document.createElement("li");
    oLi.appendChild(document.createTextNode("Item " + i);
    oTemp.appendChild(oLi);
}

oUl.appendChild(oTemp);

遵循标准的 DOM

说点书中没有的(照本宣科完毕),Javascript 其实在寻找节点(Node)也会花上一段时间。对 Web 标准友好的 (x)html 文档相对杂乱文章的页面来说,Javascript 执行速度两者也会有所差别。

浏览器处理页面有模式之分,这也许也是为什么要编写遵循 Web 标准的页面的原因之一。具体信息可以参考 这里一些言论

缓存 Ajax

Ajax 虽然提供了页面异步请求调用,但别忘记了它还是访问服务器的。Javascript 作为驱动层本身可以作为缓存使用,虽然在页面重新载入后就会被释放,但对于服务器而言这是一个好的消息。

结束语

不知不觉就写了那么多,很多东西都是书上照本宣科的。《Javascript 高级程序设计》的确是一本不可多得的好书,建议大家有机会都可以去看看。这本书不贵,59 RMB(可能在别的地方还有打折),对于烟民而言也就一条 双精度红喜 ,不过它可比香烟所能带来的快感多得多。

全文完