原文链接:https://dev.opera.com/articles/efficient-javascript/?page=2#primitiveo...
高效的JavaScript
曾经,一个Web页面不会包含太多的脚本,或者至少来说,它们不会影响页面的性能。然而,现在的Web页面越来越像本地运用了,脚本的性能成了一个很大的影响随着越来越多的运用转向使用Web技术时,提高页面的性能成为了越来越重要的问题。
ECMAScript
避免使用eval
和Function构造函数
每次当eval
和Function constructor
通过字符串源码形式调用时,脚本引擎必须开启转换机制将字符串源码转换成可执行的代码。通常来说这是比较耗性能的。
eval
调用特别的不好,当执行的内容字符串传递给eval
时不能被提前执行,由于代码执行会被eval
中执行的内容影响,那就意味着编译器不能更好的优化执行上下文,并且浏览器在运行时时放弃执行下面的上下文。这样就增加了额外的性能影响。
对于Function constructor
来说,它的名声和eval
一样也不太好,虽然使用它并不会影响上下文的执行,但是它执行的效率却很低下。示例代码如下:
错误的使用eval
:
function getProperty(oString) {
var oReference;
eval('oReference = test.prop.' + oString);
return oReference;
}
正确的姿势:
function getProperty(oString) {
return test.prop[oString];
}
错误的使用Function constructor
:
function addMethod(oObject, oProperty, oFunctionCode) {
oObject[oProperty] = new Function(oFunctionCode);
}
addMethod(
myObject,
'rotateBy90',
'this.angle = (this.angle + 90) % 360'
);
addMethod(
myObject,
'rotateBy60',
'this.angle = (this.angle + 60) % 360'
);
正确的姿势:
function addMethod(oObject, oProperty, oFunction) {
oObject[oProperty] = oFunction;
}
addMethod(
myObject,
'rotateBy90',
function() {
this.angle = (this.angle + 90) % 360;
}
);
addMethod(
myObject,
'rotateBy60',
function() {
this.angle = (this.angle + 60) % 360;
}
);
避免使用with
尽管对于开发人员来说,使用with
比较方便,但是对性能来说,却是非常消耗的。原因是对脚本引擎来说,它会拓展作用域链,而查找变量的时候不会判断是否被当前引用。尽管这种情况带来性能的开销比较少,但是每次编译的时候我们都不知道内容的作用域,那就意味着编译器不能对其进行优化,所以它就和普通的作用域一样。
一种更有效的方法代替方法是使用一个对象变量来代替with
的使用。属性的访问可以通过对象的引用来实现。这样工作起来非常有效,如果属性不是基本类型外,比如字符串和布尔值。
考虑下面的代码:
with(test.information.settings.files) {
primary = 'names';
secondary = 'roles';
tertiary = 'references';
}
使用下面的方式效率更高:
var testObject = test.information.settings.files;
testObject.primary = 'names';
testObject.secondary = 'roles';
testObject.tertiary = 'references';
不要在循环的函数里面使用try-catch-finally
try-catch-finally
语句相对于其他的语句来说它的结构非常唯一的,当脚本运行的时候它会在当前的作用域总创建一个变量,这发生在catch
语句调用的时候.捕获的异常对象会关联这个变量,这个变量不会存在其他的脚本里,即使是相同的作用域。它在catch
语句开始的时候创建,在执行结束的时候销毁它。
因为这个变量会在运行的时候创建和销毁,所以会产生一种特殊的情况,一些浏览器不能及时的在性能比较耗的循环中及时捕获该句柄。所以会导致一些性能上的问题当异常被捕获的时候。
如果可能的话,异常的执行应该在更高的级别执行,这样它就不会频繁的出现,或者通过检查期望的动作最早被允许的话来避免,下面通过示例来说明:
错误的使用方式:
var oProperties = [
'first',
'second',
'third',
…
'nth'
];
for(var i = 0; i < oProperties.length; i++) {
try {
test[oProperties[i]].someproperty = somevalue;
} catch(e) {
…
}
}
在许多情况下,try-catch-finally
结构可以移动到循环的外围, 这样看起来似乎语意上有点改变。由于异常抛出时,循环会被中断,但是下面的代码会依然执行。
var oProperties = [
'first',
'second',
'third',
…
'nth'
];
try {
for(var i = 0; i < oProperties.length; i++) {
test[oProperties[i]].someproperty = somevalue;
}
} catch(e) {
…
}
在某些情况下,try-catch-finally
结构可以避免使用。比如:
var oProperties = [
'first',
'second',
'third',
…
'nth'
];
for(var i = 0; i < oProperties.length; i++) {
if(test[oProperties[i]]) {
test[oProperties[i]].someproperty = somevalue;
}
}
隔离eval
和with
的使用
由于这些结构影响性能如此的深,所以它们使用的越少越好。但是有时候你可能需要它们,如果一个函数被调用或者一个循环重复的被计算,最好的方式还是避免使用这些结构,他们最好的解决方案就是被执行一次,或者极少数,以至于基本上对性能没什么影响。
避免使用全局变量
在全局范围内创建一个变量是很诱惑的,只因它创建的方式很简单,但是有一下几个原因会导致脚本运行变慢。
首先,全局变量需要脚本引擎查找到最外的作用域,查找速度比较慢,第二,全局变量通过window对象被分享,意味着本质上它有两层作用域(??)。
注意对象的转换
字面量,比如字符串,数字或者布尔值,在ECMAScript
中有两种表现,它门可能被当作单纯的值或者一个对象。
任何属性或者方法被调用的时候针对的是这个对象,不是这个值,当你引用一个属性或者方法的时候,ECMAScript
引擎会暗中的创建一个你值对应的字符串对象。在方法调用之前。这个对象只会被请求一次,当你尝试下一次调用该值的某个方法时它又会被创建一次。来看看下面的例子:
var s = '0123456789';
for(var i = 0; i < s.length; i++) {
s.charAt(i);
}
上面的例子需要脚本引擎创建21次字符串对象,一次length
属性的访问,一次charAt
方法的调用。
优化的方案如下所示:
var s = new String('0123456789');
for(var i = 0; i < s.length; i++) {
s.charAt(i);
}
和上面等效,但是仅仅手动创建了一个对象,性能上要比上面的好很多。
注意:不同的浏览器,对于装箱和拆箱的优化不一样。
避免咋性能堪忧的函数里使用for-in
迭代
for-in
迭代有它自己的特点,但是它经常被滥用,这种迭代需要脚本引擎创建一个所有可枚举属性的清单,并检出为当作副本,在开始枚举的时候。
使用字符串的累加形式。
字符串的拼接是个昂贵的过程,当使用"+
"运算符时,它不会把结果立即添加到变量中,反而它会创建一个新的字符串对象在内存中,并把得到的结果赋值个这个字符串。然后这个新的字符串对象在赋值给变量。但是使用"+=
"可以避免这样的过程。
原始的操作符可能比函数调用更快
示例:
var min = Math.min(a,b);
A.push(v);
下面的方式和上面的等效,但是效率更高:
var min = a < b ? a : b;
A[A.length] = v;
传递一个回调函数而不是字符串给setTimeout()
和setInterval()
当setTimeout()
和setInterval()
方法传递的是个字符串时,它内部会调用eval
,所以会导致性能上的问题。