JavaScript性能优化:(八)编程实践

每个语言都有它的“痛点”,并且随着时间的推移,它存在的低效模式也不断发展。在精华与糟粕并存的世界中,最佳实践的地位不言而喻。

避免双重求值

  1. 避免使用 eval()Function()
  2. 对于 setTimeout()setInterval(),建议传入函数而不是字符串作为第一个参数

使用字面量方式创建对象和数组

使用对象/数组字面量是创建对象/数组的最快方式,运行速度更快,而且代码量也少。

避免重复工作

如同字面意思,在实际编写代码时,我们应该尽可能地避免重复做已经做过的工作。

下面是用于浏览器探测的函数:

1
2
3
4
5
6
7
function addHandler(target, eventType, handler) {
if (target.addEventListener) {
target.addEventListener(eventType, handler, false);
} else {
target.attachEvent('on' + eventType, handler);
}
}

这个函数隐藏的性能问题在于每次函数调用时都做了重复工作,因为每次调用时其都会经过检查指定方法是否存在。理想状态下,在第一次调用之后就已经知道了当前浏览器所使用的方法,那么后续函数调用就不需要再次检查方法是否存在了。但是,这个函数没能做到这一点,所以每次调用它都是在重复相同的工作,这是极大的资源浪费。

下面以这个函数为例,看一下避免重复工作的几种解决方案。

延迟加载

延迟加载意味着信息被使用前不会做任何操作。以之前的浏览器探测代码为例,在函数被调用前,没有必要判断该调用哪个方法去绑定事件处理器。使用了延迟加载技术的函数版本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
function addHandler(target, eventType, handler) {
if (target.addEventListener) {
addHandler = function(target, eventType, handler) {
target.addEventListener(eventType, handler, false);
}
} else {
addHandler = function(target, eventType, handler) {
target.attachEvent('on' + eventType, handler);
}
}
addHandler(target, eventType, handler);
}

上面这个函数实现了延迟加载模式。函数在第一次被调用时,会先检查并决定使用哪种方法去绑定事件处理器。然后原始函数被包含正确操作的新函数覆盖。这样一来,随后每次调用都不会再做检测,因为检测代码已经被新的函数覆盖。

调用延迟加载函数时,第一次总是会消耗较长时间,因为它必须运行检测接着再调用另一个函数完成任务。但是在第一次之后的每次调用,速度将大大加快,因为它不需要再执行检测逻辑。

条件预加载

条件预加载技术,会在脚本加载期间提前检测,而不会等到函数被调用。条件检测的操作依然只有一次,只是它在过程中来得更早。

1
2
3
4
5
6
7
const addHandler = document.body.addEventListener ?
function(target, eventType, handler) {
target.addEventListener(eventType, handler, false);
} :
function(target, eventType, handler) {
target.attachEvent('on' + eventType, handler);
};

上面这段代码会先检查 addEventListener() 是否存在,然后根据结果指定选择最佳的函数。提前发生的检测

条件预加载确保所有函数调用消耗的时间相同。其代价是需要在脚本加载时就检测,而不是加载后。预加载适用于一个函数马上就要被使用,并并且在整个页面的生命周期中频繁出现的场合。

使用速度快的部分

位操作

JavaScript 位操作符作用在最基本的层次上,即按内存中表示数值的二进制比特来操作数值。JavaScript 中的数字都依照 IEEE-754 标准以 64 位格式存储。在位操作中,64 位的数字值会被转换位有符号 32 位格式。位操作符会直接操作该 32 位数以得到结果,尽管需要转换,但这个过程与 JavaScript 中其他数学运算和布尔操作相比较要快很多。

科班出身的开发者,应该都知道二进制数、原码、补码、反码、按位操作和左移、右移这些概念吧,在此不再赘述。

ECMAScript 中用于位操作的运算符有:

  • &:按位与
  • |:按位或
  • ^:按位异或
  • !:按位取反
  • <<:左移
  • >>:有符号的右移,将数值向右移动,但会保留符号
  • >>>:无符号的右移,将数值的所有 32 位都向右移动

原生方法

无论 JavaScript 代码如何优化,永远都不会比 JavaScript 引擎提供的原生方法更快。

所以开发中,可能地话,应该尽量使用 ECMAScript 规定的标准方法和宿主环境提供的原生 API 解决问题,而不是自己重复造轮子或者滥用类库的接口。特别是数学运算和 DOM 操作,我们应该尽量使用原生方法。


参考

  • 《高性能JavaSript》,[美]Nicbolas C.Zakas 著,丁琛 译,电子工业出版社 2015