JavaScript 高级技巧 - 记忆化


prtyaa
prtyaa 2023-12-26 17:50:30 63232
分类专栏: 资讯
  • 计算机领域的记忆化(英语:memoization)是一种提高计算机程序执行速度的优化技术。通过存储大计算量函数的返回值,当这个结果再次被需要时将其从缓存中提取,而不再计算来节省计算时间。

    这是一种典型的用存储空间换取计算时间的方案。

    本文将首先介绍一个简单的用记忆化优化的例子,然后会介绍记忆化在 JavaScript 框架/库中的实际应用。

    阶乘

    在数学中,正整数的阶乘是所有小于及等于该数的正整数的积。例如5的阶乘是120:

    5!=5×4×3×2×1=120

    用 JavaScript 编写一个原始的函数如下,它没有用到记忆化:

    function factorial(n) {
      if (n === 1) return 1;
      return n * factorial(n - 1);
    }
    

    如下是一个用了记忆化的例子:

    const cache = [1, 1];
    function factorial2(n) {
      if (cache[n] == undefined) {
        cache[n] = n * factorial2(n - 1);
      }
      return cache[n];
    }
    

    通过增加一个变量 cache 用于保存之前计算过的数值,以数组的索引映射到对应的阶乘数值。如此这样,后续调用此函数时如果指定的参数已经计算过则直接读取,无需再次计算。

    你可能想知道这两个函数在性能上究竟有什么差异,可以运行如下的测试用例:

    var start = performance.now();
    for (let i = 0; i < 100000; i++) {
      factorial(100)
    }
    console.log(`无记忆化:${performance.now() - start} ms`)
    
    var start2 = performance.now();
    for (let i = 0; i < 100000; i++) {
      factorial2(100)
    }
    console.log(`记忆化:${performance.now() - start2} ms`)
    

    在我的电脑上运行显示如下:

    无记忆化129.80000001192093 ms
    记忆化1.699999988079071 ms
    

    可见记忆化之后能够大大改善程序性能。

    记忆化和闭包

    上面的 factorial2 函数实现了记忆化,新增的 cache 变量却出现在了全局作用域。此外它的记忆化不具有普遍适用性,无法用于其他需要记忆化的函数里。

    此时,可以借助于 JavaScript 语言的闭包特性解决这两个问题。

    /**
     * @param {Function} fn - 计算数值的函数
     * @param {Array} defaultValue - 缓存默认值
     * @return {Function} 
     */
    function factorialMemo(fn, defaultValue) {
      const cache = [...defaultValue];
      return (n) => { // 最终返回一个可以接受参数的函数
        if (cache[n] == undefined) { // 若该入参尚未有缓存
          cache[n] = fn(n); // 计算数值并存入缓存
        }
        return cache[n];// 从缓存中读取
      };
    }
    

    把 cache 变量放在了factorialMemo 函数中,它可以被视为一个“私有变量”,作用域局限于函数内部,不会污染全局变量。这个函数返回了另一个函数,返回的这个函数可以接收一个参数。

    对于阶乘的例子,可以这样使用:

    const factorial2 = factorialMemo((n) => {
      return n * factorial2(n - 1)
    }, [1, 1]);
    

    对于整数连加,可以这样使用:

    const add = factorialMemo((n) => {
      return n + add(n - 1);
    }, [1, 1]);
    console.log(add(100)); // 5050
    

    Lodash 源码中的 memoize

    function memoize(func, resolver) {
      if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) {
        throw new TypeError(FUNC_ERROR_TEXT);
      }
      var memoized = function() {
        var args = arguments,
            key = resolver ? resolver.apply(this, args) : args[0],
            cache = memoized.cache;
    
        if (cache.has(key)) {
          return cache.get(key);
        }
        var result = func.apply(this, args);
        memoized.cache = cache.set(key, result) || cache;
        return result;
      };
      memoized.cache = new (memoize.Cache || MapCache);
      return memoized;
    }
    

    在 lodash 和 underscore 中的做法是每次调用 memoize 方法时创建一个 memoized 函数,把缓存的数据保存在此函数上,最后返回此函数。

    参考这种做法,我们可以把 factorialMemo 函数改写为如下:

    function factorialMemo(fn, defaultValue) {
      const memoize = function(n) {
        const cache = memoize.cache;
        if (cache[n] === undefined) {
          cache[n] = fn(n);
        }
        return cache[n];
      };
      memoize.cache = [...defaultValue];
      return memoize;
    }
    

    使用 _.memoize 实现阶乘如下:

    const factorial = _.memoize(function(n) {
        return n === 1 ? 1 : factorial(n - 1) * n
    });
    
    console.log(factorial(5))
    

    结语

    记忆化是一种优化技术,可以避免一些不必要的重复计算,从而提高计算速度。

网站声明:如果转载,请联系本站管理员。否则一切后果自行承担。

本文链接:https://www.xckfsq.com/news/show.html?id=30941
赞同 0
评论 0 条