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