Understanding Memoization [译]

788 阅读4分钟

随着应用的不断成长且开始执行诸多复杂的计算 , 这使得对于速度和程序执行过程的优化的需求变得越来越重要.若我们对其视而不见 , 我们的程序在执行期间将会花费和占用大量的时间和系统资源.

原文 : Understanding Memoization In JavaScript

目录

Memoization即记忆化或缓存,下文均使用缓存来指称Memoization

  • WHAT - 什么是缓存 ?

  • WHY - 为什么缓存重要 ?

  • HOW - 缓存如何工作 ?

  • 用例学习 :斐波那契数列

  • 使用JSPerf进行性能测试

  • 编写功能方法 memoizer测试

  • 何时使用缓存?

  • 缓存库

  • 总结

# 什么是缓存 ?

概念

所谓缓存即通过__保存昂贵函数调用的结果和在发生相同的输入时从缓存的结果中直接返回__来提升应用速度的一种优化技术手段。

何为昂贵的函数调用?

在计算机程序中 ,两种最主要的资源是__时间和内存__。因此由于复杂的计算在执行期间消耗了大量的这两种资源的函数调用就是昂贵的函数调用。

# 为什么缓存重要 ?

举个形象的🌰来说明缓存的重要性:

场景 :你在公园里正阅读一本封面极具吸引力的书籍,每个行人经过时都被封面吸引了并向你询问书名和作者。每当有人问你这个相同的问题时你只能把书转过来然后将书名和作者念出来。随着公园的游客越来越多 ,问的人也越来越多 ,你作为一个nice person总会不厌其烦地回答每一个人。

每次将书翻到封面并读出书名和作者还是直接将书名和作者记住之后直接回答问题省时间不言而喻。

当一个函数接收到一个输入后,在进行必要的计算后先将计算结果保存到缓存中再返回。当下次再接收到相同的输入时便可以直接从缓存中返回而无需进行多余的重复计算。

# 缓存如何工作?

js中的缓存基于两个重要的概念 :

  • 闭包
  • 高阶函数(返回值是函数的函数)

闭包由一个函数以及在该函数中声明的词法环境(词法作用域)组成,举例如下:

词法作用域指的是指定的变量和代码块(即{})的__物理位置__。

function foo(a) {
  var b = a + 2;
  function bar(c) {
    console.log(a, b, c);
}
  bar(b * 2);
}

foo(3); // 3, 5, 10
  • global scope : 只包含了 foo标识符
  • foo scope : 包含了标识符a,b,bar
  • bar scope : 只包含了标识符c

注意 , bar访问了内嵌于foo的变量a/b , 且成功将函数bar保存到foo的环境中,因此我们说barfoo的作用域上是一个闭包.

function foo(){
  var a = 2;

  function bar() {
    console.log(a);
  }
  return bar;
}
var baz = foo();
baz();//2

非常有趣的是当我们在foo的词法作用域外执行函数baz , 其仍然能获取到a的值 , 记住baz总能访问到foo中的变量即便是在foo的词法作用域之外.

# 用例学习 : 斐波那契数列

斐波那契数列 : 从第三项开始 , 每项的值等于其前两项的值的和

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …

挑战 : 编写一个返回斐波那契数列第 n 个元素的函数 , 数列如下 :

[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …]
function fibonacci(n) {
    if (n <= 1) {
        return 1
    }
    return fibonacci(n - 1) + fibonacci(n - 2);
}
//每次计算 fibonacci(5) 都得计算 fibonacci(4) 和 fibonacci(3) , 以此类推
//使用缓存来优化 , 将计算过的 fibonacci(n) 的值保存起来备用
function fibonacciMemo(n,memo) {
    memo = memo || {}
    if (memo[n]) {
        return memo[n]
    }
    if (n <= 1) {
        return 1
    }
    return memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo)
}

# 使用JSPerf进行性能测试

this link to the performance test on JSPerf

# memoizer

编写一个高阶函数memoizer , 用于返回任意函数的缓存版本.

function memoizer(fn){
  let cache = {};
  return function(n){
    if(cache[n] !== void 0){
      return cache[n];
    }else{
			let result = fn(n);
      cache[n] = result;
      return result;
    }
  }
}

测试结果见上一节的JSPerf.

Ops/sec指每秒操作数 , 代表一秒内指定的测试用例执行了多少次.

# 何时使用缓存?

一下情况使用缓存受益更佳:

  • 承载了沉重且大量计算的昂贵函数调用

  • 输入情况比较有限 , 高频出现相同的输入的表单域

  • 在递归函数中 , 高频重复出现的输入值

  • 用于纯函数 , 如 : 对于特定的输入值 , 输出值也总是相同的函数

# 缓存库有哪些?

# 总结

Memoization能有效防止重复调用函数去计算相同的结果而带来的性能损耗.