高效的JS流程控制(二)

161 阅读2分钟

递归

使用递归可以把复杂的循环过程变得简单。然而递归如果控制不当会存在很多隐患:

  • 缺少终止条件或终止条件不明确

    我们在编写递归函数时,首先要考虑的问题是递归的出口也就是终止条件。如果终止条件书写不当的话程序将无休止运行,造成无限递归的死循环。

  • 调用栈大小限制。

    JS 引擎支持的递归数量与 JS 调用栈大小直接相关。当你使用了太多的递归,甚至超过了最大调用栈时,首先你的运行时长会很大,并使得用户界面处于假死状态。并且浏览器也会报告调用栈溢出错误。

规避执行栈溢出

检查代码中的递归实例
递归有两种模式:直接调用模式、隐伏模式。
直接调用模式:这种模式在发生错误时就很容易定位到错误发生的地方。

 function foo(){
     foo();
 }
 foo();

隐伏模式:这种模式中两个模式互相调用,在大型代码库中很难定位原因。

function first(){
    second();
}
function second(){
    first();
}
first();

大多数调用栈错误都与这两种模式有关,最常见的栈溢出的原因是不正确的终止条件,因此我们定位模式错误的第一步是验证终止条件。如果条件没问题那么可能是算法中包含了太多递归层,为了改善这个问题,建议使用迭代的方式或结合使用。

迭代

任何递归能实现的算法同样可以用迭代来实现。迭代算法通常包含几个不同的循环,分别对应计算机过程的不同方向,这也会引入它们自身的性能问题。

使用优化后的循环替代长时间的递归函数可以提升性能,因为循环比反复调用一个函数的开销要少得多。
我们用归并排序算法来直观的表现

    function merge(left, right){
        const res = [];
        while(left.length > 0 && right.length > 0){
            if(left[0] < right[0]){
                res.push(left.shift());
            } else {
                res.push(right.shift());
            }
        }
        return res;
    }
    function mergeSort(itmes){
        if(items.length == 1) {
            return items;
        }
        let middle = Math.floor(items.length / 2),
            left = items.slice(0, middle),
            right = itmes.slice(middle);
        return merge(mergeSort(left), mergeSort(right));
    }

一个长度为n的数组最终会调用mergeSort() 2 * n - 1,这意味着可能造成栈溢出。
用迭代实现

    function mergeSort(items){
        if(items.length == 1) {
            return items;
        }
        const work = [],
              len = items.length;
        for(let i = 0; i < len; i++){
            work.push([items[i]]);
        }
        work.push([]); // 如果数组长度为奇数
        for(let lim = len; lim > 1; lim = (lim + 1) / 2){
            for(let j = 0, k = 0; l < lim; j++, k += 2){
                work[j] = merge(work[k], work[k + 1])
            }
            work[j] = []; // 如果数组长度为奇数
        }
        return work[0];
    }

尽管迭代版本的归并排序比递归要慢一些,但它不会受到调用栈的限制,把递归改为迭代是避免栈溢出错误的方法之一。

缓存

减少工作量就是最好的性能优化技术。代码要处理的事情越少,它运行速度就越快。缓存前一个计算结果供后续计算使用,避免重复工作。
实现一个函数阶乘的功能

function factorial(n){
    if(n <= 1){
        return 1;
    }
    return n * factorial(n - 1);
}
let fact6 = factorial(6);
let fact6 = factorial(5);
let fact6 = factorial(4);

可以看出有很多结果被重复计算,我们利用缓存对象改善该问题

function factorial(n){
    if(n <= 1){
        return 1;
    }
    if(!factorial.cache){
        factorial.cache = {
            0: 1,
            1: 1
        }
    }
    if(!factorial.cache.hasOwnProperty(n)){
        factorial.cache[n] = n * factorial(n - 1);
    }
    return factorial.cache[n];
}