递归
使用递归可以把复杂的循环过程变得简单。然而递归如果控制不当会存在很多隐患:
-
缺少终止条件或终止条件不明确
我们在编写递归函数时,首先要考虑的问题是递归的出口也就是终止条件。如果终止条件书写不当的话程序将无休止运行,造成无限递归的死循环。
-
调用栈大小限制。
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];
}