函数式编程的理解

对于函数式编程,用我刚开始的无知来理解就是:”把一些操作封装成函数,然后进行调用”。后来一想不太对,要是只有这么简单,那么怎么还会有这样一个概念产生呢。函数谁都会写,谁都会调用。
事实上,函数式编程本身并没有那么简单,就像“面向对象”一样。它是一种思想,一种编程范式。

纯函数的概念

对于相同的输入,永远会输出相同的内容,并且不会产生任务副作用

把这句定义拆成两点:

  • 1.相同的输入永远有相同的输出
  • 2.不会产生任何的副作用

我们先来看第1点,比如我们有一个sum函数,用来计算两个值的和:

function sum(a, b){
   return a + b;
}

这个函数,不管我们进行多少次重复的调用,他都会产生相同的输出:

sum(1,2);  //3
sum(1,2);  //3
sum(1,2);  //3

其实这个函数就是一个纯函数,因为在多次相同的输入1,2之后,我们永远得到相同的值3。其实我们程序里的函数式编程就是数学里函数的概念: y=f(x),例如 :y=sin(x)。即yx的映射。

JS中数组方法里有slicesplice方法,都是用来截取数组的。但是他们一个是纯函数,而一个水是纯函数。为什么呢?
我们直接来看例子:

let arr = [1,2,3,4,5,6,7,8,9];

console.log(arr.slice(0,3));   //[1,2,3]
console.log(arr.slice(0,3));   //[1,2,3]
console.log(arr.slice(0,3));   //[1,2,3]

console.log(arr.splice(0,3));  //[1,2,3]
console.log(arr.splice(0,3));  //[4,5,6]
console.log(arr.splice(0,3));  //[7,8,9]

可以看到slice对于相同的输入永远输出[1,2,3],它比较纯。但splice就不是一个纯函数,类似于这样的数组方法还有像reverse,其实splicereverse都会改变原数组,所以它们才不满足纯函数的定义。

正是由于纯函数的特性,输入什么和输出什么是可预知的。所以纯函数对于我们写单元测试是非常友好的,例如我们使用Jest来测试下之前写的sum

describe('sum test', () => {
  it('sum(1, 2) === 3', () => {
    expect(sum(1, 2)).toBe(3);
  });
})

下面我们来看一下第2点,不会产生任何的副作用:
假如我们有一个用来检测用户是否成年的函数:

const X = 18;
function overAge(age){
   return age > X;
}

在这个函数当中用到了来自函数外面的一个常量X,这样就会给函数带来副作用。因为函数外面的东西我们是不可控的,虽然一量X找不到了,或者被别人不小心改了,那整个函数的逻辑就会产生混乱,所以我们可以把X放到函数里面来解决:

function overAge(age){
   const X = 18;
   return age > X;
}

但这样就会产生一个18被硬编码在函数里,也是不太理想的,我们把X也通过参数传过来:

function overAge(X,age){
   return age > X;
}

如此一来,好像就能解决问题了,但是这个X在每次调用的时候都要进行传递:

overAge(18,19);
overAge(18,17);

每次都传这个18总感觉是多余的,所以我们可以通过闭包来进行解决:

function overAge(X){
   return function(age){
      return age > X;
   }
}

这样,我们这个18可以传到第一层进行缓存,然后内部利用这个缓存的X进行和age进行比较:

over18 = overAge(18);
over18(19);
over18(17);

其实这种操作,我们将其称之为柯里化,具体什么是柯里化呢?请看 理解柯里化函数

另外,如果一个函数内部是一个异步操作,那么它也不会是一个纯函数。因为异步操作的不确定性太高,这个不确定性就是“副作用”。