JavaScript学习笔记之函数记忆


Posted in Javascript onSeptember 06, 2017

本文讲解函数记忆与菲波那切数列的实现,分享给大家,具体如下

定义

函数记忆是指将上次的计算结果缓存起来,当下次调用时,如果遇到相同的参数,就直接返回缓存中的数据。

举个例子:

function add(a, b) {
  return a + b;
}

// 假设 memorize 可以实现函数记忆
var memoizedAdd = memorize(add);

memoizedAdd(1, 2) // 3
memoizedAdd(1, 2) // 相同的参数,第二次调用时,从缓存中取出数据,而非重新计算一次

原理

实现这样一个 memorize 函数很简单,原理上只用把参数和对应的结果数据存到一个对象中,调用时,判断参数对应的数据是否存在,存在就返回对应的结果数据。

第一版

我们来写一版:

// 第一版 (来自《JavaScript权威指南》)
function memoize(f) {
  var cache = {};
  return function(){
    var key = arguments.length + Array.prototype.join.call(arguments, ",");
    if (key in cache) {
      return cache[key]
    }
    else return cache[key] = f.apply(this, arguments)
  }
}

我们来测试一下:

var add = function(a, b, c) {
 return a + b + c
}

var memoizedAdd = memorize(add)

console.time('use memorize')
for(var i = 0; i < 100000; i++) {
  memoizedAdd(1, 2, 3)
}
console.timeEnd('use memorize')

console.time('not use memorize')
for(var i = 0; i < 100000; i++) {
  add(1, 2, 3)
}
console.timeEnd('not use memorize')

在 Chrome 中,使用 memorize 大约耗时 60ms,如果我们不使用函数记忆,大约耗时 1.3 ms 左右。

注意

什么,我们使用了看似高大上的函数记忆,结果却更加耗时,这个例子近乎有 60 倍呢!

所以,函数记忆也并不是万能的,你看这个简单的场景,其实并不适合用函数记忆。

需要注意的是,函数记忆只是一种编程技巧,本质上是牺牲算法的空间复杂度以换取更优的时间复杂度,在客户端 JavaScript 中代码的执行时间复杂度往往成为瓶颈,因此在大多数场景下,这种牺牲空间换取时间的做法以提升程序执行效率的做法是非常可取的。

第二版

因为第一版使用了 join 方法,我们很容易想到当参数是对象的时候,就会自动调用 toString 方法转换成 [Object object],再拼接字符串作为 key 值。我们写个 demo 验证一下这个问题:

var propValue = function(obj){
  return obj.value
}

var memoizedAdd = memorize(propValue)

console.log(memoizedAdd({value: 1})) // 1
console.log(memoizedAdd({value: 2})) // 1

两者都返回了 1,显然是有问题的,所以我们看看 underscore 的 memoize 函数是如何实现的:

// 第二版 (来自 underscore 的实现)
var memorize = function(func, hasher) {
  var memoize = function(key) {
    var cache = memoize.cache;
    var address = '' + (hasher ? hasher.apply(this, arguments) : key);
    if (!cache[address]) {
      cache[address] = func.apply(this, arguments);
    }
    return cache[address];
  };
  memoize.cache = {};
  return memoize;
};

从这个实现可以看出,underscore 默认使用 function 的第一个参数作为 key,所以如果直接使用

var add = function(a, b, c) {
 return a + b + c
}

var memoizedAdd = memorize(add)

memoizedAdd(1, 2, 3) // 6
memoizedAdd(1, 2, 4) // 6

肯定是有问题的,如果要支持多参数,我们就需要传入 hasher 函数,自定义存储的 key 值。所以我们考虑使用 JSON.stringify:

var memoizedAdd = memorize(add, function(){
  var args = Array.prototype.slice.call(arguments)
  return JSON.stringify(args)
})

console.log(memoizedAdd(1, 2, 3)) // 6
console.log(memoizedAdd(1, 2, 4)) // 7

如果使用 JSON.stringify,参数是对象的问题也可以得到解决,因为存储的是对象序列化后的字符串。

适用场景

我们以斐波那契数列为例:

var count = 0;
var fibonacci = function(n){
  count++;
  return n < 2? n : fibonacci(n-1) + fibonacci(n-2);
};
for (var i = 0; i <= 10; i++){
  fibonacci(i)
}

console.log(count) // 453

我们会发现最后的 count 数为 453,也就是说 fibonacci 函数被调用了 453 次!也许你会想,我只是循环到了 10,为什么就被调用了这么多次,所以我们来具体分析下:

当执行 fib(0) 时,调用 1 次

当执行 fib(1) 时,调用 1 次

当执行 fib(2) 时,相当于 fib(1) + fib(0) 加上 fib(2) 本身这一次,共 1 + 1 + 1 = 3 次

当执行 fib(3) 时,相当于 fib(2) + fib(1) 加上 fib(3) 本身这一次,共 3 + 1 + 1 = 5 次

当执行 fib(4) 时,相当于 fib(3) + fib(2) 加上 fib(4) 本身这一次,共 5 + 3 + 1 = 9 次

当执行 fib(5) 时,相当于 fib(4) + fib(3) 加上 fib(5) 本身这一次,共 9 + 5 + 1 = 15 次

当执行 fib(6) 时,相当于 fib(5) + fib(4) 加上 fib(6) 本身这一次,共 15 + 9 + 1 = 25 次

当执行 fib(7) 时,相当于 fib(6) + fib(5) 加上 fib(7) 本身这一次,共 25 + 15 + 1 = 41 次

当执行 fib(8) 时,相当于 fib(7) + fib(6) 加上 fib(8) 本身这一次,共 41 + 25 + 1 = 67 次

当执行 fib(9) 时,相当于 fib(8) + fib(7) 加上 fib(9) 本身这一次,共 67 + 41 + 1 = 109 次

当执行 fib(10) 时,相当于 fib(9) + fib(8) 加上 fib(10) 本身这一次,共 109 + 67 + 1 = 177 次
所以执行的总次数为:177 + 109 + 67 + 41 + 25 + 15 + 9 + 5 + 3 + 1 + 1 = 453 次!

如果我们使用函数记忆呢?

var count = 0;
var fibonacci = function(n) {
  count++;
  return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
};

fibonacci = memorize(fibonacci)

for (var i = 0; i <= 10; i++) {
  fibonacci(i)
}

console.log(count) // 12

我们会发现最后的总次数为 12 次,因为使用了函数记忆,调用次数从 453 次降低为了 12 次!

兴奋的同时不要忘记思考:为什么会是 12 次呢?

从 0 到 10 的结果各储存一遍,应该是 11 次呐?咦,那多出来的一次是从哪里来的?

所以我们还需要认真看下我们的写法,在我们的写法中,其实我们用生成的 fibonacci 函数覆盖了原本了 fibonacci 函数,当我们执行 fibonacci(0) 时,执行一次函数,cache 为 {0: 0},但是当我们执行 fibonacci(2) 的时候,执行 fibonacci(1) + fibonacci(0),因为 fibonacci(0) 的值为 0, !cache[address] 的结果为 true,又会执行一次 fibonacci 函数。原来,多出来的那一次是在这里!

多说一句

也许你会觉得在日常开发中又用不到 fibonacci,这个例子感觉实用价值不高呐,其实,这个例子是用来表明一种使用的场景,也就是如果需要大量重复的计算,或者大量计算又依赖于之前的结果,便可以考虑使用函数记忆。而这种场景,当你遇到的时候,你就会知道的。

Javascript 相关文章推荐
js实现的网页颜色代码表全集
Jul 17 Javascript
JavaScript对象之间的转换 jQuery对象和原声DOM
Mar 07 Javascript
location对象的属性和方法应用(解析URL)
Apr 12 Javascript
用JavaScript实现动画效果的方法
Jul 20 Javascript
JQuery性能优化的几点建议
May 14 Javascript
JQuery做的一个简单的点灯游戏分享
Jul 16 Javascript
Iframe实现跨浏览器自适应高度解决方法
Sep 02 Javascript
用javascript关闭本窗口技巧小结
Sep 05 Javascript
jQuery中innerHeight()方法用法实例
Jan 19 Javascript
jQuery控制文本框只能输入数字和字母及使用方法
May 26 Javascript
JS操作xml对象转换为Json对象示例
Mar 25 Javascript
如何使用Bootstrap 按钮实例详解
Mar 29 Javascript
node.js实现的装饰者模式示例
Sep 06 #Javascript
JavaScript使用FileReader实现图片上传预览效果
Mar 27 #Javascript
js防刷新的倒计时代码 js倒计时代码
Sep 06 #Javascript
JavaScript中运算符规则和隐式类型转换示例详解
Sep 06 #Javascript
详解Vue.js组件可复用性的混合(mixin)方式和自定义指令
Sep 06 #Javascript
轻松玩转BootstrapTable(后端使用SpringMVC+Hibernate)
Sep 06 #Javascript
vue mixins组件复用的几种方式(小结)
Sep 06 #Javascript
You might like
JavaScript表达式:URL 协议介绍
2013/03/10 Javascript
js中opener与parent的区别详细解析
2014/01/14 Javascript
使用jQuery管理选择结果
2015/01/20 Javascript
将JavaScript的jQuery库中表单转化为JSON对象的方法
2015/11/17 Javascript
jquery实现二级导航下拉菜单效果
2015/12/18 Javascript
Avalonjs 实现简单购物车功能(实例代码)
2017/02/07 Javascript
使用Browserify来实现CommonJS的浏览器加载方法
2017/05/14 Javascript
JS实现的加减乘除四则运算计算器示例
2017/08/09 Javascript
基于Vue.js 2.0实现百度搜索框效果
2020/12/28 Javascript
JS实现手写parseInt的方法示例
2017/09/24 Javascript
Vue-router 切换组件页面时进入进出动画方法
2018/09/01 Javascript
教你如何用Node实现API的转发(某音乐)
2019/09/20 Javascript
js实现简易计算器功能
2019/10/18 Javascript
JS实现canvas简单小画板功能
2020/06/23 Javascript
Vue 按照创建时间和当前时间显示操作(刚刚,几小时前,几天前)
2020/09/10 Javascript
[52:03]Secret vs VG 2018国际邀请赛小组赛BO2 第一场 8.17
2018/08/20 DOTA
Python实现的tab文件操作类分享
2014/11/20 Python
Python全局变量用法实例分析
2016/07/19 Python
python中文分词教程之前向最大正向匹配算法详解
2017/11/02 Python
Python tornado队列示例-一个并发web爬虫代码分享
2018/01/09 Python
Python实现上下班抢个顺风单脚本
2018/02/07 Python
python简单操作excle的方法
2018/09/12 Python
python opencv 批量改变图片的尺寸大小的方法
2019/06/28 Python
使用pygame写一个古诗词填空通关游戏
2019/12/03 Python
使用TensorFlow直接获取处理MNIST数据方式
2020/02/10 Python
利用CSS3实现开门效果实例源码
2016/08/22 HTML / CSS
简单html5代码获取地理位置
2014/03/31 HTML / CSS
美国顶级品牌男士大码服装店:DXL
2017/08/30 全球购物
戴尔荷兰官方网站:Dell荷兰
2020/10/04 全球购物
MYSQL相比于其他数据库有哪些特点
2013/07/19 面试题
村委会换届选举方案
2014/05/03 职场文书
机械工程学院大学生求职信
2014/05/25 职场文书
推普周活动总结
2014/08/28 职场文书
自主招生自荐信格式范文
2015/03/25 职场文书
法律意见书范文
2015/05/20 职场文书
tensorflow中的梯度求解及梯度裁剪操作
2021/05/26 Python