函数式编程 - 函数缓存Memoization
函数式编程风格中有一个“纯函数”的概念,纯函数是一种无副作用的函数,除此之外纯函数还有一个显著的特点:对于同样的输入参数,总是返回同样的结果。在平时的开发过程中,我们也应该尽量把无副作用的“纯计算”提取出来实现成“纯函数”,尤其是涉及到大量重复计算的过程,使用纯函数+函数缓存的方式能够大幅提高程序的执行效率。本文的主题即是函数缓存实现的及应用,必须强调的是Memoization
起作用的对象只能是纯函数
函数缓存的概念很简单,先来一个最简单的实现来说明一下:
function memoize(func) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args)
return cache[key] || (cache[key] = func.apply(this, args))
}
}
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) // 没有输出
memoizedSum
在第一次执行时将执行结果缓存在了闭包中的缓存对象cache
中,因此第二次执行时,由于输入参数相同,直接返回了缓存的结果。
上面memoize
的实现能够满足简单场景下纯函数结果的缓存,但要使其适用于更广的范围,还需要重点考虑两个问题:
- 1.缓存器
cache
对象的实现问题 - 2.缓存器对象使用的
key
值计算问题
下面着重完善这两个问题。
1.cache
对象问题
上述实现版本使用普通对象作为缓存器,这是我们惯用的手法。问题不大,但仍要注意,例如最后返回值的语句,存在一个容易忽略的问题:如果cache[key]
为“假值”,比如0、null、false,那会导致每次都会重新计算一次。
return cache[key] || (cache[key] = func.apply(this, args))
因此为了严谨,还是要多做一些判断,
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]
}
}
更好的选择是使用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
}
}
2.缓存器对象使用的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
既然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))
执行打印的结果为
1+2: 3
3
1-2: -1 // 只在第一次做减法运算时打印
-1
-1 // 第二次执行减法直接打印出结果
使用WeakMap
作为缓存对象还是有很多局限性,首选参数必须是对象,再比如我们把上例最后几行代码改成下面的代码,会发现后面减法的输出还是错误的,因为前后参数引用的对象都是param1
,因此对应的key
是相同的,而且在开发过程中我们不太可能一直保存参数的引用,大对数重读计算的场景下,我们都会构造新的参数对象,即使有些参数对象看起来长的一样,但却对应不同的引用,也就对应不同的key
,这就失去了缓存的效果。
console.log(memoizedCalc(param1)) // 3
param1.operator = sub
console.log(memoizedCalc(param1)) // 3
console.log(memoizedCalc(param1)) // 3
为了使开发具有最高的灵活性,在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
}
}
上述代码memoize
除了接收需要缓存的函数,还接收一个resolver
函数,方便用户自行决定如果计算key
。
函数式编程 - 函数缓存Memoization的更多相关文章
- 翻译连载 | 附录 C:函数式编程函数库-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇
原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...
- [一] java8 函数式编程入门 什么是函数式编程 函数接口概念 流和收集器基本概念
本文是针对于java8引入函数式编程概念以及stream流相关的一些简单介绍 什么是函数式编程? java程序员第一反应可能会理解成类的成员方法一类的东西 此处并不是这个含义,更接近是数学上的 ...
- 函数式编程—函数的关系—is-a、has-a、use-a
is-a:函数的实现与函数类型的关系: has-a:匿名(闭包)函数的创建者与匿名函数的关系:匿名函数与环境和上下文(函数)的关系: use-a:高阶函数与参量函数的关系: 函数式编程的基本功之一就是 ...
- C#函数式编程之缓存技术
缓存技术 该节我们将分成两部分来讲解,第一部分为预计算,第二部分则为缓存.缓存这个技术对应从事开发的人员来说是非常熟悉的,从页面缓存到数据库缓存无处不在,而其最重要的特点就是在第一次查询后将数据缓存, ...
- C#函数式编程-高阶函数
随笔分类 -函数式编程 C#函数式编程之标准高阶函数 2015-01-27 09:20 by y-z-f, 344 阅读, 收藏, 编辑 何为高阶函数 大家可能对这个名词并不熟悉,但是这个名词所表达的 ...
- Python进阶:函数式编程(高阶函数,map,reduce,filter,sorted,返回函数,匿名函数,偏函数)...啊啊啊
函数式编程 函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计.函数就是面向过程的程序设计 ...
- (转)Python进阶:函数式编程(高阶函数,map,reduce,filter,sorted,返回函数,匿名函数,偏函数)
原文:https://www.cnblogs.com/chenwolong/p/reduce.html 函数式编程 函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数 ...
- Python---12函数式编程------12.1高阶函数
函数式编程 函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计.函数就是面向过程的程序设计 ...
- 函数与函数式编程(生成器 && 列表解析 && map函数 && filter函数)-(四)
在学习python的过程中,无意中看到了函数式编程.在了解的过程中,明白了函数与函数式的区别,函数式编程的几种方式. 函数定义:函数是逻辑结构化和过程化的一种编程方法. 过程定义:过程就是简单特殊没有 ...
随机推荐
- C++: 类成员初始化列表语法
类的成员初始化列表的初始化的基本语法,类的构造函数还可以运用此语法为其变量初始化: class Class { private: int a; int b; char ch; public: Cl ...
- CentOS部署yapi
转载自 https://www.linuxidc.com/Linux/2018-01/150513.htm 在mongoDB添加yum源时,源路径有修改,原文中的路径404不可用 一.准备工作 1.1 ...
- <02>labSQL的配置和使用方法
任务布置:制作简单地铁站点管理系统<2> 要求一:正确配置系统,建立基本正常的数据通道:要求二:实现地铁站点的登记,拥有查询功能: 正文: 今天介绍labview虚拟仪器软件中 labS ...
- 网络协议 HTTP入门学习
概述 Web 的诞生,源于三大技术的诞生,它们都是当年 Web 之父 Tim Berners-Lee 自己 开发的,世界上第一个网站诞生的时间是 1991 年,三大技术的诞生也就是在此之前的不久: 1 ...
- (Python3) 连加 连乘 代码
a=[1,2,3,4,5,6,7,8,9,10] #连加 b=0 for i in a: b+=i print(b) #连乘 c=1 for i in a: c*=i print(c)
- 转载skbbuf整理笔记
1.http://blog.csdn.net/yuzhihui_no1/article/details/38666589 2.http://blog.csdn.net/yuzhihui_no1/art ...
- JAVA集合1--总体框架
JAVA集合是JAVA提供的工具包,包含了常用的数据结构:集合.链表.栈.队列.数组.映射等.JAVA集合工具包的位置是java.util.* JAVA集合主要可以分为4个部分:List.Set.Ma ...
- Codeforces Round #527 (Div. 3) C. Prefixes and Suffixes
题目链接 题意:给你一个长度n,还有2*n-2个字符串,长度相同的字符串一个数前缀一个是后缀,让你把每个串标一下是前缀还是后缀,输出任意解即可. 思路;因为不知道前缀还是后缀所以只能搜,但可以肯定的是 ...
- Lambda表达式与函数式接口
Lambda表达式的类型,也被称为目标类型(targer type),Lambda表达式的目标类型必须是"函数式接口(functional interface)".函数式接口代表只 ...
- C# 获取电脑配置信息
对于软件绑定电脑常用到的方法汇总 public class Computer { public string MyProperty { get; set; } /// <summary> ...