影响JavaScript性能的另一个杀手就是递归,在上一节中提到采用memoization技术能够优化计算数值 的递归函数,但memoization不是万能的,不是全部的递归函数均可以用memoization技术优化,本文介绍了这些状况,并介绍了解决办法, 就是将递归转换为迭代,同时须要注意,本文末尾介绍的方案不是最终的方案,还须要和上一节优化循环的方案综合起来才能达到最佳效果。javascript
【原文】Speed up your JavaScript, Part 3
【做者】Nicholas C. Zakas
【译文】http://cuimingda.com/2009/02/speed-up-your-javascript-part-3.html
【译者】明达
如下是对原文的翻译:
递归是拖慢脚本运行速度的大敌之一。太多的递归会让浏览器变得愈来愈慢直到死掉或者莫名其妙的忽然自动退出,因此咱们必定要解决在JavaScript中出现的这一系列性能问题。在这个系列文章的第二篇中, 我曾经简短的介绍了如何经过memoization技术来替代函数中太多的递归调用。memoization是一种能够缓存以前运算结果的技术,这样咱们 就不须要从新计算那些已经计算过的结果。对于经过递归来进行计算的函数,memoization简直是太有用了。我如今使用的memoizer是由 Crockford写的,主要应用在那些返回整数的递归运算中。固然并非全部的递归函数都返回整数,因此咱们须要一个更加通用的memoizer()函 数来处理更多类型的递归函数。
html
function memoizer(fundamental, cache) {
cache = cache || {};
var shell = function(arg) {
if (! (arg in cache)) {
cache[arg] = fundamental(shell, arg);
}
return cache[arg];
};
return shell;
}
这 个版本的函数和Crockford写的版本有一点点不一样。首先,参数的顺序被颠倒了,原有函数被设置为第一个参数,第二个参数是缓存对象,为可选参数,因 为并非全部的递归函数都包含初始信息。在函数内部,我将缓存对象的类型从数组转换为对象,这样这个版本就能够适应那些不是返回整数的递归函数。在 shell函数里,我使用了in操做符来判断参数是否已经包含在缓存里。这种写法比测试类型不是undefined更加安全,由于undefined是一 个有效的返回值。咱们仍是用以前提到的斐波纳契数列来作说明:
java
var fibonacci = memoizer(function(recur, n) {
return recur(n - 1) + recur(n - 2);
}, { "0": 0, "1": 1} );
同 样的,执行fibonacci(40)这个函数,只会对原有的函数调用40次,而不是夸张的331,160,280次。memoization对于那些有 着严格定义的结果集的递归算法来讲,简直是棒极了。然而,确实还有不少递归算法不适合使用memoization方法来进行优化。
我在学 校时的一位教授一直坚持认为,任何使用递归的状况,若是有须要,均可以使用迭代来代替。实际上,递归和迭代常常会被做为互相弥补的方法,尤为是在另一种 出问题的状况下。将递归算法转换为迭代算法的技术,也是和开发语言无关的。这对JavaScript来讲是很重要的,由于不少东西在执行环境中是受到限制 的(the importance in JavaScript is greater, though, because the resources of the execution environment are so restrictive.)。让咱们回顾一个典型的递归算法,好比说归并排序,在JavaScript中实现这个算法须要下面的代码:
算法
function merge(left, right) {
var result = [];
while (left.length > 0 && right.length > 0) {
if (left[0] < right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
return result.concat(left).concat(right);
}
//采用递归实现的归并排序算法
function mergeSort(items) {
if (items.length == 1) {
return items;
}
var middle = Math.floor(items.length / 2),
left = items.slice(0, middle),
right = items.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}
调 用mergeSort()函数处理一个数组,就能够返回通过排序的数组。注意每次调用mergeSort()函数,都会有两次递归调用。这个算法不可使 用memoization来进行优化,由于每一个结果都只计算并使用一次,就算缓冲告终果也没有什么用。若是你使用mergeSort()函数来处理一个包 含100个元素的数组,总共会有199次调用。1000个元素的数组将会执行1999次调用。在这种状况下,咱们的解决方案是将递归算法转换为迭代算法, 也就是说要引入一些循环(关于算法,能够参考这篇《List Processing: Sort Again, Naturally》):
shell
// 采用迭代实现的归并排序算法
function mergeSort(items) {
if (items.length == 1) {
return items;
}
var work = [];
for (var i = 0,
len = items.length; i < len; i++) {
work.push([items[i]]);
}
work.push([]); //in case of odd number of items
for (var lim = len; lim > 1; lim = (lim + 1) / 2) {
for (var j = 0,
k = 0; k < lim; j++, k += 2) {
work[j] = merge(work[k], work[k + 1]);
}
work[j] = []; //in case of odd number of items
}
return work[0];
}
这 个归并排序算法实现使用了一系列循环来代替递归进行排序。因为归并排序首先要将数组拆分红若干只有一个元素的数组,这个方法更加明确的执行了这个操做,而 不是经过递归函数隐晦的完成。work数组被初始化为包含一堆只有一个元素数组的数组。在循环中每次会合并两个数组,并将合并后的结果放回 work数组中。当函数执行完成后,排序的结果会经过work数组中的第一个元素返回。在这个归并排序的实现中,没有使用任何递归,一样也实现了这个算 法。然而,这样作却引入了大量的循环,循环的次数基于要排序的数组中元素的个数,因此咱们可能须要使用在上篇讨论过的技术来进行修订,处理这些额外开销。
总结一下基本原则,不论是何时使用递归的时候都应该当心谨慎。memoization和迭代是代替递归的两种解决方案,最直接的结果固然就是避免那个提示脚本失控的对话框。数组