继续我们的 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(可能在别的地方还有打折),对于烟民而言也就一条 双精度红喜 ,不过它可比香烟所能带来的快感多得多。
全文完
好文章,谢谢!
对dom添加节点会引起页面渲染,请问对dom进行display:none和block是否也同样会引起页面重新渲染呢?
@Guest 对节点进行 CSS 渲染我认为也是一样的,期待能有一个确切的答案 :^)
FireFox 对待被display=none隐藏的对象不会重新渲染而IE会
[...]关于作用域的问题都是看了Javascript 优化计划(程序优化篇)[...]