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 相关文章推荐
如何使用json在前后台进行数据传输实例介绍
Apr 11 Javascript
将字符串中由空格隔开的每个单词首字母大写
Apr 06 Javascript
JavaScript获取页面中第一个锚定文本的方法
Apr 03 Javascript
常用jQuery代码分享
Jul 14 Javascript
浅谈Javascript中substr和substring的区别
Sep 30 Javascript
jquery淡入淡出效果简单实例
Jan 14 Javascript
js html5 css俄罗斯方块游戏再现
Oct 17 Javascript
Node.js中看JavaScript的引用
Apr 22 Javascript
百度地图去掉marker覆盖物或者去掉maker的label文字方法
Jan 26 Javascript
JavaScript实现的DOM树遍历方法详解【二叉DOM树、多叉DOM树】
May 07 Javascript
inquirer.js一个用户与命令行交互的工具详解
May 18 Javascript
Vue实现开关按钮拖拽效果
Sep 22 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
咖啡的化学
2021/03/03 咖啡文化
[原创]效率较高的php下读取文本文件的代码
2008/07/02 PHP
php下防止单引号,双引号在接受页面转义的设置方法
2008/09/25 PHP
php中switch与ifelse的效率区别及适用情况分析
2015/02/12 PHP
CI框架AR操作(数组形式)实现插入多条sql数据的方法
2016/05/18 PHP
javascript eval函数深入认识
2009/02/21 Javascript
JavaScript 获取用户客户端操作系统版本
2009/08/25 Javascript
jQuery AJAX 调用WebService实现代码
2010/03/24 Javascript
屏蔽网页右键复制和ctrl+c复制的js代码
2013/01/04 Javascript
jQuery常用且重要方法汇总
2015/07/13 Javascript
JavaScript中创建对象的模式汇总
2016/04/19 Javascript
jQuery改变form表单的action,并进行提交的实现代码
2016/05/25 Javascript
JavaScript-html标题滚动效果的简单实现
2016/09/08 Javascript
微信小程序 基础组件与导航组件详细介绍
2017/02/21 Javascript
使用react实现手机号的数据同步显示功能的示例代码
2018/04/03 Javascript
vue-cli 打包使用history模式的后端配置实例
2018/09/20 Javascript
React父子组件间的传值的方法
2018/11/13 Javascript
通过扫小程序码实现网站登陆功能
2019/08/22 Javascript
[03:04]DOTA2英雄基础教程 影魔
2013/12/11 DOTA
import的本质解析
2017/10/30 Python
Python进阶之递归函数的用法及其示例
2018/01/31 Python
Django ManyToManyField 跨越中间表查询的方法
2018/12/18 Python
python3 使用traceback定位异常实例
2020/03/09 Python
python中scrapy处理项目数据的实例分析
2020/11/22 Python
纯CSS实现的大小渐变、渐远效果
2014/04/15 HTML / CSS
英国奢侈品牌时尚购物平台:Farfetch(支持中文)
2020/02/18 全球购物
科技开发中心办公室主任岗位责任制
2014/02/10 职场文书
学生意外伤害赔偿协议书
2014/09/17 职场文书
介绍信怎么写
2015/01/30 职场文书
个性发展自我评价2015
2015/03/09 职场文书
超搞笑婚前保证书
2015/05/08 职场文书
二十年同学聚会感言
2015/07/30 职场文书
爱岗敬业先进典型事迹材料(2016推荐版)
2016/02/26 职场文书
2016年世界艾滋病日宣传活动总结
2016/04/01 职场文书
2019优秀干部竞聘演讲稿范文!
2019/07/02 职场文书
python中取整数的几种方法
2021/11/07 Python