函数式编程风格中有一个“纯函数”的概念,纯函数是一种无副作用的函数,除此之外纯函数还有一个显著的特点:对于同样的输入参数,总是返回同样的结果。在平时的开发过程中,我们也应该尽量把无副作用的“纯计算”提取出来实现成“纯函数”,尤其是涉及到大量重复计算的过程,使用纯函数+函数缓存的方式能够大幅提高程序的执行效率。本文的主题即是函数缓存实现的及应用,必须强调的是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

参考
LodashRamda源码

函数式编程 - 函数缓存Memoization的更多相关文章

  1. 翻译连载 | 附录 C:函数式编程函数库-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  2. [一] java8 函数式编程入门 什么是函数式编程 函数接口概念 流和收集器基本概念

      本文是针对于java8引入函数式编程概念以及stream流相关的一些简单介绍 什么是函数式编程?   java程序员第一反应可能会理解成类的成员方法一类的东西 此处并不是这个含义,更接近是数学上的 ...

  3. 函数式编程—函数的关系—is-a、has-a、use-a

    is-a:函数的实现与函数类型的关系: has-a:匿名(闭包)函数的创建者与匿名函数的关系:匿名函数与环境和上下文(函数)的关系: use-a:高阶函数与参量函数的关系: 函数式编程的基本功之一就是 ...

  4. C#函数式编程之缓存技术

    缓存技术 该节我们将分成两部分来讲解,第一部分为预计算,第二部分则为缓存.缓存这个技术对应从事开发的人员来说是非常熟悉的,从页面缓存到数据库缓存无处不在,而其最重要的特点就是在第一次查询后将数据缓存, ...

  5. C#函数式编程-高阶函数

    随笔分类 -函数式编程 C#函数式编程之标准高阶函数 2015-01-27 09:20 by y-z-f, 344 阅读, 收藏, 编辑 何为高阶函数 大家可能对这个名词并不熟悉,但是这个名词所表达的 ...

  6. Python进阶:函数式编程(高阶函数,map,reduce,filter,sorted,返回函数,匿名函数,偏函数)...啊啊啊

    函数式编程 函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计.函数就是面向过程的程序设计 ...

  7. (转)Python进阶:函数式编程(高阶函数,map,reduce,filter,sorted,返回函数,匿名函数,偏函数)

    原文:https://www.cnblogs.com/chenwolong/p/reduce.html 函数式编程 函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数 ...

  8. Python---12函数式编程------12.1高阶函数

    函数式编程 函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计.函数就是面向过程的程序设计 ...

  9. 函数与函数式编程(生成器 && 列表解析 && map函数 && filter函数)-(四)

    在学习python的过程中,无意中看到了函数式编程.在了解的过程中,明白了函数与函数式的区别,函数式编程的几种方式. 函数定义:函数是逻辑结构化和过程化的一种编程方法. 过程定义:过程就是简单特殊没有 ...

随机推荐

  1. Redis 高级部分

    一.主从复制   image.png Rdis 的主从复制特点   image.png 1. 配置主从 实现方式同样有两种: 命令方式和配置文件方式 命令方式 只需要在从服务器上执行如下命令即可 sl ...

  2. sshpass-Linux命令之非交互SSH密码验证

    sshpass-Linux命令之非交互SSH密码验证 参考网址:https://www.cnblogs.com/chenlaichao/p/7727554.html ssh登陆不能在命令行中指定密码. ...

  3. jvm 字节码执行 (二)动态类型支持与基于栈的字节码解释执行

    动态类型语言 动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期. 举例子解释“类型检查”,例如代码: obj.println("hello world"); 假 ...

  4. CSS 分栏结构

    CSS 固定左侧导航栏 left----左侧菜单 cont -- 实际内容 right  ---右侧附加内容   两栏布局---左侧高度为内容撑开的高度     方法一:[坏处是需要 float]   ...

  5. Eclipse——如何设置代码字体大小

    eclipse默认字体太小,1920*1080下分辨不清楚,接下来介绍一下如何更改默认字体大小: 1.window-Preferences 2.General-Appearance-Colors an ...

  6. rcu-bp关键代码解读

    1      什么是TLS 原理在网上资料很多,这里不展开. 简单点说,动态申请的每线程变量.有一类比较熟悉的每线程变量是一个带__thread的每线程变量,两者的区别在于,TLS这类每线程变量是动态 ...

  7. 自编译Apache Spark2.3.3支持CDH5.16.1

    1 下载源代码文件 https://archive.apache.org/dist/spark/spark-2.3.3/ 2 解压后导入编辑器,修改依赖的Hadoop版本,下面截图是修改后的,要看自己 ...

  8. spark2.2.1安装、pycharm连接spark配置

    一.单机版本Spark安装 Win10下安装Spark2.2.1 1. 工具准备 JDK 8u161 with NetBeans 8.2: http://www.oracle.com/technetw ...

  9. selenium定位方式-获取标签元素:find_element_by_xxx

    定位方式取舍# 唯一定位方式.多属性定位.层级+角标定位(离目标元素越近,相对定位越好) # 推荐用css selector(很少用递进层次的定位)# 什么时候用xpath呢? 当你定位元素时,必须要 ...

  10. 解决:在微信中访问app下载链接提示“已停止访问该网页”

    前言 现如今微信对第三方推广链接的审核是越来越严格了,域名在微信中分享转发经常会被拦截,一旦被拦截用户就只能复制链接手动打开浏览器粘贴才能访问,要不然就是换个域名再推,周而复始.无论是哪一种情况都会面 ...