1、什么是函数式编程
函数式编程的基本理念是只关注做什么而不是怎么做,即把函数提升到第一等的地位为核心来组织代码,即函数可以出现在任何地方,比如你可以把函数作为参数传递给另一个函数,不仅如此你还可以将函数作为返回值。
2、纯函数
上面的描述仅仅只是只是简单的概括了一下函数式编程,想要更加深入明了还需要明白一些概念:纯函数、高阶函数、函数的柯里化、组合函数。
首先说一下什么叫纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用。举以下两两个例子就明白了
// 非纯函数,函数的输入与输出的对应并不是固定的,会受到外面常量NUM的影响
const NUM = 2;
const getResult = n => NUM * n;
// 纯函数,输入与输出永远是对应的,不管外界如何变化,并不会改变其对应的结果。
const getResult = n => n * 2;
通过以上的比较,不难得出结论:一般尽可能的使用纯函数来保持函数本身不被外部所影响,从保持代码的可维护性与可迭代性。
3、高阶函数
仅仅通过上面的纯函数还远远不能体现函数式编程范式的优点,因为那只是基本要求而已,接下来的高阶函数才是打开函数式编程范式的大门。
什么叫高阶函数:当一个函数使用函数作为参数或返回值是一个函数的时候,这个函数就称为高阶函数。
还是以上面的那个案例来说,当你不确定到底是传过来的参数是2还是多少的时候,然后又不希望被外部的变量和常量影响的话,那可以使用高阶函数的形式封装它。
// 高阶函数方式
const fn = num => {
return (n) => num * n;
}
const num2 = fn(2); // 返回一个*2的方法
const num3 = fn(3); //返回一个*3的方法
const res1 = num2(100);
const res2 = num2(200);
const res3 = num3(300);
console.log(res1, res2, res3); // 结果为:200 400 900
仔细看这个代码就会发现我们使用一个高阶函数的形式将2或3这个这里的处理封装返回了一个可复用的函数。
同样还有使用函数作为参数的高阶函数,如:
// 封装一个遍历操作数组每项的方法
const arr = [1, 2, 3 ,4 ,5 ,6];
const myForEach = (ctx, fn) => {
for(let i = 0; i < ctx.length; i++) {
fn(i);
}
}
console.log(myForEach(arr, item => console.log(item) )); // 1 2 3 4 5 6
console.log(myForEach(arr, item => console.log(item * 2) )); 2, 4, 6, 8, 10, 12
以上封装了一个myForEach的高阶函数,使得遍历数组和操作数组直接在形式上进行分开,让使用者只要把关注点放在“我需要输出数组的每一项或每一项*2之后再输出”,至于遍历这个操作已经被封装了,已经不关它的事了。像数组的map、forEach、reduce、filter等方法这些也是属于高阶函数,也是按照这种思路封装好的原生方法。
所以可以大概总结一下高阶函数的用法:
1、将本身不确定的因素抽离通过传参的形式返回一个可复用的符合当前因素的方法
2、将做怎么做这一部分代码抽离封装到一个可复用的函数中,将要做什么这一部分作为实参函数传递到该函数中,使得程序员只需要关注我要做什么,直接省去了程序员怎么做这一部分的工作与逻辑。
4、函数的柯里化
通过了解上面高阶函数之后就会发现一个很重要的思路那就是抽离,那可不可以也将函数的多个参数进行抽离分开使用使得在特定的时候使用参数更加灵活多变,如让一个求和函数进行处理让add(1, 2, 3)的结果和add(1)(2)(3)的运行结果是一样的呢。
这就得引入另一个概念了:函数的柯里化,是把接受多个参数的函数变换成接受一个单一参数的函数,其实就是一个颗粒化的过程。
就以上面提到的求和方法为例想要把add(1, 2, 3)的每个参数都单独抽离出来值参,直接使用上面高阶函数的思路先实现一个最简单的求和方法的柯里化。
const add = a => {
return function (b) {
return function(c) {
return a + b + c;
}
}
}
console.log( add(1)(2)(3) ); // 输出为:6
以上这个方法就实现将求和方法的每个参数都单独抽离出来,但是这个例子只是为了更好的理解柯里化,并不推荐这么写,因为这个写法即不灵活也不优雅。所以我们可以在这个基础上改造一下。
const add = (...args) => {
// 创建一个数组用来存放所有的参数
const _args = [...args];
const _add = function() {
_args.push(...arguments);
return _add;
}
// 重写toString方法
_add.toString = function() {
return _args.reduce((count, cur) => count + cur, 0);
}
return _add;
}
const res1 = add(1)(2)(3);
const res2 = add(1, 2)(3);
const res3 = add(1, 2, 3);
console.log(res1 + ''); // 使用字符串拼接的方式,使其触发toString,来获取计算的结果。
console.log(res2 + '');
console.log(res3 + '');
通过这个改造之后使用起来就灵活多了,可传一个,可同时传多个,就是最后需要转换成字符串触发最后的计算。 但是这个还是有缺陷,这个的求和是灵活了但是也仅仅只限于求和了,功能都限制死了复用性不高。所以还得改造一个通用的柯里化处理方法出来。
// 待柯里化的函数:多个数的求和方法
const _add = (...args) => args.reduce((count, item) => count + item, 0);
// // 柯里化的处理方法
const currying = fn => {
return function curryFn(...args) {
return function() {
// 判断当前参数的个数,如果有参数则继续柯里化
if(arguments.length) {
return curryFn(...args, ...arguments);
} else {
// 如果没有参数则开始计算
return fn(...args);
}
}
}
}
const add = currying(_add); // 返回一个被柯里化的求和方法
console.log(add(1)(2)(3)()); // 输出结果:6
console.log(add(1, 2)(3)()); // 输出结果:6
console.log(add(1, 2, 3)()); // 输出结果:6
这样通过一个公共的柯里化的处理方法,可以将我们所需要的各种功能柯里化一个可复用的方法,同时还将柯里化过程进行抽离出来让使用者只关心自己的功能逻辑。
5、组合函数
当我们的功能越来越复杂的时候,往往需要抽离出许多方法,使得这些方法依赖上一个方法的运行结果。但是这些方法一多写起来也起来也不太优雅。用一个简单的例子:判断一串字符中所有数字字符的和是否为一个偶数,那按我们函数式的思路就可以抽离出三个方法:获取所有数字、求和、判断是否为偶数
const str = "dsfsj34jljlksdjf45df3534";
// 返回所有的数字,并转换成数字
const getNums = str => str.match(/\d/g).map(item => Number(item));
// 对所有数字求和
const getSum = arrNums => arrNums.reduce((count, item) => count + item, 0);
// 判断是否为偶数
const isEven = n => n %2 === 0;
// 1、这么写觉得麻烦
const arrNumbs = getSum(str);
const sum = getSum(arrNumbs);
const even = isEven(sum);
// 2、这样写又不便于理解与维护
isEven(getSum(getNums(str)))
在这种情况下,我们可以创建一个通用的方法将这些函数组合起来去执行,然后将最后一步的计算结果返回出来,同样可以使得代码比较简练还便于理解维护。
// 创建一个组合函数执行的通用方法:按从左到右的顺序
const compose = (...fns) => {
return function(x) {
return fns.reduce((res, fn) => fn(res), x);
}
}
const str = "dsfsj34jljlksdjf45df3534";
// 返回所有的数字,并转换成数字
const getNums = str => str.match(/\d/g).map(item => Number(item));
// 对所有数字求和
const getSum = arrNums => arrNums.reduce((count, item) => count + item, 0);
// 判断是否为偶数
const isEven = n => n %2 === 0 ? "偶数" : "非偶数";
// 生成一个从左到右依次执行 getNums getSum isEven的方法
const evenFn = compose(
getNums,
getSum,
isEven
);
console.log( evenFn(str) );
总结
随着项目的功能越来越多,封装的公共基础方法也越来越多,这时候函数式编程范式的优势就开始体现出来了,通过上面的方法可以将项目中的功能代码进行各种抽离优化,不止让代码变得更加简洁、优雅还能大大加强项目的后期可迭代性与可维护性。
以上这些只是一些基本的,未完待续……