函数式编程风格中有一个“纯函数”的概念,纯函数是一种无副作用的函数,除此之外纯函数还有一个显著的特点:对于同样的输入参数,总是返回同样的结果。在平时的开发过程中,我们也应该尽量把无副作用的“纯计算”提取出来实现成“纯函数”,尤其是涉及到大量重复计算的过程,使用纯函数+函数缓存的方式能够大幅提高程序的执行效率。本文的主题即是函数缓存实现的及应用,必须强调的是Memoization
起作用的对象只能是纯函数
函数缓存的概念很简单,先来一个最简单的实现来说明一下:
function memoize(func) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args)
return cache[key] || (cache[key] = func.apply(this, args))
}
}
JavaScript
Copy
memoize
就是一个高阶函数,接受一个纯函数作为参数,并返回一个函数,结合闭包来缓存原纯函数执行的结果,可以简单的测试一下:
function sum(n1, n2) {
const sum = n1 + n2
console.log(`${n1}+${n2}=${sum}`)
return sum
}
const memoizedSum = memoize(sum)
memoizedSum(1, 2) // 会打印出:1+2=3
memoizedSum(1, 2) // 没有输出
JavaScript
Copy
memoizedSum
在第一次执行时将执行结果缓存在了闭包中的缓存对象cache
中,因此第二次执行时,由于输入参数相同,直接返回了缓存的结果。
上面memoize
的实现能够满足简单场景下纯函数结果的缓存,但要使其适用于更广的范围,还需要重点考虑两个问题:
cache
对象的实现问题key
值计算问题下面着重完善这两个问题。
cache
对象问题上述实现版本使用普通对象作为缓存器,这是我们惯用的手法。问题不大,但仍要注意,例如最后返回值的语句,存在一个容易忽略的问题:如果cache[key]
为“假值”,比如0、null、false,那会导致每次都会重新计算一次。
return cache[key] || (cache[key] = func.apply(this, args))
JavaScript
Copy
因此为了严谨,还是要多做一些判断,
function memoize(func) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args)
if(!cache.hasOwnProperty(key)) {
cache[key] = func.apply(this, args)
}
return cache[key]
}
}
JavaScript
Copy
更好的选择是使用ES6+支持的Map
对象
function memoize(func) {
const cache = new Map()
return function(...args) {
const key = JSON.stringify(args)
if (cache.has(key)) {
return cache.get(key)
}
const result = func.apply(this, args)
cache.set(key, result)
return result
}
}
JavaScript
Copy
key
值计算问题ES6+的支持使得第一个问题很容易就完善了,毕竟这年头什么代码不是babel
加持;而缓存器对象key
的确定却是一个让人脑壳疼的问题。key
直接决定了函数计算结果缓存的效果,理想情况下,函数参数与key
满足一对一关系,上述实现中我们通过const key = JSON.stringify(args)
将参数数组序列化计算key
,在大多数情况下已经满足了一对一的原则,用在平时的开发中大概也不会有问题。但是需要注意的是序列化将会丢失JSON
中没有等效类型的任何Javascript
属性,如函数或Infinity
,任何值为undefined
的属性都将被JSON.stringify
忽略,如果值作为数组元素序列化结果又会有所不同,如下图所示。

虽然我们很少将这些特殊类型作为函数参数,但也不能排除这种情况。比如下面的例子,函数calc
接收两个普通参数和一个算子,算子则执行具体的计算,如果使用上面的方法缓存函数结果,可以发现第二次输入的是减法函数,但仍然打印出结果3
而不是-1
,原因是两个参数序列化结果都是[1,2,null]
,第二次打印的是第一次的缓存结果。
function sum(n1, n2, ) {
const sum = n1 + n2
return sum
}
function sub(n1, n2, ) {
const sub = n1 - n2
return sub
}
function calc(n1, n2, operator){
return operator(n1, n2)
}
const memoizedCalc = memoize(calc)
console.log(memoizedCalc(1, 2, sum)) // 3
console.log(memoizedCalc(1, 2, sub)) // 3
JavaScript
Copy
既然JSON.stringify
不能产生一对一的key
,那么有什么办法可以实现真正的一对一关系呢,参考Lodash的源码,其使用了WeakMap
对象作为缓存器对象,其好处是WeakMap
对象的key
只能是对象,这样如果能够保持参数对象的引用相同,对应的key
也就相同。
function memoize(func) {
const cache = new WeakMap()
return function(...args) {
const key = args[0]
if (cache.has(key)) {
return cache.get(key)
}
const result = func.apply(this, args)
cache.set(key, result)
return result
}
}
function sum(n1, n2) {
const sum = n1 + n2
console.log(`${n1}+${n2}:`, sum)
return sum
}
function sub(n1, n2, ) {
const sub = n1 - n2
console.log(`${n1}-${n2}:`, sub)
return sub
}
function calc(param){
const {n1, n2, operator} = param
return operator(n1, n2)
}
const memoizedCalc = memoize(calc)
const param1 = {n1: 1, n2: 2, operator: sum}
const param2 = {n1: 1, n2: 2, operator: sub}
console.log(memoizedCalc(param1))
console.log(memoizedCalc(param2))
console.log(memoizedCalc(param2))
JavaScript
Copy
执行打印的结果为
1+2: 3
3
1-2: -1 // 只在第一次做减法运算时打印
-1
-1 // 第二次执行减法直接打印出结果
JavaScript
Copy
使用WeakMap
作为缓存对象还是有很多局限性,首选参数必须是对象,再比如我们把上例最后几行代码改成下面的代码,会发现后面减法的输出还是错误的,因为前后参数引用的对象都是param1
,因此对应的key
是相同的,而且在开发过程中我们不太可能一直保存参数的引用,大对数重读计算的场景下,我们都会构造新的参数对象,即使有些参数对象看起来长的一样,但却对应不同的引用,也就对应不同的key
,这就失去了缓存的效果。
console.log(memoizedCalc(param1)) // 3
param1.operator = sub
console.log(memoizedCalc(param1)) // 3
console.log(memoizedCalc(param1)) // 3
JavaScript
Copy
为了使开发具有最高的灵活性,在Memoization过程中,key
的计算最好由开发者自己决定使用何种规则产生与函数结果一一对应的关系,实际上Lodash和Ramda都提供了类似的实现。
function memoize(func, resolver) {
if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) {
throw new TypeError('Expected a function')
}
const cache = new Map() //可以根据实际情况使用WeakMap或者{}
return function(...args) {
const key = resolver ? resolver.apply(this, args) : args[0]
if (cache.has(key)) {
return cache.get(key)
}
const result = func.apply(this, args)
cache.set(key, result)
return result
}
}
JavaScript
Copy
上述代码memoize
除了接收需要缓存的函数,还接收一个resolver
函数,方便用户自行决定如果计算key
。
Original url: Access
Created at: 2019-04-18 13:19:12
Category: default
Tags: none
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
java windows火焰图_mob64ca12ec8020的技术博客_51CTO博客 - 在windows下不可行,不知道作者是怎样搞的 监听SpringBoot 服务启动成功事件并打印信息_监听springboot启动完毕-CSDN博客 SpringBoot中就绪探针和存活探针_management.endpoint.health.probes.enabled-CSDN博客 u2u转换板 - 嘉立创EDA开源硬件平台 Spring Boot 项目的轻量级 HTTP 客户端 retrofit 框架,快来试试它!_Java精选-CSDN博客 手把手教你打造一套最牛的知识笔记管理系统! - 知乎 - 想法有重合-理论可参考 安宇雨 闲鱼 机械键盘 客制化 开贴记录 文本 linux 使用find命令查找包含某字符串的文件_beijihukk的博客-CSDN博客_find 查找字符串 ---- mac 也适用 安宇雨 打字音 记录集合 B站 bilibili 自行搭建 开坑 真正的客制化 安宇雨 黑苹果开坑 查找工具包maven pom 引用地 工具网站 Dantelis 介绍的玩轴入坑攻略 --- 关于轴的一些说法 --- 非官方 ---- 心得而已 --- 长期开坑更新 [本人问题][新开坑位]关于自动化测试的工具与平台应用 机械键盘 开团 网站记录 -- 能做一个收集的程序就好了 不过现在没时间 -- 信息大多是在群里发的 - 你要让垃圾佬 都去一个地方看难度也是很大的 精神支柱 [超级前台]sprinbboot maven superdesk-app 记录 [信息有用] [环境准备] [基本完成] [sebp/elk] 给已创建的Docker容器增加新的端口映射 - qq_30599553的博客 - CSDN博客 [正在研究] Elasticsearch, Logstash, Kibana (ELK) Docker image documentation elasticsearch centos 安装记录 及 启动手记 正式服务器 39 elasticsearch 问题合集 不断更新 6.1.1 | 6.5.1 两个版本 博客程序 - 测试 - bug记录 等等问题 laravel的启动过程解析 - lpfuture - 博客园 OAuth2 Server PHP 用 Laravel 搭建带 OAuth2 验证的 RESTful 服务 | Laravel China 社区 - 高品质的 Laravel 和 PHP 开发者社区 利用Laravel 搭建oauth2 API接口 附 Unauthenticated 解决办法 - 煮茶的博客 - SegmentFault 思否 使用 OAuth2-Server-php 搭建 OAuth2 Server - 午时的海 - 博客园 基于PHP构建OAuth 2.0 服务端 认证平台 - Endv - 博客园 Laravel 的 Artisan 命令行工具 Laravel 的文件系统和云存储功能集成 浅谈Chromium中的设计模式--终--Observer模式 浅谈Chromium中的设计模式--二--pre/post和Delegate模式 浅谈Chromium中的设计模式--一--Chromium中模块分层和进程模型 DeepMind 4 Hacking Yourself README.md update 20211011
Laravel China 简书 知乎 博客园 CSDN博客 开源中国 Go Further Ryan是菜鸟 | LNMP技术栈笔记 云栖社区-阿里云 Netflix技术博客 Techie Delight Linkedin技术博客 Dropbox技术博客 Facebook技术博客 淘宝中间件团队 美团技术博客 360技术博客 古巷博客 - 一个专注于分享的不正常博客 软件测试知识传播 - 测试窝 有赞技术团队 阮一峰 语雀 静觅丨崔庆才的个人博客 软件测试从业者综合能力提升 - isTester IBM Java 开发 使用开放 Java 生态系统开发现代应用程序 pengdai 一个强大的博主 HTML5资源教程 | 分享HTML5开发资源和开发教程 蘑菇博客 - 专注于技术分享的博客平台 个人博客-leapMie 流星007 CSDN博客 - 舍其小伙伴 稀土掘金 Go 技术论坛 | Golang / Go 语言中国知识社区
最新评论