1. 执行速度要比变异性语言慢的多。受限于系统分配给的Web应用的内存和cpu周期起源。
不过还是有一些方式可以改进代码的整体性能的。
先来理解那些东西影响代码性能依然是非常重要的。
1. 注意作用域
- 避免全局查找:优化脚本性能最重要的就是注意全局查找。全局查找开销更大。如下代码:
function updateUI(){
var imgs = document.getElement.getElementsByTagName("img");
for(var i=0,len = imgs.length;i<len;i++){
imgs[i].title = document.title + " image " + i;
}
var msg = document.getElementById("msg");
msg.innerHTML = "Update complete.";
}
该段代码包含了三个对全局document对象的引用。如果页面上有多个图片,那么for循环中的document引用就会被执行多次甚至上百次,
每次都要进行作用域链查找。通过创建一个指向document对象的局部变量,就可以通过限制一次全局查找来改建这个函数的性能能:
function updateUI(){
var doc = document;
var imgs = doc.getElement.getElementsByTagName("img");
for(var i=0,len = imgs.length;i<len;i++){
imgs[i].title = doc.title + " image " + i;
}
var msg = doc.getElementById("msg");
msg.innerHTML = "Update complete.";
}
将在一个函数中会用到多次的全局对象存储为局部变量总是没错的。
- 避免with语句
大多数情况下,可以用局部变量完成相同的事情而不引入新的作用域。如下一个例子:
function updateBody(){
with(document.body){
alert(tagName);
innerHTML = "Hello World!";
}
}
这个with语句让document.body变得更容易使用,时期也可以使用局部变量达到相同的效果,如下所示:
function updateBody(){
var body = document.body;
alert(body.tagName);
body.innerHTML = "Hello World!";
}
这段代码通过将document.body存储在局部变量中省去了额外的全局查找。
2. 选择正确的方法
性能问题的一部分是和用于解决问题的算法或方法相关的。
- 避免不必要的属性查找
在计算机科学中,算法的复杂度是使用O符号来表示的。下表列出了javascript中常见的算法类型:
常数值,即O(1),指代字面值和存储在变量中的值。符号O(1)表示无论有多少个值,需要获取常量值的时间都一样。获取常量值是非常搞笑的过程。
请看下面代码:
var value = 5;
var sum = 10+value;
alert(sum);
该段代码的整体复杂度被认为是O(1)
在javascript中访问数据元素也是一个O(1)操作,和简单的变量查找效率一样。如:
var values=[5,10];
var sum = values[0] + values[1];
alert(sum);
使用变量和数组要比访问对象上的属性更有效率,后者是一个O(n)操作。对象上的任何属性查找都要比访问变量都要比访问变量或者数组
花费更长时间,因为必须在原型链中对拥有该名称的属性进行一次搜索。简而言之,属性查找越多,执行时间就越长。
var values = {first:5,second:10};
var sum = values.first + values.second;
alert(sum);
一两次属性查找并不会导致显著的性能问题,但成千上万次肯定会减慢执行速度。
注意获取单个值的多重属性查找。例如:
var query = window.location.href.substring(window.location.href.indexOf("?"));
在这段代码中有6次属性查找:只要数下代码中点的数量,就确定了属性查找的次数了。
这段代码由于两次用到了window.location.href,用样的查找用到了两次,因此效率特别不好。
解决方法:
一旦多次用到了对象属性,应该将其存储在局部变量中。第一次访问该值会是O(n),然而后续的访问都会是O(1),就会节省很多。
前面的代码可以写为:
var url = window.location.href;
var query = url.substring(url.indexOf("?"));
尽可能多的使用局部变量将属性查找替换为值查找。
- 优化循环
一般循环的基本优化步骤如下:
1)减值迭代--很多情况下,从最大值开始,在循环中布端减值的迭代器更加高效。
2)简化终止条件:由于每次循环过程都会计算终止条件,所以必须保证它尽可能快。
3)简化循环体:确保没有某些可以被很容易移出循环的密集计算。
4)使用后测试循环:最常用for和while循环都是前测试循环。而如do-while这种后测试的循环,可以避免最初终止条件的计算,因此计算更快。
ex:
for(var i=0;i<values.length;i++){
process(vaues[i]);
}
假设值的处理顺序无关紧要,那么循环可以改为i减值,如下:
for(var i=values.length;i>=0;i--){
process(values[i]);
}
在这个过程中将values.length的O(n)调用简化成了0的O(1)调用。
还可以进行进一步优化,不过循环还鞥改成后测试循环,如下:
var i = values.length;
do{
process(values[i]);
}while(i-- >=0);
使用前测试循环时必须保证要处理的值至少有一个。
- 展开循环
如果循环的次数确定,消除循环往往更快,如下:
//消除循环
process(values[0]);
process(values[1]);
process(values[2]);
这样展开循环可以消除建立循环和处理终止条件的额外开销,使代码运行更快。
如果循环中的迭代次数不能确定,那可以考虑使用一种叫做Duff装置的技术。Duff装置的基本概念是通过计算迭代的次数是否有8的倍数将一个循环展开为一系列语句。请看
一下代码:
//credit:Jeff Greenberg for JS implementation of Duff's Device
var iterations = Math.ceil(values.length/8);
var startAt = values.length %8;
var i = 0;
do{
switch(startAt){
case 0: process(values[i++]);
case 7: process(values[i++]);
case 6: process(values[i++]);
case 5: process(values[i++]);
case 4: process(values[i++]);
case 3: process(values[i++]);
case 2: process(values[i++]);
case 1: process(values[i++]);
}
startAt =0;
}while(--iterations >0)
例如:如果数组中有10个值,startAt则等于2,那么最开始的时候Process()则只会被调用2次。在接下来的循环中,startAt被重置为0,
这样之后的每次循环都会调用8次process().展开循环可以提升大数据集的处理速度。
更快的Duff装置技术,将do-while循环分为2个单独的循环。
//credit : Speed Up Your Site(New Riders,2003)
var iterations = Math.floor(values.length/8);
var leftover = values.length %8;
var i=0;
if(leftover >0){
do{
process(values[i++];
}while(--leftover >0)
}
do{
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
}while(--iterations>0)
在这个实现中,剩余的计算部分不会再实际的循环中处理,而是子啊一个初始化循环中进行除以8的操作。当处理掉额外的元素,继续执行每次调用8次process()
的主循环。这个方法几乎比原始的Duff装置实现快乐40%。
- 避免双重解释
当使用eval()函数或者是Function构造函数以及使用setTimeout()传递一个字符串参数时都会发生双重解释惩罚。
如下所示:
//某些代码求值
eval("alert('Hello world!')");
//创建新函数
var sayHi = new Function("alert('Hello World!')");
//设置超时
setTimeout("alert('Hello world')",500);
解析包含了javascript代码的字符串,这个操作是不能在初始的解析过程中完成的,必须重新启动一个解析器来解析新的代码。实例化一个新的
解析器有不容忽视的开销,因此比直接解析慢得多。
只有极少的情况下eval()时必须的,所以尽可能避免使用。上面的代码可以改写为:
//已修正
alert("Hello world;");
//创建新函数---已修正
var sayHi = function(){
alert('Hello world!');
}
//设置一个超时--已修正
setTimout(function(){
alert("Hello world!");
},500);
如果要提升系能,尽可能避免出现需要按照JavaScript解释的字符串。
- 性能的其他注意事项
下面并非主要的问题,不过如果使用得当也会有相当大的提升。
1) 原生方法较快:原生方法是诸如c/c++之类的编译型语言写出来的,所以要比javascrit的快的多。
2) Switch语句较快:如果有一系列复杂的if-else语句,可以转换成单个switch语句则可以得到更快的代码。还可以通过将case语句
按照最可能到最不可能的顺序进行组织,来进一步优化switch语句。
3)位运算符较快:当进行数学运算的时候,位运算符要比任何布尔运算符或蒜素运算快。选择性地用位运算替换算数运算可以极大提升复杂计算的性能。
诸如:取模,逻辑与和逻辑或都可以考虑用位运算来替换。
3. 最小化语句数
语句数量也会影响到所执行的操作的速度。
参考模式可以确认那些语句可以组合在一起,如下:
- 多个变量声明
在javascript中所有的变量都可以使用单个var语句来声明,如下所示:
//一个语句
var count = 5,
color = "blue",
values = [],
now = new Date();
比单个变量分别声明快很多。
- 插入迭代值
使用迭代值(在不同的位子进行增加或减少的值)的时候尽可能合并语句,如下:
var name = values[i];
i++;
可以合并为一句:
var name = values[i++];
- 使用数组和对象字面量
前面有两种创建数组和对象的方法:使用构造函数或者是使用字面量。
构造函数总是要用到更多的语句来插入元素或者定义属性,而字面量就可以将这些操作在一个语句中完成,请按下例:
//用4个语句创建和初始化数组--浪费
var values = new Array();
valus[0] = 123;
valus[1] = 456;
valus[2] = 789;
//用4个语句创建和初始化对象-浪费
var person = new Object();
person.name = "nicholas";
person.age = 29;
person.sayName = function(){
alert(this.name);
}
以上代码,其实很容易转换成使用字面量的形式,如下:
//只用一条语句创建和初始化数组:
var values = [123,456,789];
//只用一条语句创建和初始化对象
var person = {
name : "Nicholas",
age : 29,
sayName : function(){
alert(this.name);
}
};
只要有可能,尽量使用数组和对象的字面量表达式来消除不必要的语句。
4. 优化DOM交互
在js中,DOM是最慢的一部分。dom操作和交互消耗大量的时间,因为他们往往需要重新渲染整个页面或者某一部分。
理解如何优化与dom的交互可以极大得提高脚本完成的速度。
- 最小化现场更新
现场更新:一旦你需要访问的是DOM部分是已经显示的页面的一部分,那么就可以在进行一个现场更新。之所以叫现场更新,
是因为需要立即对页面对用户的显示进行更新。每一个更改,不管是插入单个字符,还是移出整个片段,都有一个性能惩罚。
以为内浏览器要重新计算无数尺寸以进行更新。现场更新进行的越多,代码完成执行所需的时间就越长;如下例:
var list = document.getElementById("myList");
for(var i=0;i<10;i++){
var item = document.createElement("li");
list.appendChild(item);
list.appendChild(document.createTextNode("Item "+i);
}
这段代码,有10个项目,共有20个现场更新。
要修正这个性能瓶颈,需要减少现场更新的数量。方法有二:
1)将列表从页面上移出,最后进行更新,最后再将列表插回同样的位置。但引起不必要的闪烁。
2)使用文档碎片来构建DOM结构,接着将其添加到List元素中。这个方法避免了现场更新和页面闪烁问题。
看如下内容:
var list = document.getElementById("mylist");
var fragment = document.createDocumentFragment();
for(var i=0;i<10;i++){
var item = document.createElement("li");
fragment.appendChild(item);
item.appendChild(document.createTextNode("Item "+i);
}
list.appendChild(fragment);
在这个例子中只有一次现场更新,它发生在所有项目都创建好之后。文档碎片用作一个临时的占位符,放置新创建的项目。
然后使用appendChild()将所有项目添加到列表用。记住,当给appendChild()传入文档碎片时,只有碎片中的子中的子节点被添加到目标,
碎片本身不会被添加。
一旦需要更新dom,请考虑使用文档碎片来构建DOM结构,然后再将其添加到现存的文档中。
- 使用innerHTML
有两种在页面上创建dom节点的方法:
* 诸如createElement()和appendChild()之类DOM方法
* 以及使用innerHTML
对于小的dom更改而言,两种方法效率差不多。然而,对于大的dom更改,使用innerHTML
更快。‘
把innerHTML设置为某个值时,后台会创建一个HTML解析器,然后使用内部的DOM调用来创建DOM结构
,而非基于js的dom调用。对于内部方法是编译好的而非解释执行的,所以执行要快的多。
前面的例子用innerHTML改写如下:
var list = document.getElementById("myList");
var html = "";
for(var i=0;i<10;i++){
html += "<li>Item "+ i +"</li>";
}
list.innerHTML = html;
虽然字符串连接上总是有点性能损失,单这种方式还是要比多个DOM操作更快。
使用innerHTML的关键在于(和其他DOM操作一样)最小化调用它的次数。例如:
var list = document.getElementById("myList");
for(var i=0;i<10;i++){
list.innerHTML += "<li>Item "+ i +"</li>"; //避免!!!
}
调用innerHTML实际上就是一次现场更新,所以也要如此对待。
- 使用事件处理
在用户交互上大量用到事件处理程序。页面上的事件处理程序的数量和页面相应用户交互的速度之间有个负相关。事件代理
可以减轻这种惩罚。
事件代理用到了12章讲的事件冒泡。任何可以冒泡的事件可以在目标的任何祖先节点上处理。使用这个知识,就可以将事件处理
程序附加到更高层的地方负责多个目标的事件处理。如果可能,在文档级别附加事件处理程序,这样可以处理整个页面的事件。
- 注意NodeList
NodeList对象的陷阱对于web应用的性能而言是巨大的损害。
也许优化NodeList访问最重要的地方就是循环了。前面提到过将长度计算
移入到for循环的初始化部分。现在看一个例子:
var images = document.getElementsByTagName("img");
for(var i=0,len=images.length;i<len;i++){
//处理
}
这里的关键在于长度length存入了len变量,而不是每次都去访问NodeList的属性。当在循环
中使用NodeList的时候,下一步应该是获取要使用的项目的引用,如下所示:以便避免在循环体内多次调用NodeList.
var images = document.getElementsByTagName("img");
for(var i=0,len = images.length; i<len;i++){
var image = images[i];
//处理
}
编写js的时候,一定要知道何时返回NodeList对象,这样你就可以最小化对他们的访问。发生
以下情况时会返回NodeList对象:
* 进行了对getElementsByTagName()的调用;
*获取了元素的childNodes属性;
*获取了元素的attributes属性;
*访问了特殊的集合,对document.forms,document.images等等。
要了解当使用NodeList对象时,合理使用会极大提升代码执行速度。