函数式编程 是一种编程范式,它强调将计算过程视为数学函数的求值,注重函数的组合、避免副作用和使用不可变数据。
函数式编程的一些优点:
- 可组合性:函数可以轻松地组合成更复杂的函数,使得代码更易于理解和维护。例如,可以将多个简单的函数通过管道或组合操作符连接起来,形成一个功能更强大的函数。
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
const complexOperation = (x, y) => multiply(add(x, y), 2);
-
可测试性:由于函数通常是纯函数,对于相同的输入总是产生相同的输出,并且没有副作用(说人话就是,不会改变传入的引用类型的数据源),使得测试变得更加简单和可靠。
- 比如,对于一个简单的加法函数
const add = (a, b) => a + b;,只需要给定输入参数,就能准确预测输出结果,方便进行单元测试。
- 比如,对于一个简单的加法函数
-
可读性:函数式编程的代码通常更简洁、更具声明性,能够清晰地表达计算的意图。
像使用map、filter和reduce等函数来处理数组,比使用循环和条件语句更具可读性。
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0);
-
避免副作用:减少了由于共享状态和可变数据导致的难以预测的错误。这有助于提高程序的稳定性和可靠性。
- 例如,一个纯函数不会修改全局变量或外部数据结构。
-
并行和并发编程:由于函数的确定性和无副作用等特性,使得在并行和并发环境中更容易进行任务的分配和协调。
-
代码复用性:纯函数可以在不同的上下文中被重复使用,而无需担心外部状态的影响。
-
易于推理和理解:函数的输出只取决于输入,使得代码的逻辑更容易被理解和推导。
函数式编程提供了一种优雅、可靠和高效的方式来编写代码,尤其在处理复杂的业务逻辑和大规模的代码库时,能够显著提高开发效率和代码质量(逻辑一目了然)。
函数组合
函数组合 是函数式编程中的一个重要概念。 如果说函数式编程,是一种规范,那么函数组合,应该可以称之为实践方式
函数组合 指的是将多个函数按照一定的顺序连接起来,使得前一个函数的输出成为后一个函数的输入,从而构建出更复杂的功能。
实际场景
假设现在我们有这样一个需求:给到一个全小写字母的字符串,将这个字符串首位转化成大写,然后倒序。
使用命令式(脚本式)编程的方式写完了,感觉一切都很完美:
//单行实现
function oneLine(str){
const res = (str.charAt(0).toUpperCase() + str.slice(1)).split('').reverse().join('');
return res;
}
//多行实现
function multiLine(str){
const firstUpperStr = str.charAt(0).toUpperCase() + str.slice(1);
const reverseStr = firstUpperStr.split('').reverse().join(''):
return reverseStr;
}
const str = 'function program';
console.log(oneLine(str)); //margorp noitcnuF
console.log(multiLine(str)); //margorp noitcnuF
产品下午要改需求,不要倒序了,但字符串依旧首位转化成大写,于是临时咔咔的注释代码,稍微修改一下,感觉工作量也还可接受:
//单行实现
function oneLine(str){
const res = str.charAt(0).toUpperCase() + str.slice(1)//.split('').reverse().join('');
return res;
}
//多行实现
function multiLine(str){
const firstUpperStr = str.charAt(0).toUpperCase() + str.slice(1);
//const reverseStr = firstUpperStr.split('').reverse().join(''):
return firstUpperStr;
}
const str = 'function program';
console.log(oneLine(str)); //Function program
console.log(multiLine(str)); //Function program
第二天,产品跟需求方沟通后,又决定全部字母要转大写,并且还是要倒序...
虽然很让人崩溃,但我相信一般开发遇上这种情况的不在少数,而真实的开发场景,逻辑只会更加复杂;
函数式编程应用
拿到需求,尝试把每个小需求拆分,独立封装成一个函数,然后使用通过一个compose函数,生成一个从右往左依次执行的新函数,以组合的方式,实现我们的需求;
compose函数实现
function compose(...args){
return function(res){
return args.reduceRight((res,fn)=>{
return fn(res)
},res);
}
}
回到最开始的一版需求,我们使用函数式编程的方式去开发,把需求拆分为小需求,并封装为独立函数,最后使用compose函数生成一个新的函数,在需要执行的时候执行,最终拿到结果
const str = 'function program'
function firstStringToUpper(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
function stringReverse(str) {
return str.split('').reverse().join('')
}
const getRes = compose(stringReverse, firstStringToUpper)
const res = getRes(str);//margorp noitcnuF
第二版需求:不要倒序了,但字符串依旧首位转化成大写
const str = 'function program'
function firstStringToUpper(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
function stringReverse(str) {
return str.split('').reverse().join('')
}
//const getRes = compose(stringReverse, firstStringToUpper)
const getRes = compose(firstStringToUpper)
const res = getRes(str);//Function program
第三版需求:全部字母要转大写,并且还是要倒序
const str = 'function program'
function firstStringToUpper(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
function stringToUpper(str) {
return str.toUpperCase()
}
function stringReverse(str) {
return str.split('').reverse().join('')
}
//const getRes = compose(stringReverse, firstStringToUpper)
//const getRes = compose(firstStringToUpper)
const getRes = compose(stringReverse, stringToUpper)
const res = getRes(str);//MARGORP NOITCNUF
至此,第三版的需求变更完成,函数式编程的方式的改动,仅限于新增了一个stringToUpper函数,以及变更了compose函数的入参
函数柯里化
函数柯里化(Currying) 是函数式编程中常用的的一种技术。
它指的是将一个接受多个参数的函数,转化为一系列接受单个参数的函数。
例如,原本有一个函数 f(x, y) ,通过柯里化可以将其转换为一个新的函数 g(x) ,这个 g(x) 会返回一个新的函数 h(y) ,当 h(y) 被调用时,才会真正执行 f(x, y) 的逻辑并返回结果。
函数柯里化的主要优点包括:
- 参数复用:可以固定某些参数的值,然后创建一个新的函数来处理剩余的参数。例如,如果有一个函数
multiply(a, b)用于计算两个数的乘积,通过柯里化可以创建一个新函数double = multiply(2),用于计算某个数的两倍。 - 提高代码的灵活性和可组合性:柯里化后的函数更容易与其他函数进行组合和链式调用。
- 延迟计算:可以根据需要逐步传递参数,控制函数的执行时机。
在实际应用中,函数柯里化常用于创建更具通用性和可复用性的函数,比如在一些框架或库中用于处理事件监听、数据处理等场景。
例如,在处理不同类型的事件时,可以先柯里化一个事件处理函数,固定部分参数(如事件类型),然后在具体的使用场景中传递其他参数(如处理逻辑)。
又如,在处理数据的过滤和转换时,可以柯里化相关函数,先固定一些常见的条件或转换规则,然后根据具体的数据再传递特定的参数。
实际场景
光看理论可能比较难以理解,我们以业务举例,假设场景过了个周末,又想改需求了...
需求是检测经过处理后的字符串中,是否存在某个字符串,存在结果是yes,否则是no;
面向过程的开发模式就不举例了,我们尝试使用函数式编程的方式开发
咔咔两下就新增了两个新函数,但是好像卡壳了,因为find函数好像不知道如何传入,有两个参数
const str = 'function program'
function firstStringToUpper(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
function stringToUpper(str) {
return str.toUpperCase()
}
function stringReverse(str) {
return str.split('').reverse().join('')
}
//新增的检索匹配函数
function find(targetStr,str) {
return str.includes(targetStr)
}
//新增的判断函数
function judge(is) {
return is ? 'yes' : 'no'
}
//const getRes = compose(stringReverse, firstStringToUpper)
//const getRes = compose(stringToUpper)
const getRes = compose(stringReverse, stringToUpper)
const res = getRes(str);//MARGORP NOITCNUF
这就到柯里化发挥作用的时候了,我们尝试把find函数做柯里化
const str = 'function program'
function firstStringToUpper(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
function stringToUpper(str) {
return str.toUpperCase()
}
function stringReverse(str) {
return str.split('').reverse().join('')
}
//新增的检索匹配函数
function find(targetStr,str) {
return str.includes(targetStr)
}
// 柯里化 find 函数
function findCurry(targetStr) {
return str => str.includes(targetStr)
}
const findIT = findCurry('IT');
//新增的判断函数
function judge(is) {
return is ? 'yes' : 'no'
}
const getRes = compose(judge,findIT,stringReverse, stringToUpper)
const res = getRes(str);//MARGORP NOITCNUF -> yes
至此,使用函数式编程的第四版需求完成,我们的改动,还是仅限于增加了两三个函数,变更了compose函数的入参,且getRes函数的逻辑线路非常清晰(从右往左);
以下是一个简单的 JavaScript 函数通用柯里化的示例和demo:
function curry(fn,...args) {
if (args.length >= fn.length) {//fn.length指的是fn函数接收的参数数量
return fn(...args);
} else {
return function (...nextArgs) {
return curry(fn,...args,...nextArgs);
};
}
}
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = curry(add);
const add5 = curriedAdd(5);
const result = add5(3, 2);
//const result = add5(3)(2);//也可以
function find(str, targetStr) {
return str.includes(targetStr)
}
const findIT = curry(find)('IT');
findIt('IT');//true
findIt('TI');//false