函数式编程的理解
对于函数式编程,用我刚开始的无知来理解就是:”把一些操作封装成函数,然后进行调用”。后来一想不太对,要是只有这么简单,那么怎么还会有这样一个概念产生呢。函数谁都会写,谁都会调用。
事实上,函数式编程本身并没有那么简单,就像“面向对象”一样。它是一种思想,一种编程范式。
纯函数的概念
对于相同的输入,永远会输出相同的内容,并且不会产生任务副作用
把这句定义拆成两点:
- 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)
。即y
和x
的映射。
JS中数组方法里有slice
和splice
方法,都是用来截取数组的。但是他们一个是纯函数,而一个水是纯函数。为什么呢?
我们直接来看例子:
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
,其实splice
和reverse
都会改变原数组,所以它们才不满足纯函数的定义。
正是由于纯函数的特性,输入什么和输出什么是可预知的。所以纯函数对于我们写单元测试是非常友好的,例如我们使用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);
其实这种操作,我们将其称之为柯里化,具体什么是柯里化呢?请看 理解柯里化函数 。
另外,如果一个函数内部是一个异步操作,那么它也不会是一个纯函数。因为异步操作的不确定性太高,这个不确定性就是“副作用”。