本文深入解析JavaScript性能优化的两个核心维度:作用域链管理与流控制。通过缩短作用域链、避免with语句以及合理使用局部变量,可显著提升变量解析速度。同时,对比switch、数组映射与条件判断的性能差异,并演示递减循环与缓存length属性的技巧,帮助开发者编写更高效的代码。
在前文《JavaScript提高网站性能的几点建议(一)》中,我们从HTTP请求到页面渲染等多个层面探讨了提升网站性能的方法。本文基于Steve Sounders的著作《高性能网站建设进阶指南》,聚焦于JavaScript层面的性能优化进行总结,供各位开发者参考借鉴。
JavaScript的性能表现是构建高效Web应用的核心要素。
——Steve Sounders
利用js作用域链优化性能
1 利用js作用域链
作用域链(scope chain)
当执行JavaScript代码(无论是全局代码还是函数内部代码)时,JavaScript引擎会创建对应的执行上下文(Execution Context)。页面加载完成后,首先会生成一个全局作用域。随后每执行一个函数,就会建立一个新的对应作用域,从而形成一条作用域链。每条作用域链的头部指向全局作用域,尾部指向当前函数的作用域。
作用域链的主要功能是解析标识符。当函数被创建(而非执行)时,this、arguments、命名参数以及该函数内的所有局部变量都会被添加到当前作用域中。当JavaScript引擎需要查找变量X(即变量解析过程)时,它会先从作用域链的尾部(当前作用域)开始查找是否存在X。若未找到,则沿着作用域链向上查找,直至到达链头(全局作用域)。如果在全局作用域中仍未找到该变量,引擎会抛出ReferenceError异常,表明该变量在当前作用域链中不存在。
合理管理作用域链的深度是一种低成本且高效提升性能的方法。我们应尽量避免因无意中增加作用域链长度而导致执行速度下降。
使用局部变量(尽量缩短作用域链)
理解了作用域链的概念后,我们应当意识到JavaScript引擎解析变量所需的时间与作用域链的深度直接相关。显而易见,局部变量位于作用域链的尾部,因此其存取速度最快。一个良好的编程经验是:当非局部变量被使用超过一次时,应使用局部变量将其缓存下来。例如:
function changeDiv(){
document.getELementById('myDiv').className = 'changed';
document.getELementById('myDiv').style.height = 150;
}
在上述代码中,myDiv这个DOM元素被引用了两次。为了加快引用速度,我们应当使用一个局部变量来存储它。这样做不仅缩短了作用域链,还避免了重复查询DOM元素带来的性能损耗:
function changeDiv(){
var myDivStyle = document.getElementById('myDiv').style;
myDiv.className = 300;
myDiv.style.height = 150;
}
避免使用with(不要增长作用域链)
通常情况下,代码执行过程中函数的作用域链是固定的。然而,with语句可以临时延长函数的作用域链。with用于将对象属性作为局部变量来处理,以便更便捷地访问它们,例如:
var user = {
name:'vicfeel',
age:'23'
};
function showUser(){
var local = 0;
with(user){
console.log("姓名" + name);
console.log("年龄" + age);
console.log(local);
}
}
showUser();
在这个例子中,通过with语句在showUser作用域链的尾部增加了一个临时作用域,该作用域存储了user对象的所有属性。这意味着作用域链被延长了,导致局部变量(如local)从链尾的第一个对象变成了第二个,从而减慢了标识符的存取速度。直到with语句执行完毕,作用域链才会恢复原状。鉴于with语句的这一缺陷,我们应尽量避免使用它。
更合理的流控制策略
2 更合理的流控制
JavaScript与其他编程语言一样,包含多种流控制语句(如循环、条件判断等)。在各个环节使用恰当的语句能极大提升脚本的运行速度。
快速条件判断
谈及条件判断,首先需要避免的一种使用方式是:
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{
return result7;
}
这种使用if进行多层条件判断的方式主要问题在于嵌套层次过深。当我们需要value = 7时,其消耗的时间远大于value = 0的情况,这不仅严重损耗性能,还导致代码可读性极差。
一种更优的方式是利用switch进行判断。
swithc(value){
case 0:
return result0;
case 1:
return result1;
case 2:
return result2;
case 3:
return result3;
case 4:
return result4;
case 5:
return result5;
case 6:
return result6;
default:
return result7;
}
这种方式不仅提高了代码的可读性,查询速度也比if更快。不过需要注意的是,如果条件数量仅为一两个,if语句的执行速度反而比switch更快。
在JavaScript中,条件查询还有另一种实现方式。之前的例子是根据值返回不同的结果,我们可以利用数组来实现类似哈希表的映射查询。
//定义数组
var results = [result0,result1,result2,result3,result4,result5,result6,result7];
//查询结果
return results[value];
这种基于数组的方式在查询范围较大时更为有效,因为它无需检测上下边界,只需填入索引值即可查询。其局限性在于条件对应的是单一值,而非一系列操作。因此,需结合实际情况选择合适的条件判断方式,以最大化性能。
快速循环
JavaScript中存在四种循环方式:for循环、do-while循环、while循环和for-in循环。下面是一个常用的循环使用示例:
var values = [1,2,3,4,5];
for(var i = 0;i < values.length;i++){
process(values[i]);
}
我们可以看到,这段代码最明显的优化点在于values.length。每次循环中,i都要与values的长度进行比较,而查询属性比访问局部变量更耗时。如果循环次数越多,这种耗时就越明显。因此,可以这样优化:
var values = [1,2,3,4,5];
var length = values.length;//局部变量存储数组长度
for(var i = 0;i < length;i++){
process(values[i]);
}
这段代码还可以进一步优化,即将循环变量递减至0,而不是递加到总长度。
var values = [1,2,3,4,5];
var length = values.length;
for(var i = length;i--;){ //递减到0
process(values[i]);
}
这里将循环结束条件改为与0比较,因此每个循环的执行速度更快。根据循环复杂度的不同,这种简单的改变可以比原来节省大约50%的时间。