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 请求缓存和任何需要避免重复计算的场景。

相关推荐
今晚吃什么呢?6 分钟前
前端面试题之CSS中的box属性
前端·css
我是大龄程序员9 分钟前
Babel工作理解
前端
《独白》19 分钟前
将图表和表格导出为PDF的功能
javascript·vue.js·ecmascript
CopyLower22 分钟前
提升 Web 性能:使用响应式图片优化体验
前端
南通DXZ24 分钟前
Win7下安装高版本node.js 16.3.0 以及webpack插件的构建
前端·webpack·node.js
什码情况24 分钟前
微服务集成测试 -华为OD机试真题(A卷、JavaScript)
javascript·数据结构·算法·华为od·机试
你的人类朋友1 小时前
浅谈Object.prototype.hasOwnProperty.call(a, b)
javascript·后端·node.js
Mintopia1 小时前
深入理解 Three.js 中的 Mesh:构建 3D 世界的基石
前端·javascript·three.js
打瞌睡de喵1 小时前
JavaScript 空对象检测
javascript
前端太佬1 小时前
暂时性死区(Temporal Dead Zone, TDZ)
前端·javascript·node.js