去往js函数式编程(2)

  Memoization:备忘录技术。由于纯函数对于给定的输入失踪产生相同的输出,所以可以缓存函数的结果,避免可能昂贵的重新计算。这个过程意味着只在第一次计算表达式并将结果缓存起来,供后续调用使用,称为备忘录技术。

  斐波那契数列常用于此示例,因为它简单切隐藏了计算成本。当 n=0 时,fib(n)=0。当 n=1 时,fib(n)=1.当 n>1 时,fib(n)=fib(n-2)+fib(n-1).

const fib = (n) => {
  if (n == 0) {
    return 0
  } else if (n == 1) {
    return 1
  } else {
    return fib(n - 2) + fib(n - 1)
  }
}

console.log(fib(10)) // 55

// 一行也可以
const fib = (n) => (n <= 1 ? n : fib(n - 2) + fib(n - 1))

  如果你尝试使用不断增长的 n 值来运行这个函数,很快就会意识到一个问题,计算时间开始变得太长。

let cache = []
const fib2 = (n) => {
  if (cache[n] === undefined) {
    if (n === 0) {
      cache[0] = 0
    } else if (n === 1) {
      cache[n] = fib2(n - 2) + fib2(n - 1)
    }
  }
  return cache[n]
}

console.log(fib2(10)) //55,与之前相同,但速度更快!

  初始时,缓存数组为空。每当我们需要计算 fib2(n)的值时,我们检查它是否已经在之前计算过。如果没有计算过,我们进行计算,但有一个变化:我们不立即返回值,而是先将其存储在缓存中,然后再返回。这样我们将不会在重复计算已经计算过的值。当然我们可以用高阶函数来实现,而且全局缓存也不是好实践,应该使用 IIFE 和闭包来隐藏缓存。当然,你也可能因为耗尽所有可用的 RAM 而导致应用程序崩溃。

const fib = (() => {
  const cache = {}

  const fibonacci = (n) => {
    if (n in cache) {
      return cache[n]
    } else {
      if (n === 0) {
        cache[n] = 0
      } else if (n === 1) {
        cache[n] = 1
      } else {
        cache[n] = fibonacci(n - 2) + fibonacci((n = 1))
      }
      return cache[n]
    }
  }
  return fibonacci
})()

console.log(fib(10)) // 55

  当然,并不需要对程序中每个纯函数都进行这样的优化。只需要针对那些被频繁调用且耗时较长的函数进行这种优化,如果情况不是这样的话,那么额外的缓存管理时间最终会比你期望节省的时间更多。

  纯函数还有一个有点。由于函数需要的一切都通过参数传递给它,没有任何隐藏的依赖关系,所以当你阅读源代码时,你拥有了理解函数目标的一切信息。而且,当你知道一个函数不会访问其参数之外的任何东西,使你在使用它时更加自信,因为你不会意外产生副作用;函数只会完成其文档中所描述的任务。

  减少副作用时一个很好的目标,但我们不应该过分追求!所以让我们考虑如何避免使用不纯的函数,如果不可能的话,如何处理它们,并寻找最好的方式来限制或限定它们的作用范围。

  使用全局状态(无论是获取还是设置),解决方案已经广为人知。其中关键要点如下:将全局状态作为参数提供给函数;如果函数需要更新状态,不应直接进行更新,而是生成一个新版本的状态并返回它;调用者的责任是获取返回的状态(如果有的话)并更新全局状态。这就是 Redux 在其 reducer 中使用的技术。reducer 一般写成(previousState,action)=>newState,意味着它接受状态和动作作为参数,并将新状态作为结果返回。特别要注意的是,reducer 不能简单地更改 proviousState 参数,它必须保持不变。

  如果一个函数因为需要调用另一个非纯函数而变得非纯,解决这个问题的一种方式是在调用时注入所需的函数。

const getRandomFileName = (fileExtension = '') => {
  //...
  for (let i = 0; i < NAME_LENGTH; i++) {
    namePart[i] = getRandomLetter()
  }
  //...
}

// 解决这个问题的一种方法是用一个注入的外部函数替代非纯函数;

const getRandomFileName2 = (fileExtension = '', randomLetterFunc) => {
  const NAME_LENGTH = 12
  let namePart = new Array(NAME_LENGTH)
  for (let i = 0; i < NAME_LENGTH; i++) {
    namePart[i] = randomLetterFunc()
  }
  return namePart.join('') + fileExtension
}

// 生成文件名的方法同样可以应用技巧,写一个新版本

const getRandomLetter = (getRandomInt = Math.random) => {
  const min = 'A'.charCodeAt()
  const max = 'Z'.charCodeAt()
  return String.fromCharCode(Math.floor(getRandomInt() * (1 + max - min))) + min
}

  你能确保一个函数实际上是纯函数吗?const sum3=(x,y,z)=>x+y+z;你认为这个函数是纯函数吗?看起来是的!这个函数除了它的参数之外没有访问任何东西,甚至不试图修改它们,也不执行任何 I/O 操作,也不使用我们之前提到的任何非纯函数或方法。

let x = {}
x.valueOf = Math.random
let y = 1
let z = 2
console.log(sum3(x, y, z)) // 3.2034400919849431
console.log(sum3(x, y, z)) // 3.8537045249277906
console.log(sum3(x, y, z)) // 3.0833258308458734

  sum3()应该是纯函数,但它实际上取决于你传递给它的参数;在js中,你可以使一个纯函数以非纯的方式运行!你可能安慰自己,认为肯定没有人会传递这样的参数,但边缘情况通常是错误存在的地方。

posted @ 2023-05-29 14:16  艾路  阅读(8)  评论(0编辑  收藏  举报