缓存函数,优化性能

1 首先来看一个简单的计算函数,这样写很常见,也没有什么问题。但是,如果传入的值一样, 就会出现重复计算,每调用一次就会计算一次。我们首先来考虑,怎样避免这些重复计算?

    let count = 0
    function add(a, b) {
      count ++
      return a + b
    }
    add(7, 8)
    add(7, 8)
    add(7, 8)
    add(7, 8)
    console.log('计算的次数', count) // 计算的次数 4

下面代码实现了一个闭包。里面存储了计算的结果,相同的参数避免了重复的计算。 

   function cache(fn) {
      var cacheObj = {}
      return function() {
        // 把arguments伪数组变成真数组
        let argument = Array.prototype.slice.apply(arguments)
        // 对象中如果已经有该参数,就直接去取值 不用计算
        if(cacheObj.hasOwnProperty(argument)) {
          return cacheObj[argument]
        }
        // 如果没有 就调用函数去计算,并把结果存储在cacheObj中
        return cacheObj[argument] = fn.apply(this, argument)
      }
    }
    const cacheAdd = cache(add)
    console.log(cacheAdd(7,8)) // 15
    console.log(cacheAdd(7,8)) // 15
    console.log(cacheAdd(7,8)) // 15
    console.log(cacheAdd(7,8)) // 15
    console.log('计算的次数', count) // 计算的次数 1

缓存函数写好了,下面我们就来测试一下这两个函数的性能。理论上讲,缓存函数应该比简单的add函数快很多,事实是这样吗?

    let count = 0
    function add(a, b) {
      count ++
      return a + b
    }
    console.time('add')
    for(let i=0; i<10000; i++) {
      add(1, 1)
    }
    console.timeEnd('add') // 执行次数 10000
    console.log('执行次数', count)
    // add: 0.77294921875ms 不太稳定
    let count = 0
    function add(a, b) {
      count ++
      return a + b
    }
    const cacheAdd = cache(add)
    console.time('cacheAdd')
    for(let i=0; i<10000; i++) {
      cacheAdd(1, 1)
    }
    console.timeEnd('cacheAdd') // 执行次数 1
    console.log('执行次数', count)
    // cacheAdd: 17.5810546875ms

对比一下时间发现,缓存函数并没有比非缓存函数快,相反,还会比非缓存函数慢很多。这是为什么呢?

加入我们把函数修改一下,改为 a*b, 传入大数字值,结果会怎样?

    let count = 0
    function add(a, b) {
      count ++
      return a * b
    }
    const cacheAdd = cache(add)
    console.time('cacheAdd')
    for(let i=0; i<10000; i++) {
      cacheAdd(645, 893)
    }
    console.timeEnd('cacheAdd') // 执行次数 1
    console.log('执行次数', count)
    // cacheAdd: 14.232177734375ms
    let count = 0
    function add(a, b) {
      count ++
      return a * b
    }
    // const cacheAdd = cache(add)
    console.time('add')
    for(let i=0; i<10000; i++) {
      // cacheAdd(645, 893)
      add(645, 893)
    }
    console.timeEnd('add') // 执行次数 10000
    console.log('执行次数', count)
    // add: 0.810791015625ms

然而并没有出现我们想要的结果,那是不是说缓存函数没有呢?看下面的例子

  /* 
      这是一个生成斐波纳契数列的函数,传入的参数,就是数列在第几位上的值
      在数学上,斐波纳契数列以如下被以递推的方法定义:F(1)=1,F(2)=2, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)
    */ 
    let count = 0
    function Fibonacci(n) {
      count++
      return n <= 2 ? 1 : Fibonacci(n-1) + Fibonacci(n-2)
    }
    console.time('Fibonacci')
    Fibonacci(30)
    console.timeEnd('Fibonacci') // Fibonacci: 10.988037109375ms
    console.log('执行次数', count) // 执行次数 1664079
    console.log(Fibonacci(30)) // 832040

我传递的是30,大家可以试试如果传递50会怎样?浏览器会卡死,一直转圈圈。

    let count = 0
    function Fibonacci(n) {
      count++
      return n <= 2 ? 1 : cacheFibonacci(n-1) + cacheFibonacci(n-2)
    }
    function cache(fn) {
      var cacheObj = {}
      return function() {
        // 把arguments伪数组变成真数组
        let argument = Array.prototype.slice.apply(arguments)
        // 对象中如果已经有该参数,就直接去取值 不用计算
        if(cacheObj.hasOwnProperty(argument)) {
          return cacheObj[argument]
        }
        // 如果没有 就调用函数去计算,并把结果存储在cacheObj中
        return cacheObj[argument] = fn.apply(this, argument)
      }
    }
    let cacheFibonacci = cache(Fibonacci)
    console.time('cacheFibonacci')
    cacheFibonacci(30)
    console.timeEnd('cacheFibonacci') // cacheFibonacci: 0.118896484375ms
    console.log('执行次数', count) // 执行次数 30
    console.log(cacheFibonacci(30)) // 832040

区别已经很明显,运行快了很多。计算次数也小了很多,这样我们算出第100位斐波那契数列值也不是问题。

2 上面我们亲自验证了缓存函数的好处,那我们写的缓存函数有没有什么问题呢?是不是百分之百都回返回正确的值呢?看下面的例子

    function add(obj) {
      return obj.a
    }
    function cache(fn) {
      var cacheObj = {}
      return function() {
        // 把arguments伪数组变成真数组
        let argument = Array.prototype.slice.apply(arguments)
        // 对象中如果已经有该参数,就直接去取值 不用计算
        if(cacheObj.hasOwnProperty(argument)) {
          return cacheObj[argument]
        }
        // 如果没有 就调用函数去计算,并把结果存储在cacheObj中
        return cacheObj[argument] = fn.apply(this, argument)
      }
    }
    let cacheAdd = cache(add)
    console.log(cacheAdd({a:1})) // 1
    console.log(cacheAdd({a:2})) // 1
    console.log(cacheAdd({a:3})) // 1

参数传入的是对象,每次返回的值都是1 这是为什么?我门来看下cacheObj是什么样的?

         console.log(cacheObj) 
        /* {[object Object]: 1}
          [object Object]: 1
          __proto__: Object
          */

可以看到cacheObj中只有一个属性,[object,object],因为参数会作为key存储,key在对象中以字符串的形式存在的,这里有一个隐士转换,生成key的过程中,也把调用了对象的toString方法,参数都会转变成[object,object]。每次执行的时候就会去取第一次存下来来的值,所以一直就只能拿到一个结果。我们可以在 Array.prototype.slice.apply(arguments)改一下代码,JSON.stringfy(Array.prototype.slice.apply(arguments))就可以了。

    let count = 0
    function add(obj) {
      count++
      return obj.a
    }
    function cache(fn) {
      var cacheObj = {}
      return function() {
        // 把arguments伪数组变成真数组
        let argument = Array.prototype.slice.apply(arguments)
        let argumentString = JSON.stringify(Array.prototype.slice.apply(arguments))
        console.log(cacheObj)
        // 对象中如果已经有该参数,就直接去取值 不用计算
        if(cacheObj.hasOwnProperty(argument)) {
          return cacheObj[argumentString]
        }
        // 如果没有 就调用函数去计算,并把结果存储在cacheObj中
        return cacheObj[argumentString] = fn.apply(this, argument)
      }
    }
    let cacheAdd = cache(add)
    console.log(cacheAdd({a:1, b:3, c:4})) // 1
    console.log(cacheAdd({a:1, c:4, b:3})) // 1
    console.log(cacheAdd({c:1, b:3, a:3})) // 1
    console.log('执行次数', count) // 执行次数 3

看下上面的例子,我传递的对象是一样的,就是换了顺序,函数就执行了3次,而我们希望的值执行一次。打印一下cacheObj会发现{[{"a":1,"b":3,"c":4}]: 1, [{"a":1,"c":4,"b":3}]: 1}结果是这样的。所以在使用缓存函数的时候,要注意传递参数问题。

猜你喜欢

转载自blog.csdn.net/qq_41831345/article/details/93631575
今日推荐