JavaScript Memoization 让函数也有记忆功能


Posted in Javascript onOctober 27, 2011

比如说,我们想要一个递归函数来计算 Fibonacci 数列。一个 Fibonacci 数字是之前两个 Fibonacci 数字之和。最前面的两个数字是 0 和 1。

var fibonacci = function (n) { 
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); 
}; for (var i = 0; i <= 10; i += 1) { 
document.writeln('// ' + i + ': ' + fibonacci(i)); 
} 
// 0: 0 
// 1: 1 
// 2: 1 
// 3: 2 
// 4: 3 
// 5: 5 
// 6: 8 
// 7: 13 
// 8: 21 
// 9: 34 
// 10: 55

这样是可以工作的,但是它做了很多无谓的工作。 Fibonacci 函数被调用了 453 次。我们调用了 11 次,而它自身调用了 442 次去计算可能已经被刚计算过的值。如果我们让该函数具备记忆功能,就可以显著地减少它的运算量。

我们在一个名为 memo 的数组里保存我们的储存结果,储存结果可以隐藏在闭包中。当我们的函数被调用时,这个函数首先看是否已经知道计算的结果,如果已经知道,就立即返回这个储存结果。

var fibonacci = function() { 
var memo = [0, 1]; 
var fib = function (n) { 
var result = memo[n]; 
if (typeof result !== 'number') { 
result = fib(n - 1) + fib(n - 2); 
memo[n] = result; 
} 
return result; 
}; 
return fib; 
}();

这个函数返回同样的结果,但是它只被调用了 29 次。我们调用了它 11 次,它自身调用了 18 次去取得之前储存的结果。
以上内容来自:http://demon.tw/programming/javascript-memoization.html

realazy在blog上给出了一个JavaScript Memoization的实现,Memoization就是函数返回值的缓存,比如一个函数参数与返回结果一一对应的hash列表,wiki上其实也有详细解释,我不细说了,只讨论一下具体实现的问题,realazy文中的代码有一些问题,比如直接用参数拼接成的字符串作为查询缓存结果的key,如果参数里包括对象或数组的话,就很难保证唯一的key,还有1楼评论里提到的:[221,3]和[22,13]这样的参数也无法区分。
那么来改写一下,首先还是用hash表来存放缓存数据:

function Memoize(fn){ 
var cache = {}; 
return function(){ 
var key = []; 
for( var i=0, l = arguments.length; i < l; i++ ) 
key.push(arguments[i]); 
if( !(key in cache) ) 
cache[key] = fn.apply(this, arguments); 
return cache[key]; 
}; 
}

嗯,区别是直接把数组当作键来用,不过要注意函数里的arguments是js解释器实现的一个特殊对象,并不是真正的数组,所以要转换一下……
ps: 原来的参数包括方法名称和上下文引用:fib.fib_memo = Memoize(‘fib_memo', fib),但实际上currying生成的函数里可以用this直接引用上层对象,更复杂的例子可以参考John Resig的makeClass,所以我改成直接传函数引用:fib.fib_memo = Memoize(fib.fib_memo)
这样写看上去似乎很靠谱,由参数组成的数组不是唯一的么。但实际上,数组之所以能作为js对象的属性名称来使用,是因为它被当作字符串处理了,也就是说如果你给函数传的参数是这样:(1,2,3), cache对象就会是这个样子:{ “1,2,3″: somedata },如果你的参数里有对象,比如:(1,2,{i:”yy”}),实际的键值会是:”1,2,[object Object]“,所以这跟把数组拼接成字符串的方法其实没有区别……
示例:
var a = [1,2,{yy:'0'}]; 
var b = [1,2,{xx:'1'}]; 
var obj = {}; 
obj[a] = "111"; 
obj[b] = "222"; 
for( var i in obj ) 
alert( i + " = " + obj[i] ); //只会弹出"1,2,[object Object] = 222",obj[a] = "111"被覆盖了

直接用参数作为键名的方法不靠谱了…………换一种方法试试:
function Memoize(fn){ 
var cache = {}, args = []; 
return function(){ 
for( var i=0, key = args.length; i < key; i++ ) { 
if( equal( args[i], arguments ) ) 
return cache[i]; 
} 
args[key] = arguments; 
cache[key] = fn.apply(this, arguments); 
return cache[key]; 
}; 
}

可以完全避免上述问题,没有使用hash的键值对索引,而是把函数的参数和结果分别缓存在两个列表里,每次都先遍历整个参数列表作比较,找出对应的键名/ID号之后再从结果列表里取数据。以下是比较数组的equal方法:
function equal( first, second ){ 
if( !first || !second || first.constructor != second.constructor ) 
return false; 
if( first.length && typeof first != "string" ) 
for(var i=0, l = ( first.length > second.length ) ? first.length : second.length; i<l; i++){ 
if( !equal( first[i], second[i] ) ) return false; 
} 
else if( typeof first == 'object' ) 
for(var n in first){ 
if( !equal( first[n], second[n] ) ) return false; 
} 
else 
return ( first === second ); 
return true; 
}

千万不要直接用==来比较arguments和args里的数组,那样比较的是内存引用,而不是参数的内容。
这种方法的速度很慢,equal方法其实影响不大,但是缓存的结果数量多了以后,每次都要遍历参数列表却是很没效率的(求80以上的fibonacci数列,在firefox3和safari3上都要40ms左右)
如果在实际应用中参数变动不多或者不接受参数的话,可以参考Oliver Steel的这篇《One-Line JavaScript Memoization》,用很短的函数式风格解决问题:
function Memoize(o, p) { 
var f = o[p], mf, value; 
var s = function(v) {return o[p]=v||mf}; 
((mf = function() { 
(s(function(){return value})).reset = mf.reset; 
return value = f.apply(this,arguments); //此处修改过,允许接受参数 
}).reset = s)(); 
}

示例:
var fib = { 
temp: function(n){ 
for(var i=0;i<10000;i++) 
n=n+2; 
return n; 
} 
} 
Memoize(fib,"temp"); //让fib.temp缓存返回值 
fib.temp(16); //执行结果:20006,被缓存 
fib.temp(20); //执行结果:20006 
fib.temp(10); //执行结果:20006 
fib.temp.reset(); //重置缓存 
fib.temp(10); //执行结果:20010
Javascript 相关文章推荐
javascript或asp实现的判断身份证号码是否正确两种验证方法
Nov 26 Javascript
jQuery函数的等价原生函数代码示例
May 27 Javascript
JS获取select-option-text_value的方法
Dec 26 Javascript
Node.js抓取中文网页乱码问题和解决方法
Feb 10 Javascript
Jquery数字上下滚动动态切换插件
Aug 08 Javascript
js实现左侧网页tab滑动门效果代码
Sep 06 Javascript
开启Javascript中apply、call、bind的用法之旅模式
Oct 28 Javascript
JavaScript &amp; jQuery完美判断图片是否加载完毕
Jan 08 Javascript
jQuery zTree 异步加载添加子节点重复问题
Nov 29 jQuery
JS通过位运算实现权限加解密
Aug 14 Javascript
微信小程序实现购物车代码实例详解
Aug 29 Javascript
vue canvas绘制矩形并解决由clearRec带来的闪屏问题
Sep 02 Javascript
JavaScript 类型的包装对象(Typed Wrappers)
Oct 27 #Javascript
40款非常棒的jQuery 插件和制作教程(系列一)
Oct 26 #Javascript
JavaScript学习笔记(二) js对象
Oct 25 #Javascript
JavaScript学习笔记(一) js基本语法
Oct 25 #Javascript
jQuery数据显示插件整合实现代码
Oct 24 #Javascript
基于jquery跨浏览器显示的file上传控件
Oct 24 #Javascript
firefox下input type=&quot;file&quot;的size是多大
Oct 24 #Javascript
You might like
社区(php&amp;&amp;mysql)三
2006/10/09 PHP
利用static实现表格的颜色隔行显示
2006/10/09 PHP
php下目前为目最全的CURL中文说明
2010/08/01 PHP
PHP fopen中文文件名乱码问题解决方案
2020/10/28 PHP
JavaScript写的一个自定义弹出式对话框代码
2010/01/17 Javascript
JavaScript Event学习补遗 addEventSimple
2010/02/11 Javascript
JavaScript 面向对象编程(2) 定义类
2010/05/18 Javascript
IE6背景图片不缓存问题解决方案及图片使用策略多个方法小结
2012/05/14 Javascript
JQ实现新浪游戏首页幻灯片
2015/07/29 Javascript
jQuery给指定的table动态添加删除行的操作方法
2016/10/12 Javascript
基于Vue实例生命周期(全面解析)
2017/08/16 Javascript
vue webuploader 文件上传组件开发
2017/09/23 Javascript
vue实现简单的星级评分组件源码
2018/11/16 Javascript
微信小程序 JS动态修改样式的实现方法
2018/12/16 Javascript
微信小程序iOS下拉白屏晃动问题解决方案
2019/10/12 Javascript
vue3自定义dialog、modal组件的方法
2021/01/04 Vue.js
[19:54]夜魇凡尔赛茶话会 第一期02:看图识人
2021/03/11 DOTA
Python聊天室实例程序分享
2016/01/05 Python
Python爬虫:通过关键字爬取百度图片
2017/02/17 Python
import的本质解析
2017/10/30 Python
Python实现的根据IP地址计算子网掩码位数功能示例
2018/05/23 Python
python爬虫模拟浏览器访问-User-Agent过程解析
2019/12/28 Python
Python接口测试get请求过程详解
2020/02/28 Python
python不相等的两个字符串的 if 条件判断为True详解
2020/03/12 Python
英国复古服装和球衣购买网站:3Retro Football
2018/07/09 全球购物
Anthropologie英国:美国家喻户晓的休闲服装和家居产品品牌
2018/12/05 全球购物
Jack Rogers官网:美国经典的女性鞋靴品牌
2019/09/04 全球购物
农村改厕实施方案
2014/03/22 职场文书
服装设计师求职信
2014/06/04 职场文书
教师演讲稿开场白
2014/08/25 职场文书
停车位租赁协议书
2014/09/24 职场文书
税务干部个人整改措施思想汇报
2014/10/10 职场文书
习近平在党的群众路线教育实践活动总结大会上的讲话
2014/10/21 职场文书
医院营销工作计划
2015/01/16 职场文书
2015年社区党务工作总结
2015/04/21 职场文书
中学生打架《检讨书》范文
2019/08/12 职场文书