本文介绍如何在 typescript 中以最简方式为函数实现单例式内存缓存,避免模块级变量污染,兼顾可读性与类型安全,支持箭头函数、普通函数及直接传入构造器等多种写法。
本文介绍如何在 typescript 中以最简方式为函数实现单例式内存缓存,避免模块级变量污染,兼顾可读性与类型安全,支持箭头函数、普通函数及直接传入构造器等多种写法。
在 TypeScript 项目中,我们常需对高开销或纯副作用无关的初始化函数(如 ts.createDocumentRegistry())做“首次调用即缓存、后续直接返回”的优化。理想方案应满足:不依赖外部模块变量、不增加嵌套层级、保持函数签名清晰、支持类型推导。传统做法(如在函数体内通过 self 引用自身赋值)虽可行,但冗余且易出错;而借助通用 memoize 工具函数,则能将关注点分离,实现真正声明式的缓存逻辑。
以下是一个轻量、类型安全的 memoize 实现:
function memoize<T>(fn: () => T): () => T {
const cache: { value?: T } = {};
return () => cache.value ?? (cache.value = fn());
}该函数接收一个无参工厂函数,返回一个带内部缓存的等效函数。注意:我们使用独立对象 { value?: T } 而非直接挂载到 fn 上,规避了 TypeScript 对函数对象属性的隐式类型扩展警告(如 fn.value 会触发 Property 'value' does not exist on type '() => T'),同时保证类型严格性与运行时可靠性。
使用 Scrapfly SDK(scrapfly-sdk)在 TypeScript 或 JavaScript 中进行网页抓取,用于用户请求抓取 URL、绕过反爬虫防护、渲染页面等场景。
使用示例如下:
// ✅ 推荐:箭头函数写法,简洁直观
const getDocumentRegistry = memoize(() => ts.createDocumentRegistry());
// ✅ 同样有效:直接传入具名函数(要求其签名匹配 `() => T`)
const getDocumentRegistry = memoize(ts.createDocumentRegistry);
// ✅ 若需保留命名函数用于调试或递归场景,也可用函数表达式
const getDocumentRegistry = memoize(function createRegistry() {
console.log('Initializing registry...'); // 可加日志
return ts.createDocumentRegistry();
});⚠️ 注意事项:
- 不要混用缓存与非缓存版本:若仅需缓存行为,应统一使用 getDocumentRegistry(),无需额外定义 getDocumentRegistryCached —— 命名后缀不仅冗余,还暗示接口契约变化,违背封装原则。
- 避免在模块顶层多次调用 memoize(...):每个调用都会创建独立缓存实例。确保 const getDocumentRegistry = memoize(...) 在模块作用域内只执行一次。
- 不适用于有参数的函数:本方案专为无参、幂等初始化函数设计;如需参数化缓存(如 memoizeWithKey),应选用 Map 或 WeakMap 实现,并显式管理键空间。
总结而言,memoize(fn) 模式以极小的认知成本换取了显著的工程收益:它将缓存逻辑从业务函数中解耦,消除手动 self 引用和重复条件判断,保持代码扁平、可读、可测试,且完全兼容 TypeScript 的类型系统。对于绝大多数单例资源初始化场景,这是比“函数自引用 + 属性挂载”更专业、更可持续的选择。

















