1.什么是函数式编程
函数式编程是一种思维方式,强调在编程过程中把更多的关注点放在如何去构建映射关系。
函数式编程和命令式编程区别:函数式编程关心数据的映射,命令式编程关心解决问题的步骤。
下面举个例子展示下两者区别:
假设我们需要进行如下结构转换:
['john-rose', ‘linda-luo ', ‘lucy-han']
// 转成
[{name: 'John Rose'}, {name: Linda Luo'}, {name: Lucy Han'}]
命令式编程
思路:
/* 1.定义一个临时变量 newArr。
2.做一个循环,需要做 arr.length 次。
3.循环每次把名字的首位取出来大写,然后拼接剩下的部分。
……
4.最后返回结果。*/
//代码实现
const arr = ['john-rose', 'linda-luo', 'lucy-han'];
const newArr = [];
for (let i = 0, len = arr.length; i < len ; i++) {
let name = arr[i];
let names = name.split('-');
let newName = [];
for (let j = 0, naemLen = names.length; j < naemLen; j++) {
let nameItem = names[j][0].toUpperCase() + names[j].slice(1);
newName.push(nameItem);
}
newArr.push({ name : newName.join(' ') });
}
return newArr;
缺点:产生一堆中间临时变量,同时过程中掺杂了大量逻辑,可读性差、不易维护。通常一个函数需要从头读到尾才知道它具体做了什么,而且一旦出问题很难定位。
函数式编程
思路:
/*
1.将string数组转化为对象数组 [a, b, c] -> [{name: a’}, {name: b’}, {name: c’}]
2.将单个string转化为单个object( convert2Obj ) String -> Object a -> {name: a’}
a.将string转为为指定的string( capitalizeName ) String -> String a –> a’
b.将string转化为object( genObj ) String -> Object a' -> {name: a’}
*/
// 代码实现
import * as R from 'ramda'
const {curry, compose, join, map, split} = R
const capitalize = x => x[0].toUpperCase() + x.slice(1).toLowerCase();
const genObj = curry((key, x) => {
let obj = {};
obj[key] = x;
return obj;
})
const capitalizeName = compose(join(' '), map(capitalize), split('-'));
const convert2Obj = compose(genObj('name'), capitalizeName)
const convertName = map(convert2Obj);
convertName(['john-rose', 'linda-luo', 'lucy-han'])
特点: 着眼点是函数,而不是过程。强调的是如何通过函数的组合变换去解决问题
2. 函数式编程的特点
- 函数是“一等公民”
这是函数式编程得以实现的前提,因为我们基本的操作都是在操作函数。这个特性意味着函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
- 声明式编程
函数式编程大多时候都是在声明我需要做什么,而非怎么去做。这种编程风格称为声明式编程 。这样有个好处是代码的可读性特别高,同时也方便我们进行分工协作。
- 函数惰性执行
所谓惰性执行指的是函数只在需要的时候执行,即不产生无意义的中间变量。函数式编程跟命令式编程最大的区别就在于几乎没有中间变量,它从头到尾都在写函数。
- 无状态和数据不可变
数据不可变: 它要求你所有的数据都是不可变的,这意味着如果你想修改一个对象,那你应该创建一个新的对象用来修改,而不是修改已有的对象。
无状态: 主要是强调对于一个函数,不管你何时运行,它都应该像第一次运行一样,给定相同的输入,给出相同的输出,完全不依赖外部状态的变化。
- 没有副作用
副作用指:在完成函数主要功能之外完成的其他副要功能。在我们函数中最主要的功能当然是根据输入返回结果,而在函数中我们最常见的副作用就是随意操纵外部变量, 如修改全局变量,修改入参等。
- 纯函数
不依赖外部状态(无状态): 函数的的运行结果不依赖全局变量,this 指针,IO 操作等。
没有副作用(数据不变): 不修改全局变量,不修改入参。
所以纯函数才是真正意义上的 “函数”, 它意味着相同的输入,永远会得到相同的输出。
纯函数的意义:
a. 便于测试和优化 b. 可缓存性 c. 更少的 Bug
3.流水线的构建(柯里化、函数组合)
函数式编程更为关注构建关系,数据可以不断的从一个函数的输出可以流入另一个函数输入,最后再输出结果。我们可以把上面函数式编程的过程抽象为流水线的构建,有两种操作是必不可少的那无疑就是柯里化(Currying)和函数组合(Compose),柯里化其实就是流水线上的加工站,函数组合就是我们的流水线,它由多个加工站组成。
接下来,就让我们看看柯里化(Currying)和函数组合(Compose)。
加工站-函数柯里化
柯里化的意思是将一个多元函数,转换成一个依次调用的单元函数。
f(a,b,c) → f(a)(b)(c)
为什么这个单元函数很重要?因为函数的返回值,有且只有一个, 如果我们想顺利的组装流水线,那我就必须保证我每个加工站的输出刚好能流向下个工作站的输入。因此,在流水线上的加工站必须都是单元函数。
- 部分函数应用 vs 柯里化
// 柯里化
f(a,b,c) → f(a)(b)(c)
// 部分函数调用
f(a,b,c) → f(a)(b,c) 、 f(a,b)(c)
部分函数( Partial Function Application )应用强调的是固定一定的参数,返回一个更小元的函数。
柯里化强调的是生成单元函数,部分函数应用的强调的固定任意元参数,而我们平时生活中常用的其实是部分函数应用,这样的好处是可以固定参数,降低函数通用性,提高函数的适合用性。
- 高级柯里化
现成的函数库(如Ramda)提供的curry 函数,做了优化,不是纯粹的柯里化,可理解成高级柯里化。
实现可以根据你输入的参数个数,返回一个柯里化函数/结果值。即,如果你给的参数个数满足了函数条件,则返回值。
我们可以用高级柯里化去实现部分函数应用,但是柯里化不等于部分函数应用。
//代码演示
//简易实现
const curry = function(func) {
return function curried(...args) {
if (args.length < func.length) {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
return func.apply(this, args);
}
}
const add = R.curry((x, y, z) => x + y + z);
const add7 = add(7);
add7(1,2) // 10
const add1_2 = add(1,2);
add1_2(7) // 10
add(7)(1)(2) // 10
流水线-函数组合
函数组合的目的是将多个函数组合成一个函数。下面来看一个简化版的实现:
const compose = (...fns) =>
(...args) =>
fns.reduceRight((val, fn) => fn.apply(null, [].concat(val)), args);
const f = x => x + 1;
const g = x => x * 2;
const t = (x, y) => x + y;
let fgt = compose(f, g, t);
fgt(1, 2); // 3 -> 6 -> 7
函数组合的好处
函数组合的好处显而易见,它让代码变得简单而富有可读性,同时通过不同的组合方式,我们可以轻易组合出其他常用函数,让我们的代码更具表现力。
大型的程序,都可以通过一步步的拆分组合去实现,而剩下要做的,就是去构造足够多的积木块(函数)。
4.总结
优点:
- 代码简洁,开发快速:
函数式编程大量使用函数的组合,函数的复用率很高,减少了代码的重复,因此程序比较短,开发速度较快。
- 接近自然语言,易于理解:
函数式编程大量使用声明式代码,基本都是接近自然语言的,加上它没有乱七八糟的循环,判断的嵌套,因此特别易于理解。
- 易于并发编程:
函数式编程没有副作用,所以函数式编程不需要考虑“死锁”(Deadlock),所以根本不存在“锁”线程的问题。
- 更少的出错概率:
因为每个函数都很小,而且相同输入永远可以得到相同的输出,因此测试很简单,同时函数式编程强调使用纯函数,没有副作用,因此也很少出现奇怪的 Bug。
缺点:
- 性能:
函数式编程往往会对一个方法进行过度包装,从而产生上下文切换的性能开销。
- 资源占用:
在 JS 中为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式。这在某些场合会产生十分严重的问题。
- 递归陷阱:
在函数式编程中,为了实现迭代,通常会采用递归操作。
总结:
因此,在性能要求很严格的场合,函数式编程其实并不是太合适的选择。
我们完全可以在日常工作中将函数式编程作为一种辅助手段,在条件允许的前提下,借鉴函数式编程中的思路,例如:多使用纯函数减少副作用的影响;使用柯里化增加函数适用率;使用它的编程风格,减少无意义的中间变量,让代码更具可读性。