实现函数缓存

基础知识

  1. 什么是函数缓存 (Memoization)? 函数缓存(也称“记忆化”)是一种编程技巧和优化策略。它通过存储昂贵函数调用的结果,并在后续使用相同输入再次调用该函数时,直接返回缓存的结果,而无需重新执行函数的计算过程。

  2. 核心思想:空间换时间 函数缓存的本质是一种“空间换时间”的权衡。它使用额外的内存空间(缓存存储)来避免重复的、耗时的计算,从而显著提高程序的执行速度。

  3. 适用场景 函数缓存最适用于纯函数 (Pure Functions)。纯函数是指满足以下两个条件的函数:

    • 相同的输入,永远产生相同的输出
    • 没有副作用(不会修改函数外部的任何状态,如全局变量、DOM 等)。

    常见的应用场景包括:

    • 复杂的数学计算: 如斐波那契数列、阶乘等,这些计算中包含了大量的重复子问题。
    • 递归操作: 许多递归算法在递归过程中会反复计算相同子节点的结果。
    • 数据转换: 对一个大型数据结构进行昂贵的格式化或计算,如果源数据不变,结果也不应改变。
    • 需要稳定结果的 API 请求: 对于返回静态数据的 API,可以在前端缓存其结果,避免不必要的网络请求。

核心思路

我们的目标是创建一个高阶函数 memoize(func),它接收一个需要被缓存的函数 func,并返回一个经过缓存优化的新函数。

  1. 高阶函数与闭包: memoize 函数是高阶函数,它返回的新函数将通过闭包“记住”一个用于存储结果的缓存对象 cache。这个 cache 对象在多次调用之间是持久存在的。

  2. 缓存存储: 我们可以使用一个普通对象 {} 或 ES6 的 Map 来作为缓存。Map 在处理非字符串键时更具优势,但对于通用场景,使用对象并通过特定规则生成字符串键是一种常见且有效的方法。

  3. 缓存键 (Cache Key) 的生成: 这是实现中最关键的一步。如何根据函数的输入参数,生成一个唯一的、可用于在缓存对象中查找的键?

    • 如果函数只有一个简单的原始类型参数(如数字或字符串),可以直接使用该参数作为键。
    • 如果函数有多个参数或参数是对象等复杂类型,我们需要一种可靠的方式将它们“序列化”成一个唯一的字符串。JSON.stringify() 是一个通用且方便的选择。
  4. 缓存逻辑: 返回的新函数在被调用时,会执行以下逻辑:

    • 第一步: 根据传入的参数生成一个缓存键 key
    • 第二步: 检查 cache 对象中是否存在这个 key
    • 第三步 (缓存命中): 如果 key 存在,说明这个计算之前已经执行过。直接返回 cache[key] 中存储的结果,函数结束。
    • 第四步 (缓存未命中): 如果 key 不存在,说明这是一个新的计算。正常执行原始函数 func,将得到的 result 存储到缓存中 cache[key] = result,然后再返回 result

代码实现

下面是一个通用的 memoize 函数实现。

/**
 * 创建一个函数的记忆化(缓存)版本
 * @param {Function} func - 需要被缓存的函数
 * @returns {Function} - 返回一个新的、带缓存功能的函数
 */
function memoize(func) {
  // 1. 创建一个闭包变量 cache,用于存储计算结果
  const cache = {};

  // 2. 返回一个新的函数
  return function (...args) {
    const context = this; // 保持 this 指向

    // 3. 根据参数生成一个唯一的缓存键
    // JSON.stringify 是一个通用的、能处理大多数情况的序列化方法
    const key = JSON.stringify(args);

    // 4. 检查缓存中是否存在该键
    if (cache[key]) {
      console.log(`从缓存中读取: key=${key}`);
      return cache[key];
    } else {
      console.log(`首次计算: key=${key}`);
      // 5. 如果缓存中没有,则执行原始函数
      const result = func.apply(context, args);
      // 6. 将计算结果存入缓存
      cache[key] = result;
      // 7. 返回结果
      return result;
    }
  };
}

代码解析

  1. 缓存对象初始化

    • const cache = {}memoize 函数作用域内创建缓存对象,利用闭包特性使所有返回的缓存函数共享该对象,实现结果持久化存储。
  2. 返回缓存函数结构

    • memoize 返回新函数,通过 rest 参数 ...args 收集所有传入参数,形成参数数组 args,支持任意参数组合的缓存。
  3. 缓存键生成(核心步骤)

    • const key = JSON.stringify(args) 使用 JSON.stringify 将参数数组转换为唯一字符串键。
    • 示例:参数 [20, 30] 转换为 "[20,30]",作为 cache 对象的键。
    • 局限性:无法处理函数、Symbol、循环引用等非序列化类型,但适用于大多数基础数据类型。
  4. 缓存命中判断

    • if (cache[key]) 检查缓存中是否存在对应键,若存在则直接返回缓存值,避免重复执行函数,提升性能(尤其对高计算成本函数)。
  5. 函数执行与上下文绑定

    • 缓存未命中时,通过 func.apply(context, args) 执行原始函数。
    • apply 确保:
      • 函数内部 this 指向调用时的 context
      • 参数数组 args 正确传递给原始函数。
  6. 结果写入缓存

    • cache[key] = result 将计算结果存入缓存,键为参数序列化后的字符串,便于后续相同参数调用时直接获取。
  7. 结果返回机制

    • 缓存命中时返回缓存值,未命中时返回新计算结果,保证函数返回值的一致性和缓存的有效性。

使用示例:优化斐波那契数列

斐波那契数列的递归实现是测试函数缓存效果的绝佳例子。

// 一个效率极低的递归斐波那契函数
function fibonacci(n) {
  if (n <= 1) {
    return n;
  }
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// 使用我们的 memoize 函数进行包装
const memoizedFib = memoize(fibonacci);

// --- 性能对比 ---

console.time('未缓存的斐波那契');
console.log('结果:', fibonacci(40));
console.timeEnd('未缓存的斐波那契');
// 在大多数机器上,这需要几秒钟时间

console.log('\n--- 分割线 ---\n');

console.time('缓存后的斐波那契');
console.log('结果:', memoizedFib(40));
console.timeEnd('缓存后的斐波那契');
// 第一次调用,会逐级计算并缓存,速度可能与未缓存版本相似或稍慢

console.log('\n--- 分割线 ---\n');

console.time('第二次调用缓存后的斐波那契');
console.log('结果:', memoizedFib(40));
console.timeEnd('第二次调用缓存后的斐波那契');
// 第二次调用,直接从缓存中读取,几乎是瞬时完成!
Copyright © Jun 2025 all right reserved,powered by Gitbook该文件修订时间: 2025-07-03 17:35:08

results matching ""

    No results matching ""

    results matching ""

      No results matching ""