函数式编程是React的精髓,在正式讲解React之前,有必要先了解一下函数式编程,有助于更好的理解React的特点。函数式编程(Functional Programming)不是一种新的框架或工具,而是一种以函数为主的编程范式。编程范式也叫编程范型,是一类编程风格,除了函数式编程,常用的还有面向对象编程、命令式编程等。
一、声明式编程
声明时编程也是一种范式,但它是一个比较大的概念,函数式编程是它的一个子集。声明式编程能指定每一步操作,而不用向计算机描述具体的实现细节。与之相对立的是命令式编程,它会命令计算机每一步该怎么做。以数组的元素翻倍为例,先用命令式编程实现,如下所示。
var arr = [1, 2, 3], length = arr.length, doubles = []; for (let i = 0; i < length; i++) { doubles.push(arr[i] * 2); }
在命令式的代码中,先用for循环遍历整个数组,然后让每个元素乘以二,再将计算结果插入到doubles数组中,直至将所有的元素计算完才终止整套操作。改用声明式编程可以像下面这样实现相同的功能。
var doubles = [1, 2, 3].map(value => value * 2);
在声明式的代码中,用map()方法替代了循环语句(即不指明流程的控制方式),既不用再维护计数器,也不用再通过索引访问数组的元素,配合ES6的箭头函数让整套操作变得非常简洁。
除了这些表面区别之外,还有个最本质的区别,那就是声明式编程会避免用变量保存程序的状态,从而能提高代码的无状态性。在命令式的代码中,每次迭代都会修改doubles变量,这是个状态变量,而在声明式的代码中,改用返回值保存程序的状态。
二、函数优先
函数式编程强调在程序中使用函数。由于JavaScript中的函数是一等公民,它既可以是变量的值,也可以作为另一函数的参数或返回值,因此通过函数可构建一层抽象以替代流程控制或解决复杂的逻辑操作。例如对数组中的数字进行排序和过滤,可以像下面这样运用函数式编程的思想实现。
[4, 1, 5, 2, 3].sort((a, b) => a > b).filter(value => value > 2); //[3, 4, 5]
函数式编程旨在将复杂的运算分解成一系列嵌套的函数,逐层推导,不断渐进,直至完成运算。
三、纯函数
纯函数(Pure Function)是一种没有副作用、引用透明的函数,它是函数式编程的基本概念,接下来会重点讲解它的三个特征。
1)函数的副作用
函数在读写外部资源或执行不确定的操作时就会产生副作用,例如修改函数外的变量、调用Date.now()或Math.random()、更新cookie信息等。副作用不仅会降低程序整体的可读性,有时候还会带来意料之外、难以排查的错误,下面是一个副作用的例子。
var digit = 1; function increment() { digit += Math.random(); return digit; }
在上面的代码中,increment()函数产生了副作用,因为每次调用它都会更新外部的digit变量,并且每次得到的计算结果也无法预知。
2)引用透明
如果传递给函数相同的参数,始终能得到相同的结果,那么就能说这个函数是引用透明(Referential Transparent)的。简单的说就是,函数的运行只受其输入值的影响,如下代码所示,传递给add()函数固定的参数会返回固定的值。
function add(a, b) { return a + b; }
3)参数值不可变
传递给纯函数的参数值是不允许在内部将其改变的,换句话说,在函数内部使用的是参数值的副本。如果参数值是基本类型的,那么传递给函数的就是其副本;但如果参数值是对象类型的,那么需要注意,传递给函数的是引用对象的指针。
下面用一个示例说明,addDigit()函数的参数是一个数组,它的功能是为数组的每个元素加一,在执行addDigit(digits)之后,由于digits变量是一个数组,因此它的元素会随着函数的调用而被改变。
var digits = [1, 2, 3]; function addDigit(arr) { for (let i = 0, len = arr.length; i < len; i++) { arr[i] += 1; } return arr; } addDigit(digits); console.log(digits); //[2, 3, 4]
接下来修改addDigit()函数,使之能满足纯函数的要求,如下所示。
var digits = [1, 2, 3]; function addDigit(arr) { return arr.map(value => value + 1); } addDigit(digits); console.log(digits); //[1, 2, 3]
在addDigit()函数内部,用map()方法替代for循环,使得在不改变参数的前提下,完成元素加一的功能。
四、优点
函数式编程有许多优点,本节只列出了其中的两点。
(1)函数式编程可将复杂的任务分解成一个个既简单又独立的纯函数,有利于提高代码的模块化、复用性、预测性以及可测试性。
(2)函数式编程有很高的自由度,可以采用更符合人类思维习惯的链式写法,以此提高代码的可读性。
接下来会用两种函数式的写法操作一个数组,为了便于演示省略了函数的具体实现,首先是普通的函数式写法,如下所示。
elementDouble(filterEven(arr, filterFn), doubleFn);
两个函数都有两个参数,第一个是数组,第二个是相应的回调函数。具体的执行过程是先通过filterEven()函数过滤掉数组中偶数位置的元素,再用elementDouble()函数把每个元素翻倍,下面改成链式的写法。
filerEven(arr, filterFn).elementDouble(arr, doubleFn);
通过两段代码的对比可以看出,链式的写法更容易让人理解,代码意图也更清晰。