函数式编程
- 函数式编程是一种编程范式,它强调函数的纯函数性质以及函数的组合性质。在函数式编程中,函数被视为一等公民,它可以像值一样被传递和操作。
- 函数式编程的核心思想是避免副作用和共享状态,这些特性常常导致代码的不确定性和难以调试。为了实现这个目标,函数式编程中的函数应该尽可能地使用纯函数,即输入相同,输出也应该相同,并且不会对外部状态造成影响。
- 函数式编程中的另一个重要概念是组合函数。组合函数指的是将多个函数组合成一个新的函数。这种方式可以简化代码的复杂度,并且更容易实现代码复用
- 在 JavaScript 中,函数式编程可以通过使用高阶函数、闭包、柯里化、组合函数等技术来实现。下面是一个使用高阶函数和闭包来实现函数柯里化的示例
function add(x) {
return function(y) {
return x + y;
};
}
const addFive = add(5);
console.log(addFive(3)); // 输出 8
可以看到通过使用高阶函数和闭包,我们可以轻松地创建一个新的函数,从而实现了函数柯里化。
纯函数
在函数式编程中,纯函数是一种函数,它的输出只依赖于输入,而不依赖于任何外部状态或副作用。
- 在react开发中也纯函数是被多次提及的,比如
react
中组件就被要求像是一个纯函数(为什么是像,因为还有class
组件),redux
中有一个reducer的概念,也要求必须是一个纯函数, 所以掌握纯函数对于理解很多框架设计也很有帮助
纯函数的规范
- 给定相同的输入,总是返回相同的输出
- 不会修改任何传入的参数或全局变量
- 它不会产生任何副作用,如读写文件、发送HTTP请求或修改DOM
- 举个例子该函数就是一个纯函数
function add(a, b) {
return a + b;
}
- 下面这个函数输出值受外部变量的影响,甚至改变了外部变量,不是一个纯函数
let sum = 0;
function add(a) {
sum += a;
return sum;
}
- 数组原型上的
slice
方法不会修改原数组只是将截取的新数组返回,而splice
方法会返回新的数组的同时还会改变原数组,所以slice
也算是一个纯函数,而splice
不是纯函数
简单总结:纯函数在确定的输入下会产生确定的输出,且函数在执行过程中不能产生副作用
副作用是引入医学的概念,形容药物除了本来的效果外还产生了其他负面的影响。在计算机科学中表示在执行一个函数时,除返回值之外,还对调用函数产生了附加的影响(修改了全局变量,修改参数或者改变外部的存储等)。随着程序的迭代,副作用会增加程序出现出现bug的频率
柯里化
柯里化是一种将接受多个参数的函数,变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下参数,而且返回结果的新函数的函数式编程的技术 以下就是函数柯里化的例子
function sum(z, y, z) {
return x + y + z;
}
console.log(sum(10)(20)(30)); //60
//柯里化后:
function sum1(x) {
return function (y) {
return function (z) {
return x + y + z;
};
};
}
console.log(sum1(10)(20)(30)); //60
//优化后:
const sum2 = (x) => (y) => (z) => x + y + z;
console.log(sum2(10)(20)(30)); //60
为什么需要柯里化
在函数中,我们更希望一个函数处理的问题尽可能的单一,而不是将大量处理过程交给一个函数处理,不方便维护扩展。 那么可以将每次传入的参数在单一的函数中进行处理,处理完之后在下一个函数中再使用处理后的结果,可以认为是将复杂的程序拆分出来成一个个独立的函数,降低复杂度的同时增加了可维护性 对于第一个例子,如果每一个环节多出了一些处理逻辑,函数将变为:
function sum1(x) {
x = x + 2;
return function (y) {
y = y * 2;
return function (z) {
z = z * 3;
return x + y + z;
};
};
}
可以看到每个环节都负责了一些单一职责,而且都很清晰独立,然后再将第一个环节抽出来固定使用
const add4 = sum1(4)
//此时调用更加便捷
const temp = add4(5)(6)
拿输出日志来简单举例
//普通写法
const log = (date, type, text) => {
console[type](`[${date.getHours()}:${date.getMinutes()}]-[${text}]`);
};
log(new Date(), "warn", "找不到该XX方法"); //[23:50]-[找不到该XX方法]
//柯里化后
const log1 = (date) => (type) => (text) => {
console[type](`[${date.getHours()}:${date.getMinutes()}]-[${text}]`);
};
// 若时间固定
const logNow = log1(new Date())
logNow('log','输出内容')
// 若时间类型都固定,定制输出警告的方法
const logNowWarn = logNow("warn");
logNowWarn(
"Cross-Origin Read Blocking (CORB) 已屏蔽 MIME 类型为 application/json 的跨域响应 <URL> <URL></URL>"
); //[23:33]-[Cross-Origin Read Blocking (CORB) 已屏蔽 MIME 类型为 application/json 的跨域响应 <URL> <URL></URL>]
// 定义输出错误的方法 ...
可以看到柯里化之后方便了逻辑的复用,使用上也更加自由,以上只是柯里化概念介绍,真正的柯里化需要实现以下这样的效果:
add(1)(2)(3)(4)(5)(6); // => 21
add(1, 2)(3, 4)(5, 6); // => 21
add(1, 2, 3, 4, 5, 6); // => 21
可以看到在参数个数、调用次数不确定的情况下,函数依然返回了想要的结果,接下来试着实现这样的效果
实现自动柯里化函数
//定义一个函数,传入的参数为需要实现柯里化的函数
function myCurrying(fn) {
// 第一次接收到的参数args
function curried(...args) {
// 对比fn函数应接收的参数和第一次接收到的参数是否一致,使用fn.length获取fn函数应接收的参数个数
// 若参数数量一致,表示参数已全部接收完成
if (args.length >= fn.length) {
// 调用fn函数
// 考虑到存在显式绑定this调用该方法的情况这里使用apply确保this指向
// 并将函数执行的结果返回
return fn.apply(this, args);
} else {
// 如果是分次调用,这里再创建一个函数用于后续接收参数
function curried2(...args2) {
// 使用递归的形式,汇总目前已接收的参数,传递给curried判断
return curried.apply(this, [...args, ...args2]);
}
return curried2;
}
}
return curried;
}
验证下效果
const add = function (a, b, c) {
return a + b + c;
};
const curryAdd = myCurrying(add);
console.log(curryAdd(1, 2, 3)); // 3
console.log(curryAdd(1)(2)(3)); // 3
console.log(curryAdd(1)(2, 3)); // 3
tips:
Vue3
中渲染器模块的createAppApi
函数也使用了函数柯里化增强程序的拓展性,实现可定制化的渲染,感兴趣的朋友可以移步去看看[github.com/vuejs/core/…]rudux
中的createThunkMiddleware
函数也使用了该技术 [github.com/reduxjs/red…]
组合函数
组合函数允许将多个函数组合成一个新的函数。
组合函数的实现
下面我们先列举一个普通函数:
const double = function (num) {
return num * 2;
};
const square = function (num) {
return num ** 2;
};
console.log(square(double(3))) // 6
在 JavaScript 中,我们可以使用使用高阶函数和函数柯里化来实现一个简单的组合函数:
const composeFn = function (m, n) {
return function (num) {
return n(m(num));
};
};
const newFn = composeFn(double, square);
console.log(newFn(3)); // 36
- 以上组合函数只能传入两个函数组合执行,现实情况可能有多个需要组合,这里手动封装一个可接收多个需要组合执行的组合函数方法
// 定义一个函数,参数为需要组合执行的函数fns
function myCompose(...fns) {
// 判断fns每一项是否都为函数
const length = fns.length;
for (let i = 0; i < length; i++) {
// 获取传入的每一项函数,判断函数类型
if (typeof fns[i] !== "function") {
// 如果入参存在非函数抛出类型错误
throw new TypeError("Expected arguments is functions");
}
}
//开始执行函数
function compose(...args) {
//定义目前执行函数的索引
let index = 0;
// 将参数传递给第一个函数执行,并返回第一次执行的结果
let result = length ? fns[index].apply(this, args) : args;
// 根据fns的函数个数,定位下一个需要执行的函数,将结果作为参数下一个函数的参数,循环执行
while (++index < length) {
// 拿到新的执行结果覆盖旧的执行结果
result = fns[index].call(this, result);
}
// 将最后的执行结果返回
return result;
}
return compose;
}
老规矩校验下:
const double = function (num) {
return num * 2;
};
const square = function (num) {
return num ** 2;
};
const add = function(num){
return num + 10
}
const newFun = myCompose(double, square, add);
console.log(newFun(3)); // 37