Lodash源码阅读-memoize

Lodash 源码阅读-memoize

概述

memoize 函数是 Lodash 中实现函数记忆化的工具,它能将函数调用的结果缓存起来。当你用相同参数多次调用一个函数时,只有第一次会真正执行计算,后续调用直接返回缓存结果,大大提升了性能。特别适合处理计算密集型操作或重复调用频繁的场景。

前置学习

依赖函数

  • MapCache:Lodash 内部实现的缓存数据结构,提供 get、set、has 等方法,用于存储函数调用结果

技术知识

  • 函数记忆化:通过缓存结果优化函数性能的技术
  • 闭包:JavaScript 中函数访问其外部作用域变量的特性
  • 高阶函数:接收或返回函数的函数
  • this 绑定:JavaScript 中函数执行上下文的传递
  • 缓存键生成:如何为不同的函数调用生成唯一标识

源码实现

js 复制代码
/**
 * Creates a function that memoizes the result of `func`. If `resolver` is
 * provided, it determines the cache key for storing the result based on the
 * arguments provided to the memoized function. By default, the first argument
 * provided to the memoized function is used as the map cache key. The `func`
 * is invoked with the `this` binding of the memoized function.
 *
 * **Note:** The cache is exposed as the `cache` property on the memoized
 * function. Its creation may be customized by replacing the `_.memoize.Cache`
 * constructor with one whose instances implement the
 * [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object)
 * method interface of `clear`, `delete`, `get`, `has`, and `set`.
 *
 * @static
 * @memberOf _
 * @since 0.1.0
 * @category Function
 * @param {Function} func The function to have its output memoized.
 * @param {Function} [resolver] The function to resolve the cache key.
 * @returns {Function} Returns the new memoized function.
 * @example
 *
 * var object = { 'a': 1, 'b': 2 };
 * var other = { 'c': 3, 'd': 4 };
 *
 * var values = _.memoize(_.values);
 * values(object);
 * // => [1, 2]
 *
 * values(other);
 * // => [3, 4]
 *
 * object.a = 2;
 * values(object);
 * // => [1, 2]
 *
 * // Modify the result cache.
 * values.cache.set(object, ['a', 'b']);
 * values(object);
 * // => ['a', 'b']
 *
 * // Replace `_.memoize.Cache`.
 * _.memoize.Cache = WeakMap;
 */
function memoize(func, resolver) {
  if (
    typeof func != "function" ||
    (resolver != null && typeof resolver != "function")
  ) {
    throw new TypeError(FUNC_ERROR_TEXT);
  }
  var memoized = function () {
    var args = arguments,
      key = resolver ? resolver.apply(this, args) : args[0],
      cache = memoized.cache;

    if (cache.has(key)) {
      return cache.get(key);
    }
    var result = func.apply(this, args);
    memoized.cache = cache.set(key, result) || cache;
    return result;
  };
  memoized.cache = new (memoize.Cache || MapCache)();
  return memoized;
}

// Expose `MapCache`.
memoize.Cache = MapCache;

实现思路

memoize 函数的实现思路很直接:先检查输入参数的合法性,然后创建并返回一个新函数 memoized。这个新函数会在调用时自动检查传入的参数是否已有缓存结果,有则直接返回缓存值,没有则执行原函数并将结果存入缓存。同时,它还暴露了缓存对象,允许用户自定义缓存实现或直接操作缓存内容。整个设计既简洁又灵活,可以适应多种缓存需求。

源码解析

参数验证

js 复制代码
if (
  typeof func != "function" ||
  (resolver != null && typeof resolver != "function")
) {
  throw new TypeError(FUNC_ERROR_TEXT);
}

这段代码验证输入参数是否合法:

  • func 必须是函数类型
  • 如果提供了 resolver,它也必须是函数类型

不满足条件就抛出类型错误。这种严格的参数检查能够及早发现问题,避免在后续执行中出现难以预料的错误。

示例:

js 复制代码
_.memoize("not a function"); // 抛出 TypeError
_.memoize(function () {}, "not a function"); // 抛出 TypeError

创建记忆化函数

js 复制代码
var memoized = function () {
  var args = arguments,
    key = resolver ? resolver.apply(this, args) : args[0],
    cache = memoized.cache;

  if (cache.has(key)) {
    return cache.get(key);
  }
  var result = func.apply(this, args);
  memoized.cache = cache.set(key, result) || cache;
  return result;
};

这是 memoize 的核心逻辑,创建一个新函数处理缓存逻辑:

  1. 生成缓存键

    js 复制代码
    key = resolver ? resolver.apply(this, args) : args[0];
    • 如果提供了 resolver 函数,使用它来生成键(传入所有参数)
    • 否则默认使用第一个参数 args[0] 作为键(传入的参数)

    示例:

    js 复制代码
    // 使用自定义键生成器处理多参数
    const add = _.memoize(
      (a, b) => a + b,
      (a, b) => `${a}-${b}` // 生成唯一键
    );
    add(1, 2); // 计算 3 并缓存
    add(1, 2); // 直接返回缓存的 3
  2. 检查缓存

    js 复制代码
    if (cache.has(key)) {
      return cache.get(key);
    }

    如果缓存中有对应的键,直接返回缓存结果,避免重复计算。

  3. 执行函数和缓存结果

    js 复制代码
    var result = func.apply(this, args);
    memoized.cache = cache.set(key, result) || cache;
    return result;
    • 使用 apply 确保原函数运行时的 this 上下文正确
    • 缓存计算结果并返回

    这里的 cache.set(key, result) || cache 是个技巧,因为某些缓存实现的 set 方法可能不返回自身实例,用 || 确保总是返回缓存对象。

初始化缓存

js 复制代码
memoized.cache = new (memoize.Cache || MapCache)();
return memoized;

这行代码初始化缓存并挂载到函数上:

  • 使用 memoize.Cache 构造函数(如已自定义)或默认的 MapCache
  • 将缓存实例挂载到 memoized.cache 属性,方便外部访问和操作

暴露缓存实现

js 复制代码
// Expose `MapCache`.
memoize.Cache = MapCache;

暴露默认缓存构造函数为 memoize.Cache,这样用户可以替换成自己的缓存实现:

js 复制代码
// 自定义缓存实现示例
_.memoize.Cache = WeakMap; // 使用 WeakMap 允许键被垃圾回收

只要替代的缓存类实现了 getsethas 方法接口即可。

总结

memoize 函数是一个简洁但功能强大的工具,它通过缓存计算结果显著提升了函数性能。它的设计有几个亮点:

  1. 简单接口与灵活性平衡:仅需两个参数就能实现强大的记忆化功能,同时保留了高度定制能力
  2. 良好的关注点分离:缓存逻辑与原函数逻辑完全分离,符合单一职责原则
  3. 开放/封闭原则实践 :通过暴露 memoize.Cache,允许扩展但不需修改源码
  4. 优雅的缓存键设计:默认使用第一个参数作为键,同时支持自定义键生成策略
  5. 保留函数上下文 :正确传递 this 绑定,确保函数行为一致性

这些设计使 memoize 成为优化性能的强大工具,特别适合计算密集型函数、API 请求缓存和任何需要避免重复计算的场景。

相关推荐
灵感__idea1 小时前
Hello 算法:贪心的世界
前端·javascript·算法
GreenTea3 小时前
一文搞懂Harness Engineering与Meta-Harness
前端·人工智能·后端
killerbasd4 小时前
牧苏苏传 我不装了 4/7
前端·javascript·vue.js
吴声子夜歌5 小时前
ES6——二进制数组详解
前端·ecmascript·es6
码事漫谈5 小时前
手把手带你部署本地模型,让你Token自由(小白专属)
前端·后端
ZC跨境爬虫5 小时前
【爬虫实战对比】Requests vs Scrapy 笔趣阁小说爬虫,从单线程到高效并发的全方位升级
前端·爬虫·scrapy·html
爱上好庆祝5 小时前
svg图片
前端·css·学习·html·css3
橘子编程5 小时前
JavaScript与TypeScript终极指南
javascript·ubuntu·typescript
王夏奇6 小时前
python中的__all__ 具体用法
java·前端·python
叫我一声阿雷吧6 小时前
JS 入门通关手册(45):浏览器渲染原理与重绘重排(性能优化核心,面试必考
javascript·前端面试·前端性能优化·浏览器渲染·浏览器渲染原理,重排重绘·reflow·repaint