ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

30 seconds of code 中的 JavaScript 记忆化(Memoization):原理、实现与适用场景

30 seconds of code 中的 JavaScript 记忆化(Memoization):原理、实现与适用场景 教程文档【免费下载链接】30-seconds-of-codeCoding articles to level up your development skills项目地址https://gitcode.com/gh_mirrors/30/30-seconds-of-code点击查看免费下载记忆化memoization是 JavaScript 性能优化中性价比最高的技巧之一它用一块内存缓存换取重复计算的消除能让昂贵的函数调用从每次都从头算起变为命中缓存即取即用。本文以 30 seconds of code 仓库中 content/snippets/js/s/memoization.md 为核心完整讲解记忆化的适用标准、基于Map的自实现方案、基于Proxy的进阶方案并结合仓库中 递归函数优化 与 JavaScript Proxy 介绍 的源码证据帮助你在真实项目中准确判断何时该用并落地可运行的代码。什么是记忆化Memoization记忆化是一种被广泛使用的代码加速技术核心思路非常简单依赖一个缓存cache存放已完成工作的结果。缓存的目的是避免相同的工作被重复执行从而让耗时函数的后续调用变得更快。从实现角度看记忆化本质上是在函数计算结果与导致该结果的参数之间建立映射第一次以参数A调用函数时真正执行计算并把结果存入缓存之后再次以参数A调用时跳过计算直接从缓存中取出结果返回。这一机制决定了它的两个基本特征第一次调用通常没有加速效果因为要付出写入缓存的成本加速体现在相同参数下的重复调用。这正是 30 seconds of code 的 JavaScript 性能优化合集 content/collections/js/performance.yaml 将js/s/memoization收录为独立条目的原因——它是性能调优工具箱中与减少 DOM 访问避免重复操作并列的基础手法。使用记忆化的判断标准基于记忆化的定义可以直接推导出判断某个函数是否适合记忆化的三条标准慢、贵、耗时的函数调用能从记忆化中获益。如果一个函数几乎不消耗时间引入缓存反而会带来不必要的内存开销与查找成本。记忆化加速的是后续调用因此它最适合在相同条件下被多次调用的场景。例如同一个输入会被反复处理或函数在循环、渲染周期中被高频触发。结果存储在内存中所以当同一个函数在差异很大的不同条件下被调用时应避免使用记忆化——此时缓存几乎无法命中白白占用内存且没有加速收益。这三条标准可以概括为一个朴素的直觉只有当参数重复出现的概率足够高、且单次计算足够贵时缓存才划算。需要注意的是它们是基于定义推导出的启发式准则实际效果仍应结合具体调用频率与输入分布来验证。基于 Map 的自实现记忆化函数在 JavaScript 中手写一个记忆化函数并不复杂。仓库给出的实现选用Map来存储结果理由是Map保存键值对且记住键的原始插入顺序非常适合用函数的参数作键、计算结果作值const memoize fn { const cache new Map(); const cached function (val) { return cache.has(val) ? cache.get(val) : cache.set(val, fn.call(this, val)) cache.get(val); }; cached.cache cache; return cached; };这个实现有几个值得注意的细节cache.has(val)负责命中检测命中时直接cache.get(val)返回这是加速的路径未命中时先cache.set(val, fn.call(this, val))写入结果再通过 cache.get(val)取回刚写入的值返回。Map.prototype.set返回Map对象本身真值因此右侧的get一定被执行这是一种紧凑的写入并读取写法使用fn.call(this, val)而非fn(val)保留了调用时this上下文避免改变函数原有的绑定行为把缓存对象挂到返回函数上cached.cache cache调用方可以查看甚至清空缓存例如通过memoizedFn.cache.clear()释放内存。仓库文档用字母重排anagrams递归函数演示了它的实际效果——这类指数级组合的递归非常适合作为记忆化演示对象// 这个函数很慢会从记忆化中受益 const anagrams str { if (str.length 2) return str.length 2 ? [str, str[1] str[0]] : [str]; return str .split() .reduce( (acc, letter, i) acc.concat( anagrams(str.slice(0, i) str.slice(i 1)).map(val letter val) ), [] ); }; const anagramsCached memoize(anagrams); anagramsCached(javascript); // 耗时很长 anagramsCached(javascript); // 因为已缓存几乎瞬间返回可以看出anagrams在递归过程中会反复计算大量相同子串的重排结果记忆化让第二次调用直接命中缓存效果立竿见影。基于 Proxy 对象的记忆化进阶实现除手写包装函数外JavaScript 的Proxy对象为记忆化提供了一种颇具巧思的替代方案。30 seconds of code 对Proxy有专门的介绍文章 An Introduction to JavaScript Proxy其中明确说明apply(target, thisArg, argumentsList)这一 trap 专门用于拦截函数调用——这正是记忆化所需要的切入点。使用applytrap 的实现如下const memoize fn new Proxy(fn, { cache: new Map(), apply (target, thisArg, argsList) { let cacheKey argsList.toString(); if(!this.cache.has(cacheKey)) this.cache.set(cacheKey, target.apply(thisArg, argsList)); return this.cache.get(cacheKey); } });对照 proxy-introduction.md 中applytrap 的签名apply(target, thisArg, argumentsList)可以清晰看到每一步的含义用new Proxy(fn, handler)把原函数包装成代理handler 中附带一个cache: new Map()作为缓存applytrap 拦截每次函数调用拿到原始函数target、调用方thisArg与参数列表argsList用argsList.toString()生成缓存键例如[1, 2]会变成字符串1,2这比单参数版本的Map实现天然支持多参数未命中时通过target.apply(thisArg, argsList)调用原函数并写入缓存命中时直接返回缓存值。仓库文档用递归版斐波那契数列作为验证示例并给出了文档环境下的观测数据const fibonacci n (n 1 ? 1 : fibonacci(n - 1) fibonacci(n - 2)); const memoizedFibonacci memoize(fibonacci); for (let i 0; i 100; i ) fibonacci(30); // ~5000ms for (let i 0; i 100; i ) memoizedFibonacci(30); // ~50ms朴素递归版斐波那契存在大量重复子问题fibonacci(30)会被反复计算而记忆化版本只在第一次真正计算其后 99 次调用全部命中缓存性能差异接近两个数量级。需要注意这里的~5000ms与~50ms是原文档给出的示例观测值实际耗时随运行环境波动但其相对量级差异具有普遍代表性。两种实现方式的对比维度基于 Map 的包装函数基于 Proxy 的applytrap参数支持单参数val多参数缓存键为argsList.toString()this上下文fn.call(this, val)保留target.apply(thisArg, argsList)保留缓存可见性挂在cached.cache上可访问可清空挂在 handler 的this.cache上适用对象普通函数需要代理语义或希望无侵入包装的场景从源码结构看Proxy 版本更适合只关心加速、不关心缓存内部结构的场景而 Map 版本暴露了cached.cache属性便于集成测试或手动管理缓存生命周期。缓存键设计的注意点从两个实现可以推断出记忆化在工程化落地时必须注意的边界问题Map版本以参数值本身作键1与1会被视为不同键行为严谨但只支持单参数Proxy 版本的argsList.toString()会把1与1统一成1也会让两个不同的对象参数都变成[object Object]从而错误共享缓存若参数为对象或包含嵌套结构需要自定义序列化逻辑如JSON.stringify或改用Map键。这些并非文档明示的结论而是从上述实现代码中可以推断的工程细节在引入记忆化到生产代码前值得专门校验。记忆化在递归优化中的实战印证记忆化最常见的实战场景就是优化递归。仓库中的 递归函数优化 一文与本文互为印证它先用console.log展示了朴素递归版fibonacciNumber(4)会反复调用相同的子问题随后给出基于Map的手写缓存版本const fibonacciCache new Map(); const fibonacciNumber n { const cacheKey ${n}; let r; if(fibonacciCache.has(cacheKey)) { r fibonacciCache.get(cacheKey); } else { r n 2 ? fibonacciNumber(n - 1) fibonacciNumber(n - 2) : n; fibonacciCache.set(cacheKey, r); } return r; }从该文的执行日志可以看到加入缓存后每个n只被真正计算一次后续遇到相同n时直接打印[MEMO] Cache hit。这与本文memoize包装函数的内核完全一致has命中判断、未命中则计算并set。区别在于该文把缓存逻辑内联进了具体业务函数而本文的memoize把它抽象成了通用高阶函数——通用版本更可复用内联版本则省去包装层、在递归自调用中无需经过包装函数。该文同时给出了一条重要的性能权衡结论当递归计算使用频率不高时迭代iteration往往比记忆化更快因为它没有缓存的内存占用与命中检查开销而当递归函数会以不同参数被多次调用时记忆化的缓存能在多次调用间持续复用反而更具优势。因此高频 参数重复→ 记忆化本文主题低频 一次性计算→ 迭代或朴素实现即可。这也回扣了本文开头的判断标准记忆化不是越快越好的银弹而是针对重复调用这一前提条件的定向优化。记忆化使用注意事项与边界综合原文档与仓库证据落地记忆化时需要关注以下边界内存占用缓存随不同参数的增长而增长对于参数空间极大的函数如随机数输入、UUID、时间戳缓存会持续膨胀且命中率趋近于零应避免记忆化或引入容量上限与淘汰策略。参数多样性判断标准第三条明确指出结果存储在内存中因此同一函数在非常不同的条件下被调用的场景不适合作记忆化——每次调用都会新增缓存条目却几乎无命中回报。缓存清理Map版本把缓存暴露为cached.cache可在长时间运行的应用中按需clear()Proxy 版本则需自行设计缓存的生命周期管理。递归与记忆化的组合若递归函数在自调用路径上使用记忆化包装后的版本而非内联缓存需要确保每一层递归都能命中同一缓存这与 递归函数优化 中内联缓存的实现效果一致。小结记忆化是用空间换时间的经典范例通过Map或Proxy的applytrap 缓存计算结果让耗时函数的重复调用接近常数时间。本文完整覆盖了 30 seconds of code 仓库中 memoization.md 的定义、三条适用标准、两种可运行实现及其对比并结合 递归函数优化、JavaScript Proxy 介绍 与 性能优化合集 提供了源码级佐证。判断标准记住一条即可慢、高频、参数可重复——三者齐备时记忆化就是最省力的加速方案。赞分享教程文档【免费下载链接】30-seconds-of-codeCoding articles to level up your development skills项目地址https://gitcode.com/gh_mirrors/30/30-seconds-of-code点击查看免费下载相关推荐30 Seconds of Interviews 之 JavaScript 记忆化Memoization用函数级缓存提升重复计算性能30 Seconds of Interviews 之 JavaScript 记忆化Memoization用函数级缓存提升重复计算性能 记忆化Memoiz教程前端30-seconds-of-code用 JavaScript 实现凯撒密码Caesar Cipher30 seconds of code用 JavaScript 实现凯撒密码Caesar Cipher 导读 凯撒密码Caesar cipher是最经典教程文档30 seconds of code用 JavaScript Proxy 实现不可变对象30 seconds of code用 JavaScript Proxy 实现不可变对象 对象可变性Object mutability与 const 关键教程文档创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表