前端如何优雅的写一个记忆化函数?

记忆化(Memoization)是前端性能优化的重要手段,尤其适用于频繁调用、计算密集型的纯函数场景。本文将带大家实现一个功能完善、扩展性强的记忆化函数,并深入探讨其设计思路和应用场景。

什么是记忆化?

记忆化是一种缓存技术,通过存储函数的计算结果,当再次使用相同参数调用时直接返回缓存值,避免重复计算。这对于:

  • 递归函数(如斐波那契数列计算)
  • 复杂数据转换
  • 频繁调用的工具函数
  • 有固定参数范围的计算

都能带来显著的性能提升。

核心实现思路

一个完善的记忆化函数需要解决以下问题:

  1. 如何生成唯一的缓存键(处理不同参数类型和顺序)
  2. 如何控制缓存大小(防止内存溢出)
  3. 如何处理异常情况
  4. 如何支持自定义扩展

下面是完整实现代码:

运行

javascript 复制代码
/**
 * 创建一个记忆化函数,缓存函数调用的结果
 * @param {Function} fn - 需要记忆化的函数
 * @param {Object} options - 配置选项
 * @param {number} options.maxSize - 缓存最大大小,默认1000
 * @param {Function} options.keyGenerator - 自定义键生成器
 * @returns {Function} 记忆化后的函数
 */
function memoize(fn, options = {}) {
  const { maxSize = 1000, keyGenerator } = options;
  const cache = new Map();

  // 默认的键生成器,避免修改原参数数组
  const defaultKeyGenerator = (...args) => {
    // 使用扩展运算符创建新数组,避免修改原数组
    const sortedArgs = [...args].sort();
    return `${fn.name || "anonymous"}_${JSON.stringify(sortedArgs)}`;
  };

  const generateKey = keyGenerator || defaultKeyGenerator;

  return function (...args) {
    try {
      const key = generateKey(...args);

      // 检查缓存
      if (cache.has(key)) {
        return cache.get(key);
      }

      // 如果缓存已满,删除最旧的条目(简单的LRU策略)
      if (cache.size >= maxSize) {
        const firstKey = cache.keys().next().value;
        cache.delete(firstKey);
      }

      // 执行原函数并缓存结果
      const result = fn(...args);
      cache.set(key, result);
      return result;
    } catch (error) {
      console.error("Memoize error:", error);
      // 如果缓存过程中出错,直接调用原函数
      return fn(...args);
    }
  };
}

关键技术点解析

1. 缓存键生成策略

默认键生成器做了两件重要的事:

运行

javascript 复制代码
const sortedArgs = [...args].sort();
return `${fn.name || "anonymous"}_${JSON.stringify(sortedArgs)}`;
  • 参数排序 :确保参数顺序不影响缓存命中(如 sum(1,2)sum(2,1) 视为相同调用)
  • 函数名前缀:避免不同函数间的键冲突
  • JSON 序列化:支持任意参数类型(基础类型、数组、对象等)

这种设计在大多数场景下都能工作良好,同时允许通过 keyGenerator 选项自定义:

运行

javascript 复制代码
// 示例:自定义键生成器(不处理参数顺序)
const customMemo = memoize(sum, {
  keyGenerator: (a, b) => `sum_${a}_${b}`
});

2. 缓存大小控制

通过 maxSize 选项限制缓存条目数量,默认值为 1000:

运行

javascript 复制代码
if (cache.size >= maxSize) {
  const firstKey = cache.keys().next().value;
  cache.delete(firstKey);
}

这里采用了简单的 LRU(最近最少使用)策略的简化版 ------ 当缓存满时,删除最早插入的条目。这种实现轻量高效,适合大多数场景。

3. 异常处理

通过 try-catch 确保缓存过程中的错误不会阻断原函数执行:

运行

javascript 复制代码
try {
  // 缓存相关逻辑
} catch (error) {
  console.error("Memoize error:", error);
  // 降级处理:直接调用原函数
  return fn(...args);
}

这种设计保证了记忆化功能的稳健性,即使在参数无法序列化(如包含循环引用的对象)等极端情况下,函数仍能正常工作。

使用示例

1. 基础用法:优化求和函数

运行

javascript 复制代码
const sum = (a, b) => a + b;
const memoizedSum = memoize(sum);

console.log(memoizedSum(2, 3)); // 5(首次计算)
console.log(memoizedSum(3, 2)); // 5(命中缓存,参数顺序不影响)
console.log(memoizedSum(2, 3)); // 5(命中缓存)

2. 性能优化:斐波那契数列

递归计算斐波那契数列是记忆化的经典用例:

运行

javascript 复制代码
// 未优化版本:重复计算极多
const fib = (n) => n <= 1 ? n : fib(n-1) + fib(n-2);

// 记忆化版本:性能提升显著
const memoizedFib = memoize((n) => {
  if (n <= 1) return n;
  return memoizedFib(n-1) + memoizedFib(n-2);
});

console.time("fib(30)");
console.log(memoizedFib(30)); // 832040
console.timeEnd("fib(30)"); // 远快于未优化版本

3. 缓存大小限制

运行

javascript 复制代码
// 限制缓存最多2条记录
const limitedMemo = memoize(sum, { maxSize: 2 });

limitedMemo(1, 1); // 缓存:{(1,1) => 2}
limitedMemo(2, 2); // 缓存:{(1,1) => 2, (2,2) => 4}
limitedMemo(3, 3); // 缓存满,删除(1,1),新增(3,3) => 6
limitedMemo(1, 1); // 需重新计算,因为缓存已被清除

注意事项

  1. 仅用于纯函数:记忆化只对纯函数有效(相同输入必产生相同输出),避免用于有副作用的函数(如修改全局变量、DOM 操作、API 调用等)。
  2. 参数类型考量 :对于包含函数、Symbol 等无法被 JSON 序列化的参数,建议自定义 keyGenerator
  3. 缓存开销平衡 :简单函数(如 (a,b)=>a+b)的计算成本可能低于缓存开销,此时记忆化反而会降低性能。
  4. 内存管理 :对于长期运行的应用,建议根据实际场景设置合理的 maxSize,避免内存泄漏。

总结

本文实现的记忆化函数具有以下特点:

  • 轻量高效
  • 灵活可扩展,支持自定义键生成和缓存大小
  • 健壮稳定,包含异常处理和降级机制
  • 适用广泛,能应对大多数前端记忆化场景
相关推荐
wuk9985 小时前
实现ROS系统的Websocket传输,向Web应用推送sensor_msgs::Image数据
前端·websocket·网络协议
合作小小程序员小小店7 小时前
web网页开发,在线%考试管理%系统,基于Idea,vscode,html,css,vue,java,maven,springboot,mysql
java·前端·系统架构·vue·intellij-idea·springboot
天天进步20157 小时前
CSS Grid与Flexbox:2025年响应式布局终极指南
前端·css
Boop_wu8 小时前
[Java EE] 计算机基础
java·服务器·前端
Novlan18 小时前
TDesign UniApp 组件库来了
前端
用户47949283569158 小时前
React DevTools 组件名乱码?揭秘从开发到生产的代码变形记
前端·react.js
顾安r9 小时前
11.8 脚本网页 打砖块max
服务器·前端·html·css3
倚栏听风雨9 小时前
typescript 方法前面加* 是什么意思
前端
狮子不白9 小时前
C#WEB 防重复提交控制
开发语言·前端·程序人生·c#
菜鸟‍9 小时前
【前端学习】阿里前端面试题
前端·javascript·学习