天天看点

javascript函数式编程-------纯函数纯函数

前面回顾了函数是一等公民,高阶函数,闭包这些函数相关概念,可以认为它们都是函数式编程的基础。

接下来学习函数式编程第一个重要概念,也是函数式编程的核心---纯函数

纯函数

函数式编程中的函数,指的就是纯函数,纯函数的概念就是对于一个函数来说,使用相同的输入始终会得到相同的输出,而且没有可观察到的副作用。关于副作用我们后面在解释。这里我们只讨论相同的输入始终会得到相同的输出。

纯函数其实就是数学中函数的概念,他是用来描述输入和输出的映射关系。比如 y=f(x);

 我们这里通过数组的两个方法slice和splice演示一下纯函数和不纯的函数。slice是返回数组中的指定部分,不会改变原数组,splice是对数组进行操作,会改变原数组。

我们这里调用了三次slice,注意纯函数的定义,相同的输出始终会得到相同的输出。

let numbers = [1, 2, 3, 4, 5]
// 纯函数
numbers.slice(0, 3)
// => [1, 2, 3]
numbers.slice(0, 3)
// => [1, 2, 3]
numbers.slice(0, 3)
// => [1, 2, 3]
           

 测试发现三次打印的结果都是一样的,所以slice就是一个纯函数。接下来我们再来演示一下splice。

我们发现每一次打印的结果都是不同的,因为每一次调用的时候都会修改原数组,每一次都会移除掉数组中的两个元素。这里相同的输入得到的输出是不一样的-------所以splice这个方法是不纯的函数。

let numbers = [1, 2, 3, 4, 5]
// 不纯的函数
numbers.splice(0, 3)
// => [1, 2, 3]
numbers.splice(0, 3)
// => [4, 5]
numbers.splice(0, 3)
// => []
           

接下来写一个纯函数,比如我们写一个计算两个数的和的函数。

对于纯函数来说,比如要有输入,也要有输出,我们这里多次调用,得到的结果都是相同的。

function getSum (n1, n2) {
    return n1 + n2;
};
console.log(getSum(1, 2));
console.log(getSum(1, 2));
console.log(getSum(1, 2));
           

在函数式编程中,不会保留中间计算的结果,所以我们就认为他的变量是不可变的,也就是无状态的。

我们在基于函数式编程的过程中我们会经常需要一些细粒度的纯函数,我们可以把一个函数的执行结果传递给另一个函数去处理,这就是函数组合。 

有很多函数式编程的库,比如lodash,有很多细粒度的函数;

纯函数的优点

纯函数的第一个好处是可缓存,因为纯函数对相同的输入始终会有相同的输出,所以可以把纯函数的结果进行缓存。

为什么要缓存函数呢,比如说我们有个函数,执行起来特别耗时,但是这个函数需要多次调用,那每次调用这个函数的时候都需要去等一段时间,才能获取到这个结果,所以他对性能来说是有影响的,使用缓存可以很好的解决这个问题,提高程序的性能。

lodash存在一个带记忆功能的函数memoize,我们定义一个球圆面积的纯函数getArea。我们想要把这个计算结果缓存下来,就要用到memoize。这个方法会返回一个带有记忆功能的函数。这里我们来模拟一下memoize内部是如何实现纯函数的缓存的。

根据memoize我们知道,这个函数执行的时候要传入一个函数f作为参数,这个f就是真实的函数,也就是上面例子中的getArea,并且返回值也是一个函数。函数的内部要存在一个对象缓存函数f执行的结果,我们可以用f函数传入的参数作为对象的键,因为用户实际调用的是返回的这个参数,所以形参应该在返回的函数中,f的执行结果作为对象的值。

在返回的函数中我们需要存储传入的参数作为键,然后判断cache中是否存在该键对应的值,如果存在,直接返回该值,如果不存在,则调用f函数,并且将执行结果存入cache再返回执行结果。

function getArea(r){
    console.log(r);
    return Math.PI*r*r;
};
function memoize(fn){
    let cash = {};
    return function(){
        let key = JSON.stringify(arguments);
        if( !cash[key] ){
            cash[key] = fn(...arguments);
        }
        return cash[key];
    }
};
const getAreaMemoize = memoize(getArea);

console.log(getAreaMemoize(4))
console.log(getAreaMemoize(4))
console.log(getAreaMemoize(4))
           

为了演示这个函数被缓存,我们可以在getArea中打印一句话,然后调用两次getAreaWithMemory。 

可以发现,当我们第一次调用getAreaWithMemory的时候,打印了getArea中的console, 第二次调用getAreaWithMemory的时候并没有打印getArea中的console。但是两次调用getAreaWithMemory都返回了相同的结果。

到这里关于纯函数的第一个好处,可缓存,我们这里就演示完了,将来我们在写程序的时候就可以通过这种方式来提高程序的性能。

纯函数的第二个好处就是可测试,因为纯函数始终有输入和输出,而单元测试就是在断言函数的结果,所以我们所有的纯函数都是可测试的函数。
另外纯函数还方便并行处理,因为在多线程环境下并行操作共享的内存数据很可能会出现意外情况,假设多个线程同时修改一个全局变量,并且每个线程修改后的值都不同,那这个变量的值最终是没办法确定的。纯函数就不会有这样的问题,因为他只依赖参数,他不能访问共享的内存数据,也就是自己作用域外的数据,所以在并行环境下可以任意运行纯函数。

副作用

纯函数的另一个特性是没有任何可观察的副作用,我们通过一段代码来演示什么是副作用

let mini = 18;
function checkAge (age) {
    return age >= mini;
};
checkAge(20); // true
mini = 28;
checkAge(20); // false
           

上面的代码就是不纯的,因为纯函数的概念是相同的输入永远有相同的输出。但因为checkAge 函数依赖于mini变量,而mini变量是可变的,所以就不能保证相同的输入始终返回相同的输出,所以checkAge 函数不是纯函数,也就是我们这个mini变量带来了副作用,函数存在副作用。

来源:除了全局变量,副作用的来源还有配置文件,我们有可能会从配置文件中获取信息。还有数据库和获取用户输入等等,这些都会带来副作用。

总结:所有的外部交互都会产生副作用,副作用也会使得方法通用性下降不适合以后的扩展和重用。同时副作用也会给程序中带来一些安全隐患,比如说用户的输入可以带来攻击。虽然副作用存在这么多问题,但是副作用是不可能完全禁止的,因为我们不可能将用户名密码等一些信息记录到代码中,这些信息还是需要放在数据库中的,我们应该尽可能的控制副作用在可控的范围内发生。

继续阅读