当今的Web应用程序都是由大量的JavaScript代码驱动。每次用户在界面上操作时,都要执行成千上万行的Js代码。因此,关注性能,不仅要关注页面的加载时间,也要关注在页面上操作时的响应速度。要实现快速而友好的用户界面,最好的办法就是编写的JavaScript代码能在浏览器中高效的运行。
在编写代码的过程中,如果多注意一下自己的代码质量,就可以得到更流畅的用户体验。身为一名前端开发攻城狮,一手漂亮的代码也是成长进阶的必备技能。下面是看书的几点总结:
到目前为止,局部变量是JavaScript中读写最快的标识符。因为它存在于执行函数的活动对象中(上一篇文章已经进行总结),解析标识符只需要查找作用域链中的单个对象。读取变量值的总耗时随着查找作用域链的逐层深入而不断增长,所以标识符越深读取速度越慢。在编写代码时,能用局部变量的就尽量使用局部变量,不但可以提升代码执行速度,还可以避免全局变量的污染,一举两得。
数据在脚本中存储的位置直接影响脚本执行的总耗时。一般而言,在脚本中有4种地方可以存取数据:
字面量值;
变量;
数组元素;
对象属性;
在大多数浏览器中,从字面量中读取值和从局部变量中读取值的开销差异很小,以至于可以忽略;真正的差异在于从数组或对象中读取数据。存储这些数据结构中的某个值,需要通过索引(对于数组)或属性值(对于对象)来查询数据存储的位置。请看下面代码:
function process(data){
if(data.count > 0){
for(var i = 0; i< data.count ; i++){
processData(data.item[i]);
}
}
}
这段代码多次读取data.count,如果将值存储到局部变量中,函数将运行的更快:
function process(data){
Var count = data.count;
if(count > 0){
for(var i = 0; i < count ; i++){
processData(data.item[i]);
}
}
}
改写后的执行过程中读取data.count仅有一次。由于减少了查找对象属性的次数,函数的执行效率将比之前的更高(当然还可以进一步优化,将在下面进行说明)。随着数据结构的增加,它对数据存储速度的影响也跟着变大,例如,存取data.count比data.item.count快,存储data.item.count比data.item.subitem.count快。在处理属性时,点符号(.)的使用次数直接影响存取该属性的总耗时。
在处理HTMLCollection对象(比如getElementsByTagName这样的DOM方法返回的值,或element.childNodes这样的属性值)时使用局部不变量特别重要。实际上,每次存取HTMLCollection对象的属性时,都会对DOM文档进行动态查询。例如:
var divs = document.getElementsByTagName("div");
for (var i = 0; i< divs.length ; i++){ //避免!
var div = divs[i];
process(div);
}
这段代码每次通过属性名或索引存取divs属性时,DOM实际上重复执行了一次对页面上所有
var divs = document.getElementsByTagName("div");
for( var i = 0, len = divs.length; i< len; i++ ){ //更好的方式
var div = divs[i];
process(div);
}
这样就减少了直接存取该对象的次数。
除了数据存取之外,流控制或许是提升JavaScript性能最重要的一环。在一个if-else语句中,语句的执行流越深,需要判断的条件就越多。
if语句:看下面的例子:
if(value == 0){
return result0;
}else if(value == 1){
return result1;
}else if(value == 2){
return result2;
}else if(value == 3){
return result3;
}else if(value == 4){
return result4;
}else if(value == 5){
return result5;
}else if(value == 6){
return result6;
}else if(value == 7){
return result7;
}else if(value == 8){
return result8;
}else if(value == 9){
return result9;
}else {
return result10;
}
通常这样类型的结构是不被推荐的,它的主要问题在于:语句的执行流越深,需要判断的条件就越多。当value为9是,执行完成的事件会比value为0时要长,因为它之前的所有条件都需要判断。尽管使用大量的if条件语句是不可取的,但我们可以采取以下几个方法来提高整体性能:
第一种方法是将条件按照频率降序排列。由于最快的运算在第一个条件语句后就退出,所以要确保这种情况尽可能多地出现。假设前面例子中的最常见的情况时value 等于5,其次是value等于9,这就意味着,执行流在达到最常见的情况之前要进行5次条件判断,在达到第二常见的情况之前要进行9次判断。尽管按数字递增的方式排序更容易阅读,但实际上把它写成下面这个样子执行效率更高:
if(value == 5){
return result5;
}else if(value == 9){
return result9;
}else if(value == 0){
return result0;
}else if(value == 1){
return result1;
}else if(value == 2){
return result2;
}else if(value == 3){
return result3;
}else if(value == 4){
return result4;
}else if(value == 6){
return result6;
}else if(value == 7){
return result7;
}else if(value == 8){
return result8;
}else{
return result10;
}
另一种方法是优化if语句的条件,将条件分成几个分支,下面的二分查找算法可以逐步找出有效的条件,在条件数量众多,且没有出现频率特别高的条件,从而不能简单的按频率排列的情况下,这个方法可取。
if(value < 6){
if(value < 3){
if(value == 0){
return result0;
}else if(value == 1){
return result1;
}else if(value == 2){
return result2;
}
}else{
if(value == 3){
return result3;
}else if(value == 4){
return result4;
}else{
return result5;
}
}
}else{
if(value < 8){
if(value == 6){
return result6;
}else{
return result7;
}
}else{
if(value == 8){
return resdult8;
}else if(value == 9){
return result9;
}else{
reutrn result10;
}
}
}
这段代码确保在任何情况下,都不会超过4次的条件判断。
然而问题依然存在,每增加一个条件最终都会导致执行时间变长,这影响性能和可维护性。这是switch语句就有了用武之地。
switch语句:switch语句简化了多重条件判断的结构,并提升了性能:
switch(value){
case 0:
return result0;
break;
case 1:
return result1;
break;
case 2:
return result2;
break;
case 3:
return result3;
break;
case 4:
return result4;
break;
case 5:
return result5;
break;
case 6:
return result6;
break;
case 7:
return result7;
break;
case 8:
return result8;
break;
case 9:
return resutl9;
break;
default:
return result10;
}
Firefox可以很好的处理switch语句,每个条件判断执行的时间大致相同,与他们定义的顺序无关。然而其他浏览器做的不怎么好。在IE、Opera、Safari和Chrome中,switch语句越深,执行的时间增加越明显。不过这比每个条件都用if语句要增加的少。
在JavaScript中,当仅判断一两个条件时,if语句通常比switch语句更快。当有两个以上条件且条件比较简单(不是进行范围判断)时,switch语句往往更快。这是因为大多数情况下,switch语句中执行单个条件所需时间比在if语句中短。所以当有大量的条件判断时,使用switch语句更合适。
数组查询:在JavaScript中,处理条件判断的解决方案不止两种。除了if语句和switch语句外,还有第三种方法:在数组中查询值:
//定义数组results
var results = [result0,result1,result2,result3,result4,result5,result6,result7,result8,result9,result10];
//返回正确结果
return results[value];
不使用条件语句,而是把所有的结果都存储在数组中,并用数组的索引映射value变量。检索对应的结果就是简单的查询数组值。虽然查询数组的耗时也会随进入数组的深度而增加,但是和每个条件都用if语句或者switch语句来判断相比,增加的时间还是要小得多。使用数组查询的理想情况时有大量的条件存在,并且这些条件都能用数字或字符串(对于字符串,可以使用对象而非数组来存储)这样的离散值来表示。
使用数组查询少量的结果是不合适的,因为数组查询往往比少量的条件判断语句慢。当要查询的条件范围很大时,数组查询才会变得非常有用。
最快的条件判断
前面列出的3种技术:if语句、switch语句和数组查询,在代码优化执行方面都有各自的用处:
两个之内的离散值需要判断;
大量的值能容易地分到不同的区间范围中。
超过两个而少于10个离散值需要判断;
条件值是非线性的,取法分离出区间范围。
超过10个值需要判断;
条件对应的结果是单一值而不是一些列操作。
在 JavaScript中,循环是导致性能问题的常见起因,编写循环的方式能彻底改变它的执行时间。看下面的例子:
//未优化的代码
var values = [1,2,3,4,5];
//for循环
for(var i = 0; i< values.length; i++){
process(values[i]);
}
最显眼的是循环中反复比较变量与数组的长度,在前面的例子中已经提到,查找属性要比存取局部变量更耗时优化一下是这样:
//第一步优化
var values = [1,2,3,4,5];
var length = values.length;
for(var i = 0; i< length; i++){
process(values[i]);
}
另外一种提高性能的简单方式是将循环变量递减到0,而不是递增到总长度。根据每个循环的复杂度不同,这个简单的改变可以比原来节约多达50%的执行时间。例如:
var vaules = [1,2,3,4,5];
var length = values.length;
for(var i = length; i--; ){
process(values[i]);
}
因为结束条件被改造为与0进行比较(注意一旦循环变量等于0,结束条件就会变为假)。执行效率更高了。
避免 使用for-in循环
JavaScript中的for循环,while循环,do-while循环的执行效率差不多。但是for-in循环的效率差异还是很大的。它用来遍历JavaScript对象的可枚举属性。典型的使用方法:
for(var prop in object){
if(object.hasOwnProperty(prop)){ //确保只处理实例自身的属性
process(object[prop]);
}
}
这段代码遍历了给定对象的属性。它的结束条件无法改变(除非使用throw抛异常,然后手动捕获),而且遍历属性的顺序也无法改变。此外for-in循环需要从对象中解析每个可枚举的属性,为了提取这些属性需要检查对象的原型和整个原型链。遍历原型链就像遍历作用域链,它会增加耗时,从而降低整个循环的性能。因此尽可能地避免使用for-in循环可以让代码执行的更快速。