纯函数编程,避免意外的bug

131 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第27天,点击查看活动详情

并非所有的函数都是平等的,这是我们学习编程首先要知道的知识,对于js函数来说,更是如此。

什么是纯函数

首先我们回顾一下什么是函数?函数是一段可以调用的代码,可能会带有参数,执行一系列操作,然后返回另一个值。换句话说,就好像你早上给奶牛喂了草料,晚上你就可以得到牛奶了,这个比喻可能有点不恰当,但是话糙理不糙。
纯函数是能实现上述功能,同时,每次调用它都以完全相同的方式执行。这意味着,每次使用相同的参数集调用纯函数时,都会得到完全相同的结果,并且不会产生任何副作用。来看看下面这个函数:

function times2(num) {
  return num * 2;
}

这是个典型的纯函数,因为结果是完全可以预测的,对于每次相同的输入,都会得到相同的结果。

输入 -> 输出
1 -> 2
2 -> 4
3 -> 6
4 -> 8
5 -> 10

上面我们创建了一个映射函数,它将每个数字映射为它的两倍,这就是纯函数-输入和输出之间的映射。 或许你会发问,难道不应该每个函数都是输入和输出的映射吗?答案并不是,来看一些非纯函数的例子。

function whatTimeIsIt() {
  return new Date()
}

let tax = 1.24;

function getTotalCost(product) {
  return product.cost() * tax;
}

function updateUserName(user, newName) {
  user.name = newName;
  return user;
}

这三个函数展示了不同程度的非纯函数:

  • whatTimeIsIt:这个函数不接受任何参数,这意味着按照纯函数标准,它应该总是返回相同的结果,但是,每次执行它时,它都会返回不同的值,除非你在一秒钟内多次执行它。随着时间的推移,它不会保持不变。

  • getTotalCost:这个函数是不纯的,因为它依赖于一个全局变量。现在,在不做任何更改的情况下,它总是会返回相同的结果,但当有人改变tax的值时,看似纯粹的函数现在开始返回不同的值,从而打破了现有的映射。纯函数不能使用全局变量,重要的事情再说两遍,纯函数不能使用全局变量。纯函数不能使用全局变量。

  • updateUserName:这不是纯函数,因为它有一个很大的副作用:它修改了作为参数接收的对象。在JavaScript中,对象是引用传递的,因此在函数中对它们所做的任何更改都将影响原始对象。

问题来了,我们如何把它们改为纯函数呢?让她们变得像秋天的菠菜一样纯净。答案是真的可以,但是第一个函数是没法改为纯函数的,它本身的功能就不应该是纯函数,因为它返回当前的时间,后面两个是可以的。

function getTotalCost(product, tax) {
  return product.cost() * tax
}

function updateUserName(user, newName) {
  let newUser = {...user};
  newUser.name = newName;
  return newUser;
}

修复getTotalCost很简单,我们只需要把变量tax由全局变量改为参数即可。
对于updateUserName函数,我们需要克隆一个参数对象,这样就不会直接修改到原对象了,当然例子中使用的是浅克隆,更保险的方式是使用深克隆。

为什么我们要使用纯函数?

这是一个值的我们思考的问题。毕竟如果我们要写纯函数,在某些情况下我们是要克服点障碍的。克服障碍是需要成本的,那么这些成本是否值得呢?答案是肯定的。

纯函数可以避免意外

纯函数的逻辑不依赖全局变量或其他外部变量,所以可以避免一些不确定的因素。有了纯函数的帮助,我们的逻辑坚如磐石,是确定的并且是可预测的。另外,这能使得我们避免外部变量不包含某个属性或值所带来的bug。在高并发情况下,这些尤其的重要。

纯函数更加的简单

但是要知道,简单并不意味着弱小。
相反,这意味着程序员更容易掌控它们,因为每段逻辑都在函数内,更重要的是它们没有隐藏的副作用。 通过将一个纯函数的输出用作另一个纯函数的输入,这也使它们更容易组合。你能想象用不纯函数的情况吗?副作用的数量可能是灾难性的。
纯函数的简单性的另一大好处是,它也使它们更容易测试。处理纯函数的单元测试只需要关注输入和输出。

纯函数是可以缓存的

因为,对于相同的参数,它们总是返回相同的结果,这意味着您可以缓存函数调用的结果。
假设有一个纯函数需要几秒钟才能执行完,因为它需要进行一些非常复杂的计算。通过缓存这个函数的执行结果,当我们第一次执行它的时候,它会运行一段时间,但是结果会和参数一起被缓存。因此,下次使用相同的参数调用相同的函数时,我们将直接跳转到结果中,而不再执行代码。

/**
 * 把参数合并成一个字符串
 */
function getCacheKey(args) {
    let list = [...args]
    return list.reduce((prev, curr) => prev + curr, "")
}

/**
 * 带有缓存功能的函数
 */
function memoize(fn) {
    let cache = {}

    return function() {
        let key = getCacheKey(arguments);
        if(cache[key]) {
            console.log("-- Using memoized version...")
            return cache[key]
        }
        console.log("Calling the actual function...")
        let result = fn.apply(fn, arguments);
        cache[key] = result;
        return result;
    }
}

上面的函数是个简单的例子,可以使用第三方模块来做的更好。现在memoize函数返回一个新函数,它首先检查我们的函数是否已经用该参数调用。如果调用了,那么它直接返回缓存的结果,否则它调用函数,然后缓存结果以备下次使用。我们可以这样使用它:

/**
*一个纯函数
*/
function testFunction(name) {
    return "Hello " + name;
}

const memoizedFn = memoize(testFunction)

console.log(memoizedFn("Fernando"))
console.log(memoizedFn("World!"))
//这次调用会使用缓存
console.log(memoizedFn("Fernando"))

正如我们看到的,最后一次调用使用的是缓存版本,在实际工作中,这是一个很大的优化。

总结

总之,纯功能主要提供了三个好处:

  • 我们得到的意外bug更少,从而使代码更加稳定,尤其是在无法控制每一个逻辑的大型团队中工作时。
  • 它们更容易理解、推理和结合。
  • 它使得我们可以通过缓存来提高他们的速度。 所以尽可能地让函数保持像秋天的菠菜一样纯洁,这会让你的工作更加的轻松。

参考:blog.bitsrc.io/pure-functi…