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

记忆化(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,避免内存泄漏。

总结

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

  • 轻量高效
  • 灵活可扩展,支持自定义键生成和缓存大小
  • 健壮稳定,包含异常处理和降级机制
  • 适用广泛,能应对大多数前端记忆化场景
相关推荐
Zyx20079 小时前
🎹用 HTML5 打造“敲击乐”钢琴:前端三剑客的第一次交响曲
前端
小时前端9 小时前
面试官:我为什么总在浏览器存储问题上追问IndexedDB?
前端·浏览器
今禾9 小时前
Git完全指南(下篇):Git高级技巧与问题解决
前端·git·github
llq_3509 小时前
为什么 JS 代码执行了,但页面没马上变?
前端·javascript
汤姆Tom9 小时前
CSS 预处理器深入应用:提升开发效率的利器
前端·css·面试
练习前端两年半9 小时前
Vue3组件二次封装终极指南:动态组件+h函数的优雅实现
前端·vue.js
皮皮虾我们跑9 小时前
前端HTML常用基础标
前端·javascript·html
Yeats_Liao9 小时前
Go Web 编程快速入门 01 - 环境准备与第一个 Web 应用
开发语言·前端·golang
卓码软件测评9 小时前
第三方CMA软件测试机构:页面JavaScript动态渲染生成内容对网站SEO的影响
开发语言·前端·javascript·ecmascript