JavaScript Memoization

Memoization 是一种将函数返回值缓存起来的方法,在 Lisp, Ruby, Perl, Python 等语言中使用非常广泛。随着 Ajax 的兴起,客户端对服务器的请求越来越密集(经典如 autocomplete),如果有一个良好的缓存机制,那么客户端 JavaScript 程序的效率的提升是显而易见的。

Memoization 原理非常简单,就是把函数的每次执行结果都放入一个散列表中,在接下来的执行中,在散列表中查找是否已经有相应执行过的值,如果有,直接返回该值,没有才真正执行函数体的求值部分。很明显,找值,尤其是在散列中找值,比执行函数快多了。现代 JavaScript 的开发也已经大量使用这种技术。

我通过 Google 寻找了好几种 JavaScript Memoization 的实现,都不太如人愿,有的实现不能缓存递归函数,有的需要修改函数的 prototype,于是自己实现一个:

/**
 * JavaScript Momoization
 * @param {string} func name of function / method
 * @param {object} [obj] mothed’s object or scope correction object
 *
 * MIT / BSD license
 */

function Memoize(func, obj){
    var obj = obj || window,
        func = obj[func],
        cache = {};
    return function(){
        var key = Array.prototype.join.call(arguments, "");
        var key = Array.prototype.join.call(arguments, "_")
        if (!(key in cache))
            cache[key] = func.apply(obj, arguments);
        return cache[key];
    }
}

并写了一个测试案例,空口无凭,让大家亲自看看 Memoization 的威力。

见:http://realazy.org/lab/memoization.html

另,例子中的 fibonacci 函数有很多更有效率的实现方法,在此我使用最无效率的递归实现,只是为了更直达地演示 memoization.

又,longwosion 留言所提到的 key 唯一性问题,我略作修正,但应该还有更好的办法,欢迎您留言探讨。

15 条留言

  1. 1 longwosion @ 说:

    多参数时,用那样的方式形成key的话不能保证唯一性。比如,我们要写一个下面的组合函数的话。

    var comm = {
    comm: function(m, n) {
    if(n==0) return 1;
    if(m==n) return 1;
    if(n==1) return m;
    return this.comm(m-1,n) + this.comm(m-1,n-1);
    },

    comm_memo: function(m, n) {
    if(n==0) return 1;
    if(m==n) return 1;
    if(n==1) return m;
    return this.comm_memo(m-1,n) + this.comm_memo(m-1,n-1);
    }
    }

    comm.comm_memo = Memoize(’comm_memo’, comm);

    下面两组参数会获得一样的结果,显然是不对的。

    [221,3]
    [22,13]

  2. 2 fcicq @ 说:

    求 fib(n)? 乱吵一下, 当个小总结.

    1 应该使用尾递归, 只是加一个参数…
    2 应该使用矩阵乘法, 可以在 O(logn) 的时间内… :D
    or 用这个结论
    fib(2n) = fib(n-1)*fib(n) + fib(n)*fib(n+1)
    fib(2n+1) = fib(n)^2 + fib(n+1)^2
    fib(2n-1) = fib(n-1)^2 + fib(n)^2

    ps: 楼上这位的程序, 也有相当大的优化空间.

  3. 3 Lunatic Sun @ 说:

    @longwosion - 你的建议不错。

    另外,不递归的话不能够提升效率吧,如果cache能够成为obj的私有属性或许不递归也能提升效率。

  4. 4 魏中华 @ 说:

    >>随着 Ajax 的兴起,客户端对服务器的请求越来越密集(经典如 autocomplete),如果有一个良好的缓存机制,那么客户端 JavaScript 程序的效率的提升是显而易见的。

    既然需求是解决服务器请求过多的问题,那么直接缓存 Ajax 请求(URL + get/post 的数据)的结果应该可以解决问题,直接缓存函数貌似太复杂了。

  5. 5 zamanewby @ 说:

    帅! 前些日子还在想, 怎么能在前端做个很好的cache。 发现一些开源框架会缓存住selector取得的元素信息, 以为自己能做的只是尽可能缓存取来的数据。 这个利用闭包缓存住函数执行结果的方法提供了新的思路, 很赞:)

  6. 6 arthuridea @ 说:

    现在的前端开发对于程序的效率越来越高也越来越体系化了。。。
    不错,学习中。。

  7. 7 realazy @ 说:

    @魏中华

    或许我没说得太明白。我的意思是,在自动完成的输入过程中,在客户端缓存已经查找过的结果,在没有离开页面时,再次查找相同的值时,直接使用缓存结果了,并不用重新请求(即使请求已经缓存),而且也节省了函数的运行时间。

  8. 8 闲耘 @ 说:

    学习了。之前有关于“兼容性记忆体”的想法:就是浏览器在第一次执行某兼容代码时,记忆此时选择执行的分支,以确认当前浏览器支持的对象/方法,在以后的执行过程中,直接使用记忆体里的对象/方法。部分示例如下:
    XmlHttpRequest.AXO = [
    'MSXML3.XMLHTTP.5.0',
    'MSXML3.XMLHTTP.4.0',
    'MSXML3.XMLHTTP.3.0',
    'MSXML3.XMLHTTP.2.0',
    "Msxml3.XMLHTTP",
    "Msxml2.XMLHTTP.5.0",
    "Msxml2.XMLHTTP.4.0",
    "Msxml2.XMLHTTP.3.0",
    "Msxml2.XMLHTTP",
    "Microsoft.XMLHTTP"];
    XmlHttpRequest.AXOI=0; // 兼容性记忆体。

    if(window.ActiveXObject){ // 支持ActiveX的(ie)
    for (var i=XmlHttpRequest.AXOI, l=XmlHttpRequest.AXO.length; i

  9. 9 Dragon @ 说:

    提个建议,要是Memoize函数能够接受 Refresh参数就更好了,
    毕竟实际应用中有需要刷新cache 的需求。

    btw,仰慕博主的威名,希望和博主有更多交流,方便请加我MSN:dylanjia@hotmail.com

    期盼!

  10. 10 闲耘 @ 说:

    refresh希望渺茫。
    另,参数唯一性:参数本身使用逗号分隔,何不将就。
    var key = Array.prototype.join.call(arguments);

  11. 11 dexter_yy @ 说:

    不应该把参数拼成字符串作key,如果参数里有object或数组之类的就败了哑,不如改成这样:

    function Memoize(fn){
    var cache = {}, args = [];
    return function(){
    for( var i=0, key = args.length; i second.length ) ? first.length : second.length; i

  12. 12 dexter_yy @ 说:

    …评论有字数限制么…

    我贴到BLOG上了:http://www.limboy.com/2008/04/27/javascript_memoization/

  13. 13 muqiao @ 说:

    函数中包含global对象 就不能用这个函数了

  14. 14 muqiao @ 说:

    你的文章回复系统 怎么设置的
    我的回复http://hi.baidu.com/emkiao/blog/item/6612caedb8e0c3d1b21cb124.html

  15. 15 domkey @ 说:

    这东西似乎只能用在一个函数上。绑两个以上的函数就冲突了,想法很好但还要再通用些吧。

留个言先