一。函数式编程概述
首先我们需要函数式编程,主要是因为函数式编程的可读性和可维护性都较好,下图是《JavaScript轻量级函数式编程》所提供的一张图:
我们可以看到,如果我们在恰当的时候使用函数式编程可以有效的提高我们代码的可读性。另外函数式编程可以有效的提高我们代码的可维护性,这在后面也会有所讨论。
要了解函数式编程,首先就应当了解什么是函数,学过数学的应该对这个等式很熟悉:y = f(x)。我们可以看到,在数学中,一个函数总是接受输入,并且总是给出输出。而在FP中也是如此,在FP中有个术语叫做“态射(morphism)”,这个词用来描述一个值的集合映射到另一值的集合,就像一个函数的输入与这个函数的输出的关系一样。
而函数与过程区别就在于:如果我们考虑过程,那么一个任意功能的集合,它可能有输入,也可能没有,它可能有输出也可能没有;而一个函数接受数据输入并总是有一个return。当我们打算进行函数式编程的时候,我们就尽可能多的使用函数,而不是过程。
但这里的,我们使用函数式编程也是要对函数的使用是有一些要求,这也是函数式编程思想的体现,总的来说有以下几点:
- 声明式
- 纯函数
- 数据的不可变
二、声明式
既然有了声明式,那么与之相对的就是命令式的了。其实命令式的编程在我们的日常编程中是最常见的一种编程方式了。举个很简单的例子,如果我们要实现一个数组的元素全部加一,通常来说我们会这样做:
const arr = [1,2,3,4,5,6]
const addOne = (arr) => {
const result = []
for (let i = 0; i < arr.length; i++) {
result.push(arr[i] + 1)
}
return result
}
addOne(arr)
这样做乍看起来好像还可以,即实现了我们既定的目标,代码逻辑上比较清晰。但仔细想一想,在这里我们想要的是所有数组的元素都加一,那如果我们再此基础上想再实现一个所有元素加十呢,那么是不是还需要在这样做一次:
const arr = [1,2,3,4,5,6]
const addTen = (arr) => {
const result = []
for (let i = 0; i < arr.length; i++) {
result.push(arr[i] + 10)
}
return result
}
addOne(arr)
可以清楚的看到,我们两次的代码几乎一模一样,唯一不同的就是这一句语句:
result.push(arr[i] + 1)
result.push(arr[i] + 10)
因此我们就可以再此基础上这段代码进行抽象,来实现简化代码的目的,下面是一个简单的实现:
const arr = [1,2,3,4,5,6]
const getNew = (arr, fun) => {
const result = []
for (let i = 0; i < arr.length; i++) {
result.push(fun(arr[i]))
}
return result
}
const addOne = item => item + 1
const addTen = item => item + 10
const getOne = getNew(arr, addOne)
const getTen = getNew(arr, addTen)
我们可以看到,这样做我们在往后如果还需实现诸如加二十,乘二之类的操作时,就无需再将循环的逻辑再实现一遍了,只需将其所要完成的任务传入即可,当然这对数组我以上的方法还是麻烦了一些,我只是为了举个例子而已,JavaScript的数组直接为我们提供了map方法可以让我们非常容易的就实现了上述功能:
const addOne = arr => arr.map(item => item + 1)
const addTen = arr => arr.map(item => item + 10)
是不是代码非常简单明了,以上的两种方法都将for循环给封装起来了,我们只需去完成我们想要完成的功能的代码的编写即可,无需去关心其他的一些内部的实现。但如果这样的话,我们就需要考虑的是,在这些被忽略的实现里,是否会产生一些副作用的操作了,也正是由于这个原因,函数式编程又有了以下两个要求:纯函数and数据不可变
三、纯函数
熟悉redux的朋友应该都知道,redux 里的 reducer 就是一个纯函数,根据传入的 action creates 来更改 store 树。而所谓的纯函数,主要指的就是满足了一下两个条件的函数:
- 函数不会对外部的状态造成影响。
- 函数的执行过程完全有输入的参数所决定,不会受参数之外的任何数据的影响。
对于这两方面可以看一个示例:
const arr = [1, 2, 3, 4]
const addVal = (arr, newVal) => {
arr.push(newVal)
return arr
}
addVal(arr, 6)
console.log(arr) // [1, 2, 3, 4, 6]
就比如上面的代码,add函数会直接调用push函数,将新元素直接添加到数组的尾部,这样就会直接更改数组的值。这样看起来好像并没有什么副作用,但仔细想想,加入这是一份数据,当我们将数据传入这个函数的时候,会将原数据进行直接修改,这很容易导致其他引用此数据的函数出现意料不到的问题,就比如下面这个:
const arr = [1, 2, 3, 4]
const addOne = (arr) => arr.map(newVal => newVal + 1)
const addVal = (arr, newVal) => {
arr.push(newVal)
return arr
}
addVal(arr, 6)
addOne(arr)
可以看到上述代码,我们在addOne(arr)所期望的结果是arr数组内的所有元素都加上1,但实际结果却是:
[2, 3, 4, 5, 7]
所产生的结果与我们所预料的完全不同。也正是出于这番原因,我们在使用redux的时候,在reducer中,也要求reducer是一个纯函数。就是为了避免诸如此类的事情的发生。出于这番考虑,我们就可以通过对上述代码进行修改,以满足我们的需求:
const arr = [1,2,3,4]
const addOne = (arr) => arr.map(newVal => newVal + 1)
const addVal = (arr, newVal) => {
return [...arr, newVal]
}
addVal(arr, 6)
addOne(arr)
现在再去执行,就可以发现,我们所得的结果与我们所期望的是一致的:
[2, 3, 4, 5]
[1, 2, 3, 4, 6]
上面所说的副作用很简单,除此之外还有很多“不纯”的操作:
- 读取用户的输入
- 改变全局变量的值
- 改变输入参数引用的对象,就像我们上面所演示的
- 操作DOM元素
- 网络输入或输出
- 等等
四、数据不可变
数据不可变与纯函数其实也是紧密相关的,上面所提到的纯函数,其实也是数据不可变的一种体现。而所谓的数据的不可变就是通过产生新的数据来体现变化,从而保持原有的数据不变。这种思想再redux中的reducer中也得到了体现,我们对所传入的数据不是直接进行更改,而是使用诸如object.assign()、扩展符、元数组等方法产生一个新的状态,从而来对store树进行改变。此外Immutable也很出名~
(数据的不可变在TypeScript中也得到了很好的支持(至少是在类型方面),此外ES6所提供的const也是支持变量的常量化,所以我认为数据不可变应该在往后也会是越发推广的一种思想吧。)