:原理、實現(xiàn)與適用場景)
教程文檔【免費下載鏈接】30-seconds-of-codeCoding articles to level up your development skills項目地址https://gitcode.com/gh_mirrors/30/30-seconds-of-code點擊查看免費下載記憶化memoization是 JavaScript 性能優(yōu)化中性價比最高的技巧之一它用一塊內存緩存換取重復計算的消除能讓昂貴的函數(shù)調用從每次都從頭算起變?yōu)槊芯彺婕慈〖从谩1疚囊?30 seconds of code 倉庫中 content/snippets/js/s/memoization.md 為核心完整講解記憶化的適用標準、基于Map的自實現(xiàn)方案、基于Proxy的進階方案并結合倉庫中 遞歸函數(shù)優(yōu)化 與 JavaScript Proxy 介紹 的源碼證據(jù)幫助你在真實項目中準確判斷何時該用并落地可運行的代碼。什么是記憶化Memoization記憶化是一種被廣泛使用的代碼加速技術核心思路非常簡單依賴一個緩存cache存放已完成工作的結果。緩存的目的是避免相同的工作被重復執(zhí)行從而讓耗時函數(shù)的后續(xù)調用變得更快。從實現(xiàn)角度看記憶化本質上是在函數(shù)計算結果與導致該結果的參數(shù)之間建立映射第一次以參數(shù)A調用函數(shù)時真正執(zhí)行計算并把結果存入緩存之后再次以參數(shù)A調用時跳過計算直接從緩存中取出結果返回。這一機制決定了它的兩個基本特征第一次調用通常沒有加速效果因為要付出寫入緩存的成本加速體現(xiàn)在相同參數(shù)下的重復調用。這正是 30 seconds of code 的 JavaScript 性能優(yōu)化合集 content/collections/js/performance.yaml 將js/s/memoization收錄為獨立條目的原因——它是性能調優(yōu)工具箱中與減少 DOM 訪問避免重復操作并列的基礎手法。使用記憶化的判斷標準基于記憶化的定義可以直接推導出判斷某個函數(shù)是否適合記憶化的三條標準慢、貴、耗時的函數(shù)調用能從記憶化中獲益。如果一個函數(shù)幾乎不消耗時間引入緩存反而會帶來不必要的內存開銷與查找成本。記憶化加速的是后續(xù)調用因此它最適合在相同條件下被多次調用的場景。例如同一個輸入會被反復處理或函數(shù)在循環(huán)、渲染周期中被高頻觸發(fā)。結果存儲在內存中所以當同一個函數(shù)在差異很大的不同條件下被調用時應避免使用記憶化——此時緩存幾乎無法命中白白占用內存且沒有加速收益。這三條標準可以概括為一個樸素的直覺只有當參數(shù)重復出現(xiàn)的概率足夠高、且單次計算足夠貴時緩存才劃算。需要注意的是它們是基于定義推導出的啟發(fā)式準則實際效果仍應結合具體調用頻率與輸入分布來驗證?;?Map 的自實現(xiàn)記憶化函數(shù)在 JavaScript 中手寫一個記憶化函數(shù)并不復雜。倉庫給出的實現(xiàn)選用Map來存儲結果理由是Map保存鍵值對且記住鍵的原始插入順序非常適合用函數(shù)的參數(shù)作鍵、計算結果作值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; };這個實現(xiàn)有幾個值得注意的細節(jié)cache.has(val)負責命中檢測命中時直接cache.get(val)返回這是加速的路徑未命中時先cache.set(val, fn.call(this, val))寫入結果再通過 cache.get(val)取回剛寫入的值返回。Map.prototype.set返回Map對象本身真值因此右側的get一定被執(zhí)行這是一種緊湊的寫入并讀取寫法使用fn.call(this, val)而非fn(val)保留了調用時this上下文避免改變函數(shù)原有的綁定行為把緩存對象掛到返回函數(shù)上cached.cache cache調用方可以查看甚至清空緩存例如通過memoizedFn.cache.clear()釋放內存。倉庫文檔用字母重排anagrams遞歸函數(shù)演示了它的實際效果——這類指數(shù)級組合的遞歸非常適合作為記憶化演示對象// 這個函數(shù)很慢會從記憶化中受益 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 對象的記憶化進階實現(xiàn)除手寫包裝函數(shù)外JavaScript 的Proxy對象為記憶化提供了一種頗具巧思的替代方案。30 seconds of code 對Proxy有專門的介紹文章 An Introduction to JavaScript Proxy其中明確說明apply(target, thisArg, argumentsList)這一 trap 專門用于攔截函數(shù)調用——這正是記憶化所需要的切入點。使用applytrap 的實現(xiàn)如下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)把原函數(shù)包裝成代理handler 中附帶一個cache: new Map()作為緩存applytrap 攔截每次函數(shù)調用拿到原始函數(shù)target、調用方thisArg與參數(shù)列表argsList用argsList.toString()生成緩存鍵例如[1, 2]會變成字符串1,2這比單參數(shù)版本的Map實現(xiàn)天然支持多參數(shù)未命中時通過target.apply(thisArg, argsList)調用原函數(shù)并寫入緩存命中時直接返回緩存值。倉庫文檔用遞歸版斐波那契數(shù)列作為驗證示例并給出了文檔環(huán)境下的觀測數(shù)據(jù)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 次調用全部命中緩存性能差異接近兩個數(shù)量級。需要注意這里的~5000ms與~50ms是原文檔給出的示例觀測值實際耗時隨運行環(huán)境波動但其相對量級差異具有普遍代表性。兩種實現(xiàn)方式的對比維度基于 Map 的包裝函數(shù)基于 Proxy 的applytrap參數(shù)支持單參數(shù)val多參數(shù)緩存鍵為argsList.toString()this上下文fn.call(this, val)保留target.apply(thisArg, argsList)保留緩存可見性掛在cached.cache上可訪問可清空掛在 handler 的this.cache上適用對象普通函數(shù)需要代理語義或希望無侵入包裝的場景從源碼結構看Proxy 版本更適合只關心加速、不關心緩存內部結構的場景而 Map 版本暴露了cached.cache屬性便于集成測試或手動管理緩存生命周期。緩存鍵設計的注意點從兩個實現(xiàn)可以推斷出記憶化在工程化落地時必須注意的邊界問題Map版本以參數(shù)值本身作鍵1與1會被視為不同鍵行為嚴謹?shù)恢С謫螀?shù)Proxy 版本的argsList.toString()會把1與1統(tǒng)一成1也會讓兩個不同的對象參數(shù)都變成[object Object]從而錯誤共享緩存若參數(shù)為對象或包含嵌套結構需要自定義序列化邏輯如JSON.stringify或改用Map鍵。這些并非文檔明示的結論而是從上述實現(xiàn)代碼中可以推斷的工程細節(jié)在引入記憶化到生產代碼前值得專門校驗。記憶化在遞歸優(yōu)化中的實戰(zhàn)印證記憶化最常見的實戰(zhàn)場景就是優(yōu)化遞歸。倉庫中的 遞歸函數(shù)優(yōu)化 一文與本文互為印證它先用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; }從該文的執(zhí)行日志可以看到加入緩存后每個n只被真正計算一次后續(xù)遇到相同n時直接打印[MEMO] Cache hit。這與本文memoize包裝函數(shù)的內核完全一致has命中判斷、未命中則計算并set。區(qū)別在于該文把緩存邏輯內聯(lián)進了具體業(yè)務函數(shù)而本文的memoize把它抽象成了通用高階函數(shù)——通用版本更可復用內聯(lián)版本則省去包裝層、在遞歸自調用中無需經過包裝函數(shù)。該文同時給出了一條重要的性能權衡結論當遞歸計算使用頻率不高時迭代iteration往往比記憶化更快因為它沒有緩存的內存占用與命中檢查開銷而當遞歸函數(shù)會以不同參數(shù)被多次調用時記憶化的緩存能在多次調用間持續(xù)復用反而更具優(yōu)勢。因此高頻 參數(shù)重復→ 記憶化本文主題低頻 一次性計算→ 迭代或樸素實現(xiàn)即可。這也回扣了本文開頭的判斷標準記憶化不是越快越好的銀彈而是針對重復調用這一前提條件的定向優(yōu)化。記憶化使用注意事項與邊界綜合原文檔與倉庫證據(jù)落地記憶化時需要關注以下邊界內存占用緩存隨不同參數(shù)的增長而增長對于參數(shù)空間極大的函數(shù)如隨機數(shù)輸入、UUID、時間戳緩存會持續(xù)膨脹且命中率趨近于零應避免記憶化或引入容量上限與淘汰策略。參數(shù)多樣性判斷標準第三條明確指出結果存儲在內存中因此同一函數(shù)在非常不同的條件下被調用的場景不適合作記憶化——每次調用都會新增緩存條目卻幾乎無命中回報。緩存清理Map版本把緩存暴露為cached.cache可在長時間運行的應用中按需clear()Proxy 版本則需自行設計緩存的生命周期管理。遞歸與記憶化的組合若遞歸函數(shù)在自調用路徑上使用記憶化包裝后的版本而非內聯(lián)緩存需要確保每一層遞歸都能命中同一緩存這與 遞歸函數(shù)優(yōu)化 中內聯(lián)緩存的實現(xiàn)效果一致。小結記憶化是用空間換時間的經典范例通過Map或Proxy的applytrap 緩存計算結果讓耗時函數(shù)的重復調用接近常數(shù)時間。本文完整覆蓋了 30 seconds of code 倉庫中 memoization.md 的定義、三條適用標準、兩種可運行實現(xiàn)及其對比并結合 遞歸函數(shù)優(yōu)化、JavaScript Proxy 介紹 與 性能優(yōu)化合集 提供了源碼級佐證。判斷標準記住一條即可慢、高頻、參數(shù)可重復——三者齊備時記憶化就是最省力的加速方案。贊分享教程文檔【免費下載鏈接】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用函數(shù)級緩存提升重復計算性能30 Seconds of Interviews 之 JavaScript 記憶化Memoization用函數(shù)級緩存提升重復計算性能 記憶化Memoiz教程前端30-seconds-of-code用 JavaScript 實現(xiàn)凱撒密碼Caesar Cipher30 seconds of code用 JavaScript 實現(xiàn)凱撒密碼Caesar Cipher 導讀 凱撒密碼Caesar cipher是最經典教程文檔30 seconds of code用 JavaScript Proxy 實現(xiàn)不可變對象30 seconds of code用 JavaScript Proxy 實現(xiàn)不可變對象 對象可變性Object mutability與 const 關鍵教程文檔創(chuàng)作聲明:本文部分內容由AI輔助生成(AIGC),僅供參考