四、算法和流程控制
1、循环的分析
循环的类型,可以参考我之前的文章:《数组遍历的几种方法及用法》,除此之外,还有最基础的for循环,while循环,do while循环,以及遍历对象的for in 循环(for in循环遍历的顺序是按照键值对加入对象的时间从先到后遍历的,并且会遍历继承自原型的属性。建议用for of 遍历Object.keys()或者Objec)。
在这些循环/遍历的方法中,for循环、while循环、do while循环的方式,单次循环所需的时间最少,而for in遍历的方式是最慢的,因为for in循环的每次迭代都会从原型链上寻找。
虽然for循环、while循环、do while循环的性能消耗最少,但是,对于数组或者对象的遍历,通常情况下还是推荐forEach等数组/对象自带的遍历方式,因为这样代码看起来简洁很多。
在循环的迭代过程中,优化性能的第一步是减少对象成员或者数组项成员的查找次数。这可以通过之前介绍的使用局部变量缓存数据来达到。
值得一提的是,通常情况下,倒序循环会略微提升性能,所以,如果循环的顺序没有影响,可以考虑使用倒序。以这份代码为例:
下面的循环方式是更节约性能的。
每次循环时,上方的for循环会比较两次:与数组长度比较,然后判定结果是否为true;下方的for循环只会比较一次:这个值是否为true。
当循环次数复杂度为O(n)时,应注重减少每次迭代的工作量;复杂度大于O(n)时,应注重减少迭代次数。
2、循环的优化
循环体运行时会带来一次小的性能开销,因此,减少迭代次数能获得比较显著的性能提升。
比较常用的限制循环迭代次数的模式叫做达夫设备(Duffs Device),
典型实现如下:
基本理念是:每次循环最多调用6次函数,所以整个循环的迭代次数将是总数除以6.这会减少因执行循环体带来的额外开销。
3、条件语句:
if else对比switch
当条件数量越大时,建议用switch,因为代码的可读性更高。条件较少时用if else,原因也是可读性更高。当然这两种方法性能并没有显著差别。当条件增加时,switch稍快。
条件语句的优化思路是,近可能的让正确分支放在前面。因此,如果对可能的结果有所预估,应该让正确的判断条件放在前面。这将减少判断的次数。。
另外一种方法是,对于if else,可以写成一系列嵌套的if else语句(参考二分法),这也会减少判断的次数。
当判断条件是一组较大的离散值时,利用数组或者对象是一个不错的选择。这适用于单个键和单个值存在映射关系的场景。
4、递归
递归是常用的一种算法实现方式。例如阶乘、斐波那契数列等。
使用递归时需要非常小心,因为递归非常因为缺少终止条件造成死循环,从而导致超出调用栈大小限制或者页面崩溃。
5、迭代(循环)
任何用递归实现的算法也可以用迭代(循环)来实现。
例如归并排序
这个算法会造成merageSort()频繁的自调用。
下面是用迭代实现该算法:
6、优化迭代的方式
在某些情况下,每一次循环都会进行重复的工作,因此,在适当的时候避免重复工作是提升性能的一个很好的方法。以之前的阶乘函数为例。
想象一下,如果要计算6,7,8的阶乘,都调用fun函数的话,1-6的阶乘其实被重复调用了3次,7的阶乘被调用了2次。所以,更好的方法是缓存并重用他们的结果。一下是改良版:
优化的关键是为这个方法绑定了一个缓存对象用于保存阶乘的值,以便在后续调用时,在需要的时候可以直接获取,而非重新计算。
在此基础上,我们可以封装一个公共的、用于缓存任意函数计算结果的函数。
cache是初始的缓存对象。这个函数会返回一个对原有函数的封装函数,给每一个计算函数声明一个缓存对象,将计算过的结果保存在对象里,以便复用。
注意,这个公告缓存函数的优化效果比上方的阶乘优化函数的优化效果更差,因为公告缓存函数只会缓存特定参数的结果,类似阶乘函数,计算8的阶乘时不会复用之前的1-7的阶乘结果,只会查看之前是否计算过8的阶乘,然后决定是复用还是计算。因此,遇到较为严重的性能问题时,最好针对性的写一个优化函数。