《高性能JavaScript》读书笔记(四)

《高性能JavaScript》第四章主要涉及算法跟流程控制,包括循环,条件语句和递归的优化。下面就开始吧。

循环

四种循环类型

四种循环类型包括 for循环,while循环,do-while循环及for-in循环。

这四种循环类型都有各自的特点,适用于不同的实现需求。比如while循环是前侧循环,适合需要在进入循环前开始判断的操作,而do-while循环则适合最少需要循环一次才开始判断的操作。for循环比较直观,当然也并不是最简单的,而for-in是针对对象的,可以枚举任何对象的属性名,但是是性能最差的。

循环优化

循环的优化上,给出了这几点建议:

尽量不使用for-in循环

for-in循环的性能最差,因为其主要针对对象,虽然表面上很简单,但是在循环的时候却因每次迭代操作都会同时搜索实例或原型属性,所以会产品更多的开销,所以是最慢的。针对对象的遍历,书中提出一种方案:如果知道对象的所有属性,则可以用一个数组存储所有的属性名,然后遍历数组会更快:

1
2
3
4
5
6
var props = ["prop1", "prop2"],
i = 0;

while (i < props.length) {
process(object[props[i++]]);
}
减少迭代的工作量

一般的数组循环,其循环体中都会有以下的操作:

  1. 在控制条件中查找一次属性 (item.length);
  2. 在控制条件中执行一次数值比较 (i < items.length);
  3. 一次比较操作查看控制条件的计算结果是否为true (i < item.length == true);
  4. 一次自增操作 (i++);
  5. 一次数组查找 (items[i]);
  6. 一次函数调用 (process(items[i]))。

一个本来迭代次数有限的循环,是没办法考虑从迭代上做优化的,所以可以从每一次都会执行的循环体中着手优化,每优化一个操作,就可以得到客观的性能提升。书中给出的两条优化建议是:

  1. 把查找属性的操作在初始化时就复制到一个局部变量中,即可减少第一个操作;
  2. 如果,循环数组的顺序跟结果无关,则可以考虑倒序处理。因为倒序的时候,只需要判断控制条件是否为true(除非控制条件为0,则都是true),不需要再做一次比较。
减少迭代次数

如果遇到一个迭代次数非常多,则可以考虑下如何减少迭代次数上下手。书中介绍一种限制迭代次数的模式,“达夫设备(Duff’s Device)”。这种模式可以在一次迭代中执行多次迭代的操作。下面是例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var iterations = Math.floor(items.length / 8),
startAt = items.length % 8,
i = 0;

do {
switch(startAt){
case 0: process(items[i++]);
case 7: process(items[i++]);
case 6: process(items[i++]);
case 5: process(items[i++]);
case 4: process(items[i++]);
case 3: process(items[i++]);
case 2: process(items[i++]);
case 1: process(items[i++]);
}
startAt = 0;
} while (--iterations);

假设原来的迭代次数为n(8(x) < n <= 8(n+1)),则这种模式可以把迭代次数减少至(n+1)次。书中还提供了用while代替switch的方案,还可以在优化迭代的速度。

基于函数的迭代

基于函数的迭代方法foreach()在使用上是非常方便的,但是因为每个循环中都必须调用一个外部方法,所以性能上还是比基于循环的迭代要慢一些。

条件语句

两种条件语句的选择

条件语句分if-else跟switch。当判断条件跟分支操作较少时,if-else语句更易读,而当判断条件跟分支操作较多时,switch语句则更易读。当判断条件跟分支操作较少时,if-else跟switch性能上没有什么区别,只有判断更加复杂的情况下才会让switch的性能突出。

if-else优化

如果中需要做大量的顺序或大小的判断,不管是if-else或者switch语句,最多都是要遍历完所有条件。书中提出可以考虑二分法的优化思路,根据判断条件的顺序或大小,优先跟居中的判断值做比较,从而把最大的判断次数减半。实现的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
if (value < 6) {
if (value < 3) {
if (value == 0) {
return result0;
} else if (value == 1) {
return result1;
} else {
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 result8;
} else if (value == 9) {
return result9;
} else {
return result10;
}
}
}

查找表

如果是判断大量的离散值,判断的结果只需要一个值而不是一系列的操作,那还可以通过构建查找表进行优化。查找表的优势在于它是通过键值对直接找到处理值,不需要通过判断语句,即使离散值增加,也不会产生更多的性能开销。

递归

递归可以把复杂的算法变得很简单,可以把很复杂的执行过程大大的简化。但是递归也有一些问题点。比如当执行的计算较长时,会使整个系统进入假死状态,而且会有浏览器调用栈的限制。而这些问题,都可能是在设计的初期并没有意识到的。

一般递归有两种模式,一种的直接递归,函数调用函数自身,如比较经典的阶乘函数。另一种是“隐伏模式”,是两个函数互相调用,这种模式在出错调试时会非常棘手。

一般递归函数出现调用栈溢出的原因在于不正确的终止条件,如果终止条件没有问题,则需要考虑为了浏览器安全。书中给出了两种方案:迭代跟Memoization。

迭代虽然在速度上快不过递归,但是由于它相对对浏览器友好,所以一旦程序遇到调用栈溢出的问题,就需要考虑迭代的实现方案。

Memoization是一种缓存阶段计算的方案。在方案中,会有一个数组专门存储阶段性的计算值,这些计算值本身可能在不被存储的时候回因程序需要而不断的被计算获得。当存在一个数组能存储下这些阶段性的计算值时,函数被调用的越多,其重复的计算工作会得到控制,整个函数的性能会逐渐稳定。

最后

这章内容主要是在程序流程上的优化,减少代码的执行量跟解决代码的风险问题。现在比较多的项目都会需要考虑大规模的并发跟流量,所以程序上的小小优化,风险上的细心处理,都能对项目有很大的帮助。值得学习。