作用域
JavaScript执行时会构建一个作用域链,用来进行变量解析,全局执行上下文只有一个object变量,定义了js中所有变量和函数。当创建一个函数时,会出现产生一个新的局部作用域,以this、arguments和命名的形参以及其他局部变量和函数初始化。整个作用域链的顶端是活动的作用域,变量解析的顺序是首先从当前作用域查找,当查找到结果后就终止查询,否则就继续往上一层作用域查询,直到全局作用域为止。因此,一个比较大的变量如果在解析的过程中跨越多层作用域,就会比较影响性能。作用域在性能上的改进就是要减少跨越的层级。
使用局部变量
对于读写局部变量的速度是最快的,他们存在当前激活的作用域下,不过这个特性在Google的chrome浏览器和Safari4+的浏览器上并不明显,因为他们使用的v8和Nitro引擎的变量解析速度非常快,变量解析的层数的减少在速度上的提升较少。但是其他浏览器都是非常
明显的,特别是IE8、FireFox浏览器。
一个好的原则是:将任何不在当前作用域内的变量,且要使用2次及以上的,使用局部变量进行存储。
作用域变换
with语句在JavaScript中会将当前执行的作用域进行切换,在这个语句块内可以访问相应的对象属性。但实际上,这种方式是在执行上下文的作用域链前面插入了新的作用域,因此,当开始进入到with语句中时,对于当前执行上下文中的局部变量的解析会跨越多个作用域,会造成性能上的损失。因此需要避免使用with语句。
数据访问
读取和写入数据的方式一共有下面四种:
- 字面量
- 变量
- 数组
- 对象属性
字面量和局部变量基本可以忽略,对这些数据的访问基本对性能的提升占比可以不考虑。真正的区别是从数组和对象的属性中读取和写入数据。从这两个方式获取数据需要进行位置的搜索,包括使用索引下标和属性名称。在进行搜索时,最耗时的就是对象的属性,特别是多级属性的寻找非常耗时。
针对这个问题,最佳的方法就是使用局部变量将任何使用1次及以上的对象属性或者数组元素存储下来。
//原始版本
function procee(data){
if(data.count > 0)
for(var i = 0; i < data.count; i++)
processData(data.item[i]);
}
//优化版本
function(){
var cnt = data.count;
if(cnt > 0)
for(var i = 0; i < cnt; i++)
processData(data.item[i]);
}
另外,在处理与DOM相关的对象时,使用局部变量存储特别重要,因为每个与DOM相关的对象的属性访问实际上都是一个DOM查询,需要尽量减少这种大的对象的属性访问,必要使用局部变量暂存。
流程控制
条件语句
当使用多个离散值进行比较时,使用if语句可能会造成比较次数较多,因此使用switch比较好。对于if语句可以使用二分法进行分裂也是一种加强方法,同时可以通过统计每个比较值出现的频率,将较大的频率的值放在靠前的位置。
对于上述的变更,可以使用数组下标的索引来进行快速选择,而不用进行一个一个比较。但是这个必须要保证离散值的个数不能太多,而且可以用索引值代替。
循环
JavaScript中的for、while和do-while三种循环,这些循环中的条件中如果存在对象的属性,要使用局部变量暂存,否则随着循环次数的增加,每次访问对象属性会非常耗时。
另一个加快循环的技巧就是讲循环的遍历顺序按照逆序的方式进行,这样会比顺序遍历节省50%左右的时间(取决于循环内处理的复杂性)。
JavaScript中还有一个单独为了遍历一个对象的各个属性使用的for-in循环,在遍历一个位置属性的对象时,这是非常有效的方法,但是这个方法比普通的循环慢很多,因为它要遍历对象的原型和整个原型链上的所有属性,这是非常耗时的。对于已知对象,使用三种普通的循环语句代替for-in是非常好的选择。
打散循环:
对于循环次数较少的循环语句,可以直接重复调用循环快语句来消除循环语句。但是对于次数较多的循环,Tom Duff最先在C语言中提出了打散循环的方法,被称为Duff’s Device。JavaScript语言的第一版发布者Jeff Greenberg改写了JavaScript版本的Duff’s Device:
var iter = Math.ceil(values.length / 8);
var left = values.length % 8;
var i = 0;
if (left > 0){
do{
process(values[i++]);
}while(--left > 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(--iter > 0);
上述方法将循环次数减少到除8的个数,这个数值8是多次试验得到的最优值。当遍历对象的个数特别大时,使用上述方法优化之后会比普通循环快很多。
字符串优化
字符串连接
所有浏览器在处理20个字符以下,1000个连接操作以下的情形时,都是非常快速的,这种情况下一般不需要考虑。
当字符串过长,或者连接操作太多时,就需要考虑性能问题。对于IE浏览器,字符串连接的运算符“+”会比较损耗性能,因此一般使用如下方式进行优化:
var buf = [], i = 0;
buf[i++] = "Hello";
buf[i++] = " ";
buf[i++] = "world";
var text = buf.join("");
但是,FireFox开始引入浏览器对字符串操作的优化之后,Chrome、Opera、Safari以及IE 8+都对字符串操作做了优化,因此不必考虑上述方式。特别地,Chrome和Opera优化之后的“+”操作符非常的快速。
字符串的trim操作
由于JavaScript没有原生的trim函数,因此一般都是用正则表达式进行上述操作:
function trim (s){
return s.replace(/^\s+|\s+$/g, "");
}
这里的正则表达式使用了或者,实际上有两个模式,同时还有一个g修饰符,表示全局搜索。为了加快速度,Steven Levithan给出了优化的trim函数:
function trim(s){
s = s.replace(/^\s+/, "");
for(var i = s.length - 1; i >= 0; i--){
if (/\S/.test(s.charAt(i))){
s = s.substring(0, i+1);
break;
}
}
return s;
}
这里简化了正则表达式,同时对于末尾空字符使用逐个检查的方式去除。
上述这些优化的前提都是他们使用的频率非常大时,对性能的提升才比较大。另外,新的ECMAScript 3.1定义了内置的trim函数,速度会非常快。