JavaScript 高性能编程:递归、迭代与查表法详解
作者:霁月时间:2026-10-21 19:55:37浏览:640
本文深入解析 JavaScript 中递归的性能瓶颈,对比迭代与查表法(Memoization)的优化方案。通过阶乘与排序实例,展示如何利用迭代避免栈溢出,以及通过缓存中间结果减少重复计算,从而显著提升程序执行效率。
JavaScript 高性能编程:递归、迭代与查表法详解
深入解析JavaScript中递归、迭代及查表法的高性能实现
递归基础与实现
递归
定义:函数通过直接自我调用或相互调用来实现特定目标,如排序和阶乘计算。
基础递归示例
阶乘计算
function factorial(n) {
if (n == 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}
递归排序实现
/*
排序且合并数组
*/
function myMerge(left, right) {
// 保存最后结果的数组
var res = [];
// 有一个数组结束了就结束排序,一般情况下,两个数组长度应该保持一样
while (left.length > 0 && right.length > 0) {
if ( left[0] < right[0] ) {
// 把left第一个成员删除,存储到新数组res中
res.push(left.shift());
} else {
res.push(right.shift());
}
}
// 如果还有剩余直接合并到新数组
return res.concat(left).concat(right);
}
/*
递归方式
*/
function recursion(items) {
if (items.length == 1) {
return items;
}
var middle = Math.floor(items.length / 2),
left = items.slice(0, middle), // 取数组前半部分
right = items.slice(middle); // 取数组后半部分
// 递归排序
return myMerge(recursion(left), recursion(right));
}
迭代:替代递归的性能方案
迭代
鉴于各浏览器对递归调用栈存在上限,且不当使用易引发死循环导致崩溃,若重视性能,推荐使用迭代替代递归,因循环执行的开销远低于反复函数调用。
/*
迭代方式,不使用递归,可以避免出现栈溢出问题
*/
function iteration(items) {
if (items.length == 1) {
return items;
}
var work = [];
// 将items成员全部转化成数组,保存到数组中
for (var i = 0, len = items.length; i < len; i++) {
work.push([items[i]]);
}
work.push([]); // 数组长度为奇数时
// 迭代
for (var lim = len; lim > 1; lim = (lim + 1) / 2) {
for (var j = 0, k = 0; k < lim; j++, k+=2) {
work[j] = myMerge(work[k], work[k + 1]);
}
work[j] = []; // 数组长度为奇数时
}
return work[0];
}
/* 迭代过程分析
假设: var test = [1, 3, 9, 7, 4, 8, 6, 5, 0, 2]; // len == 10
work = [[1], [3], [9], [7], [4], [8], [6], [5], [0], [2], []]; // len == 11;
// 迭代(二分法)
a) lim: 11 > 1
1) k = 0, work[0] = myMerge([1], [3]) ==> work[0] = [1, 3]
2) k = 2, work[1] = myMerge([9], [7]) ==> work[1] = [7, 9]
3) k = 4, work[2] = myMerge([4], [8]) ==> work[2] = [4, 8]
4) k = 6, work[3] = myMerge([6], [5]) ==> work[3] = [5, 6]
5) k = 8, work[4] = myMerge([0], [2]) ==> work[4] = [0, 2]
> 在后面添加个空数组是为了数组长度为奇数时的情况,能有个对象做比较,否则会出现越界错误
b) lim: 6 > 1, work = [[1,3], [7,9], [4,8], [5,6], [0,2], []];
1) k = 0, work[0] = myMerge([1,3], [7,9]) ==> work[0] = [1, 3, 7, 9]
2) k = 2, work[1] = myMerge([4,8], [5,6]) ==> work[1] = [4, 5, 6, 8]
3) k = 4, work[2] = myMerge([0,2], []) ==> work[2] = [0,2]
> 最后一个[]会被myMerge函数给合并,所以不用担心添加的空数组对后续产生影响
c) lim: 3 > 1, work = [[1, 3, 7, 9], [4, 5, 6, 8], [0, 2], []];
1) k = 0, work[0] = myMerge([1, 3, 7, 9], [4, 5, 6, 8]) ==> work[0] = [1,3,4,5,6,7,8,9]
2) k = 2, work[1] = myMerge([0, 2], []) ==> work[1] = [0, 2]
d) lim: 2, work = [[1,3,4,5,6,7,8,9], [0,2], []];
1) k = 0, work[0] = myMerge([1,3,4,5,6,7,8,9], [0,2]) ==> work[0] = [0,1,2,3,4,5,6,7,8,9]
> 至此排序整个过程全部完成
// 关键点
a) 将数组中的每个元素数组化,以便后续存放已经排好序的元素
b) k的取值,k+=2, 每次取两个数组进行比较,形成一个新的数组
c) 一定要在比较完之后附加空数组,否则会在数组个数为奇数个的时候出现访问越界错误
d) 最外层循环的lim的取值, lim = (lim + 1) / 2即原数组长度的一半,作为内循环终止的条件
*/
查表法(Memoization)优化递归
递归优化策略:查表法(Memoization),利用对象缓存先前计算结果以消除冗余运算。
通过缓存已执行的操作或运算结果,后续相同操作可直接读取缓存,否则执行递归,从而大幅降低递归工作量并提升性能。
var count = 0;
function factorial(n) {
console.log("factorial count = " + count++);
if (n == 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}
// var f1 = factorial(6);
// var f2 = factorial(5);
// var f3 = factorial(4);
// >>>>> 结果是函数被调用了:18次
/*
递归优化:查表法,通过缓存
*/
function memFactorial(n) {
console.log("memFactorial count = " + count++);
// JS中函数即可视为一个对象,所以可以直接通过函数名+点语法给此对象添加属性
if (!memFactorial.cache) {
memFactorial.cache = {
"0": 1,
"1": 1
};
}
// hasOwnProperty(n)可以判断对象中是否存在该属性,不会查找原型(但是"in"会先查实例再原型)
if (!memFactorial.cache.hasOwnProperty(n)) {
// 缓存中不存在的情况下再进行递归,并且将新数组缓存起来
memFactorial.cache[n] = n * memFactorial(n - 1);
}
// 最终数据都可以在缓存中找到
return memFactorial.cache[n];
}
var f1 = memFactorial(6);
var f2 = memFactorial(5);
var f3 = memFactorial(4);
// >>>>> 结果是函数被调用了:只有8次,大大的减少了函数被调用的次数
提供Memoization通用版本,但建议在实际开发中手动在普通版中实现具体逻辑。
通用版需缓存特定参数下的函数调用结果,即针对不同传入参数,其调用结果均需进行缓存存储。
/*
递归优化通用版,性能不如普通版,需要缓存每次调用结果, 即内部函数
*/
function memoize(func, cache) {
// 缓存池
cache = cache || {}; // 没有则新建
var result = function(arg) {
// console.log("memoize count = " + count++);
if (!cache.hasOwnProperty(arg)) {
cache[arg] = func(arg);
}
};
return result;
}
// 使用
// 将阶乘函数缓存起来
// 只是优化了cache的通用性,但损失了一部分性能
var memOpfactorial = memoize(factorial, {"0": 1, "1": 1});
var f1 = memOpfactorial(6);
var f2 = memOpfactorial(5);
var f3 = memOpfactorial(4);
// 结果次数依旧是:18次。因此不推荐使用
总结与建议
总结
- 需谨慎使用递归,以避免死循环、程序崩溃或调用栈溢出风险;
- 掌握使用迭代替代递归的方法,可有效规避递归调用栈溢出或死循环问题;
- 查表法是递归的优化版本:Memoization通过减少重复工作来提升性能。不推荐通用版,因其相比普通版会牺牲部分性能。
感谢阅读,期望本文能对大家有所帮助,感谢各位对本站的支持!
作者最新文章
tween.js 内存泄漏预防:怎么正确停止动画与清理对象
2026-11-05 15:52:56
Vue或React项目中为何不建议直接使用JS刷新页面
2026-11-05 13:57:30
哪里可以找到高质量的免费JS脚本资源?
2026-11-05 13:15:03
JS数组对象怎么根据指定ID字段去重
2026-11-04 21:41:10
JS数组去重方法性能对比:Set、Map与Filter谁更快
2026-11-04 16:14:15
