JavaScript函数的性能优化是前端开发中需要重点关注的内容,其中参数重组和递归函数的不合理使用是常见的性能损耗点,掌握对应的优化方法可以有效提升代码执行效率。
避免参数重组的性能损耗
参数重组指的是在函数内部对传入的参数进行重新整理、转换或者合并的操作,比如使用arguments对象转数组、解构赋值后重新组合参数等,这些操作会产生额外的内存开销和执行时间。
参数重组的常见场景与问题
很多开发者习惯在函数开头处理参数,比如下面的代码:
function sum() {
// 将arguments转为数组,这一步就是参数重组
const args = Array.prototype.slice.call(arguments);
return args.reduce((acc, cur) => acc + cur, 0);
}
console.log(sum(1, 2, 3)); // 输出6
上面的代码中,Array.prototype.slice.call(arguments)会创建一个新的数组,当函数被频繁调用时,这种重复创建数组的操作会累积成明显的性能损耗。
避免参数重组的优化方案
首先可以优先使用剩余参数代替arguments对象,剩余参数本身就是数组,不需要额外的转换操作:
function sum(...args) {
// 剩余参数args已经是数组,无需重组
return args.reduce((acc, cur) => acc + cur, 0);
}
console.log(sum(1, 2, 3)); // 输出6
如果函数需要处理默认参数,也可以直接给参数设置默认值,避免后续重新组合参数:
// 避免先解构再重组参数的写法
function getUserInfo(name, age, city) {
// 直接设置默认值,不需要额外重组
name = name || '未知';
age = age || 0;
city = city || '未知';
return { name, age, city };
}
console.log(getUserInfo('张三', 20)); // 输出{name:'张三',age:20,city:'未知'}
递归函数的优化实践
递归函数虽然逻辑清晰,但如果使用不当很容易出现栈溢出或者执行效率低下的问题,常见的优化方向包括尾递归优化和记忆化优化。
尾递归优化
尾递归指的是递归调用是函数执行的最后一个操作,这种情况下引擎可以复用当前的函数栈,避免栈溢出。但是需要注意,目前大部分JS引擎默认没有开启尾递归优化,需要手动调整写法。
比如计算阶乘的普通递归写法:
// 普通递归,存在栈溢出风险
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
console.log(factorial(5)); // 输出120
上面的递归不是尾递归,因为递归调用后还需要做乘法运算,优化为尾递归的写法如下:
// 尾递归写法,增加累加参数
function factorial(n, total = 1) {
if (n <= 1) return total;
return factorial(n - 1, n * total);
}
console.log(factorial(5)); // 输出120
如果运行环境支持尾递归优化,这种写法可以处理更大的n值而不会栈溢出。
记忆化优化
记忆化指的是将递归函数的计算结果缓存起来,当再次遇到相同的输入时直接返回缓存结果,避免重复计算。适合用于有重复子问题的递归场景,比如斐波那契数列计算。
普通斐波那契递归的写法:
// 普通递归,重复计算很多次
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
console.log(fib(10)); // 输出55
使用记忆化优化后的写法:
// 记忆化优化,缓存计算结果
function fib(n, cache = {}) {
if (n <= 1) return n;
// 如果缓存中有结果,直接返回
if (cache[n]) return cache[n];
// 计算并缓存结果
cache[n] = fib(n - 1, cache) + fib(n - 2, cache);
return cache[n];
}
console.log(fib(10)); // 输出55
console.log(fib(50)); // 普通递归会卡死,记忆化可以快速返回结果
优化效果对比
我们可以通过简单的性能测试对比优化前后的差异,以下是测试避免参数重组的示例:
// 测试参数重组优化的性能差异
function testSumWithRecombine() {
function sum() {
const args = Array.prototype.slice.call(arguments);
return args.reduce((acc, cur) => acc + cur, 0);
}
const start = Date.now();
for (let i = 0; i < 1000000; i++) {
sum(1, 2, 3, 4, 5);
}
console.log('参数重组耗时:', Date.now() - start, 'ms');
}
function testSumWithoutRecombine() {
function sum(...args) {
return args.reduce((acc, cur) => acc + cur, 0);
}
const start = Date.now();
for (let i = 0; i < 1000000; i++) {
sum(1, 2, 3, 4, 5);
}
console.log('无参数重组耗时:', Date.now() - start, 'ms');
}
testSumWithRecombine();
testSumWithoutRecombine();
多次运行测试可以看到,无参数重组的版本耗时明显更少,当调用次数越多,差异越明显。
注意事项
- 不是所有的参数处理都需要避免重组,只有高频调用的函数才需要做这类优化,低频函数过度优化反而会降低代码可读性。
- 尾递归优化依赖JS引擎的支持,如果不确定运行环境是否支持,可以手动将递归改为循环实现,避免栈溢出问题。
- 记忆化优化会增加内存占用,需要根据实际场景权衡,对于输入范围很大、缓存命中率低的场景不适合使用记忆化。