定义
通过一系列函数连接执行的编程逻辑。函数之间可以是互相独立,不关联具有独立性。 面向执行结果,通过小单元不断构建一个庞大的系统。
代表语言
- haskell
- scala
编程范式对比
函数式编程
优点
- 功能独立
- 扩展性好
- 按需使用
- 单一原则,复用性好
特点
- has a的关系 体现整体和部分的思想(内容都被封装在内部,外部无法知道细节)
class Reporter {
constructor(log) {
this.log = log
}
showReport() {
this.log.show("显示报表")
}
}
let reporter = new Reporter(new Logger())
reporter.showReport()
面向对象编程
把对象的所有行为和属性封装在一个类中,通过实例化执行相关逻辑。设计过于复杂交叉,面向执行过程。 特点
- 代表语言java .net
- is a 的关系,如 人是动物
- 是一个白盒,你需要了解里面所有的细节。 需要重写对应的方法
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 方法
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
// 创建对象
const person1 = new Person('Alice', 30);
person1.greet(); // 输出: Hello, my name is Alice and I am 30 years old.
//继承
class Student extends Person {
constructor(name, age, grade) {
super(name, age); // 调用父类的构造函数
this.grade = grade;
}
// 重写父类的方法
greet() {
console.log(`Hi, I'm ${this.name}, a ${this.grade} grade student, and I'm ${this.age} years old.`);
}
// 新方法
study() {
console.log(`${this.name} is studying.`);
}
}
// 创建对象
const student1 = new Student('Bob', 20, '10th');
student1.greet(); // 输出: Hi, I'm Bob, a 10th grade student, and I'm 20 years old.
student1.study(); // 输出: Bob is studying.
优点
- 子类自带父类信息
缺点
- 强耦合
- 代码复杂度高
- 扩展性差
概念
一般函数式编程都有一下特点
- 纯函数
- 不可变性(没有副作用)
- 高阶函数
- 函数组合
- 惰性求值
纯函数
相同的输入,永远都是一样的输出,并且没有副作用 例子:
- js里面Array
- reduce中的reducer就是纯函数
优点:
- 安全:无副作用
- 可测试:入参和出参都是不变
- 可缓存:入参和出参都是不变 可缓存
幂等性 与 纯函数区别
幂等性是关于重复执行操作时结果一致性的一种性质,通常应用在操作和系统设计中。
- HTTP方法:GET、PUT 和 DELETE 方法都是幂等的。例如,删除一个资源,无论你请求删除一次还是多次,最终的结果都是资源被删除。
- 数据库操作:更新一个记录到其当前状态的值也是幂等的,比如设置一个用户的激活状态为“已激活”,如果用户已经是“已激活”状态,那么这个操作不会产生任何变化。
纯函数是函数式编程中的一个概念,强调函数的确定性和无副作用,主要用于代码逻辑的实现。
function add(a, b) {
return a + b; // 纯函数
}
console.log(add(2, 3)); // 总是返回 5
副作用
函数调用时候,对函数外的变量,或函数入参做修改。
下面例子都是带有副作用
//修改外部变量
let a = 100
function test() {
a = a + 1
}
//修改入参对象的变量
let obj = {a:100}
function test(obj) {
obj.a = 200
}
//输出日志
function test(obj) {
console.log(obj)
}
// 操作dom
function test(obj) {
document.body.innerHTML = obj.a
}
//发送http请求
function test(obj) {
fetch('www.baidu.com').then(res => res.json()).then(data => {})
}
//操作客户端存储
function test(obj) {
localstorage.setItem('a',obj.a)
}
//serivce ifame worker通讯
function test(obj) {
serivce.postMessage('hello')
}
高阶函数
定义
- 以函数作为参数的函数。
- 以函数作为返回值的函数。
- 即函数作为参数同时返回一个函数。 任意一种都叫高阶函数 high-order function
Array的内置方法:
- Array.prototype.forEach
- Array.prototype.find
- Array.prototype.map
高阶组件也是高阶函数的扩展
import { h } from 'vue';
export function withExtraInfo(WrappedComponent) {
return {
props: WrappedComponent.props,
setup(props, { attrs, slots }) {
const extraInfo = "This is some extra info";
return () =>
h(WrappedComponent, {
...props,
...attrs,
content: `${props.content} - ${extraInfo}`,
});
},
};
}
函数组合compose
函数组合是指将多个函数组合在一起,形成一个新的函数。输出一个函数的结果作为下一个函数的输入。
const add = x => x + 1;
const multiply = x => x * 2;
const composedFunction = x => multiply(add(x));
console.log(composedFunction(5)); // 输出 12
//升级版 注意执行顺序是从右边到左边
const compose = (...fns) => (x) => fns.reduceRight((v, fn) => fn(v), x);
const add = (x) => x + 1;
const multiply = (x) => x * 2;
const square = (x) => x * x;
const processNumber = compose(square, multiply, add);
console.log(processNumber(2)); // 输出 36
pipe管道
跟组合类似,区别代码从左到右执行。
const pipe = (...fns) => (x) => fns.reduce((v, fn) => fn(v), x);
const add = (x) => x + 1;
const multiply = (x) => x * 2;
const square = (x) => x * x;
const processNumber = pipe(add, multiply, square); // 自左向右执行
console.log(processNumber(2)); // 输出 36
惰性求值
只有在最终实际被调用的时候才被执行,即延迟的值,过程只是定义没有真正执行。
柯里化是比较常用的实现惰性求值。
柯里化
把一个N元函数编程N个一元函数,持续返回函数,直到参数用完为止,只有最后一个被返回并执行,才会全部执行。(元:函数的参数数量)
function sum(a,b,c) {
return a+ b + c
}
function curry(a) {
return function (b) {
return function (c) {
return a+ b + c
}
}
}
console.log(curry(1)(2)(3)) // 输出5
实现柯里化的条件
- 接受需要柯里化的函数
- 存放每次函数调用的参数
- 当参数不够原来函数参数,返回一个新的函数,直到满足为止。
- 每次只能接受一个参数
优点
- 参数复用,逻辑复用
- 延迟执行/延迟计算
函数绑定 实现柯里化
function bind(f,...funcArgs) {
return function(...args) {
return f(...funcArgs,...args)
}
}
//使用
function f(a,b,c) {
return a * b + c
}
var f2 = bind(f,1,2);
console.log(f2(3))//输出 5 ,bind属于简单柯里化,延迟执行的值
升级版
var slice = Array.prototype.slice;
var curry = function(fn) {
var args = slice.call(arguments, 1); // 提取 fn 之后的所有参数,即跳过第一个参数(fn)并获取其余参数。
return _curry.apply(this, [fn, fn.length].concat(args));// 传入fn ,fn的长度, 以及当前除fn以外的实参
}
function _curry(fn, len) {
var oArgs = slice.call(arguments, 2);// 提取从第三个参数开始的所有参数。这些参数是之前已经收集的参数
return function() {
var args = oArgs.concat(slice.call(arguments));
if (args.length >= len) {
return fn.apply(this, args);
} else {
return _curry.apply(this, [fn, len].concat(args)); //递归地继续收集参数,直到参数数量足够为止。
}
}
}
//测试
function sum(a,b,c) {
return a+ b + c
}
console.log(curry(sum,1)(2)(3)) // 输出5
注意: function.length 属性:显示形参,定义时候的长度。
// 测试
function example(a, b, c) {}
console.log(example.length); // 输出 3
function exampleWithDefaults(a, b , c,d=2,e) {}
console.log(exampleWithDefaults.length); // 输出 3 ,以d前面为len (因为 d 有默认值)
function exampleWithRest(a, ...rest) {}
console.log(exampleWithRest.length); // 输出 1 (因为 ...rest 是剩余参数,不计算在内)
偏函数
固定一部分参数,产生更小颗粒度的函数。
function sum(a,b,c) {
return a+ b + c
}
let sum2 = curry(sum,1,2) // sum2就是偏函数
console.log(sum2(3)) // 输出5
反柯里化
把单个参数的柯里化打平所有参数,一次性调用
Function.prototype.unCurry = function() {
const self = this
return function() {
return Function.prototype.call.apply(self, arguments)
}
}
const push = Array.prototype.push.unCurry();
const arr= [1, 2, 3];
const obj = {};
push(obj, 4, 5, 6);
console.log (obj);
其他版本
// 其他版本
Function.prototype.unCurry = function () {
return this.call.bind (this);
}
Function.prototype.unCurry = function () {
return (...args) => this.call(...args)
}
使用场景
- 借用数组的方法
- 检查复制的数组
- 发送事件
Function.prototype.unCurry = function() {
const self = this
return function() {
return Function.prototype.call.apply(self, arguments)
}
}
const copy = Array.prototype.slice.unCurry()
let test1 = [1,2,3]
let test2 = copy(test1)
console.log(test1 === test2)
高阶概念
chain链式调用
特点:
- 返回对象本身
- 返回同类型实例
例子:
- jquery
- Array数组
- Promise的then().then()
- rxjs
- lodash
- axios 场景
- 需要多次赋值或计算
- 有特定的逻辑顺序
实现例子
类实现
class Cal {
constructor(val)
{
this.val = val;
}
double() {
this.val=this.val*2;
return this
}
add (num) {
this.val = this.val +num
return this
}
mutil (num) {
this.val=this.val*num;
return this
}
get value() {
return this.val
}
}
const cal = new Cal(10);
const val = cal.add(10).double().value;
console.log(val);
pipe/compose实现
const cal = pipe(
val => add (val, 10),
double ,
val => mutil (val, 10)
)
console.log(cal(10))
函子functor
容器: 包含值与值关系的函数(如Array里面的map)
函子定义:一种特殊的数据结构或容器,同时具有包含一系列操作数据的方法。如map
例子
class Functor {
construtor(val){
this._val = val
}
map(fn){
return new Functor(fn(this._val))
}
}
//使用
new Functor(5).map(x => x + 10) // 15
//升级版
class Functor {
static of (val) { // 通过静态初始化
return new Functor(val)
}
constructor (val) {
this._val = val
}
map (fn) {
return Functor.of(fn(this._val))
}
}
let a = Functor.of(5).map(x => x - 2).map(x => x + 5 )
console.log(a) // 8
Maybe 函子
当遇到副作用的时候,控制副作用在可控的范围内。
// 定义 Maybe 类
class Maybe {
constructor(value) {
this.value = value;
}
// 用于创建一个存在值的 Maybe (Just)
static just(value) {
return new Maybe(value);
}
// 用于创建一个空的 Maybe (Nothing)
static nothing() {
return new Maybe(null);
}
// 用于检查 Maybe 是否是空的 (Nothing)
isNothing() {
return this.value === null || this.value === undefined;
}
// 用于对值应用函数,如果 Maybe 是 Nothing,返回自身,否则返回一个新的 Maybe
map(fn) {
return this.isNothing() ? this : Maybe.just(fn(this.value));
}
// 用于从 Maybe 中获取值,如果是 Nothing 则返回默认值
getOrElse(defaultValue) {
return this.isNothing() ? defaultValue : this.value;
}
// 用于链式调用多个 Maybe 操作
chain(fn) {
return this.isNothing() ? this : fn(this.value);
}
// 用于过滤值,如果条件不满足则返回 Nothing
filter(fn) {
return this.isNothing() || fn(this.value) ? this : Maybe.nothing();
}
}
//测试
const safeDivide = (num, denom) => {
return denom === 0 ? Maybe.nothing() : Maybe.just(num / denom);
};
const result1 = safeDivide(10, 2)
.map(x => x * 2)
.getOrElse(0); // 结果: 10
const result2 = safeDivide(10, 0)
.map(x => x * 2)
.getOrElse(0); // 结果: 0 (因为分母为0,返回Nothing)
console.log(result1); // 输出: 10
console.log(result2); // 输出: 0
Either 函子
把处理可能出现的情况分为左边(失败)或右边(成功),让执行结果具有确定性,比try-catch可控
- Left:表示计算失败或包含错误信息。
- Right:表示计算成功,包含有效结果。
class Either {
constructor(value) {
this.value = value;
}
static left(value) {
return new Left(value);
}
static right(value) {
return new Right(value);
}
isLeft() {
return this instanceof Left;
}
isRight() {
return this instanceof Right;
}
// 在 Left 情况下,map 不做任何处理
// 在 Right 情况下,map 执行函数并返回新的 Right
map(fn) {
if (this.isRight()) {
return Either.right(fn(this.value));
}
return this;
}
// 用于处理两种情况
fold(f, g) {
return this.isLeft() ? f(this.value) : g(this.value);
}
}
class Left extends Either {}
class Right extends Either {}
// 使用示例
const getUserName = (user) =>
user ? Either.right(user.name) : Either.left('User not found');
const result = getUserName({ name: 'Alice' })
.map(name => name.toUpperCase())
.fold(
err => `Error: ${err}`,
name => `User name is ${name}`
);
console.log(result); // 输出: "User name is ALICE"
const result2 = getUserName(null)
.map(name => name.toUpperCase())
.fold(
err => `Error: ${err}`,
name => `User name is ${name}`
);
console.log(result2); // 输出: "Error: User not found"
IO 函子
- 函子用于处理包含副作用的计算,例如读取文件、打印日志、获取用户输入等。
- 可以把副作用的结果包裹起来在io对象上,不会立即执行。
- 延迟计算,确保在明确调用时候才通过run输出。
class IO {
constructor(effect) {
if (typeof effect !== 'function') {
throw 'IO Usage: function required';
}
this.effect = effect;
}
// 用于将函数应用到 IO 中的值
map(fn) {
return new IO(() => fn(this.effect()));
}
// 用于运行 IO 中的副作用
run() {
return this.effect();
}
}
// 使用示例
const read = () => 'Reading some data...'; // 这里返回的内容是不确定的。带有副作用
const write = (message) => `Writing: ${message}`;
// 将副作用包装在 IO 函子中
const readIO = new IO(read);
const writeIO = readIO.map(data => write(data));
// 运行副作用
console.log(writeIO.run()); // 输出: "Writing: Reading some data..."
分层架构
在函数式开发中,可以通过分层的架构,划分不同业务层级,使得代码能够更有效归类复用。
//common layer
const isIgnore = (ignore:Array<string>)=>(val:string) => ignore.includes(val);//忽略的方法
type Renderer = Console| ElMessage| OtherRender;//渲染输出的类型
const showMsg = ‹T extends Renderer> (renderer:T, funName:string) => (msg :string)=>{return (renderer as T) .render(msg) ; }// 定义实际需要使用的类型
//biz layer 业务层
const isIgnoreInValidate = isIgnore (["1", "2", "3"]);
const showMsgInValidate = showMsg(ElMessage); //1.直接传入
const showMsgInValidate2 = showMsg(Console); //1.直接传入
//practice layer 渲染层
isIgnoreInValidate("1");
showMsgInValidate("this is a warning");
showMsgInValidate2("this is a console");
案例
1.日期校验
面向过程式编写代码
<template>
<el-form ref="postRef" :model="postForm" >
<el-form-item label="月份" prop="month">
<el-date-picker v-model="postForm.month" type="month" placeholder="请选择" format="YYYYMM" />
</el-form-item>
<el-form-item label="设置日期区间" prop="range">
<el-date-picker v-model="postForm.range" type="daterange" placeholder="请选择" start-placeholder="开始日期" end-placeholder="结束日期" />
</el-form-item>
</el-form>
<el-button @click="submitForm">提交</el-button>
</template>
<script setup lang="ts">
//列表查询
import moment from 'moment'
import { reactive, toRefs} from 'vue'
const state = reactive({
postForm: {
month: "",
range:[]
},
postRef: null
});
const { postForm, postRef } = toRefs(state)
const submitForm = async () => {
try {
let baseValid = await postRef.validate()
if (baseValid) {
let now = moment().startOf('day')
let start = postForm.range[0]
let end = postForm.range[1]
if (moment(postForm.month).isBefore(now)) {
ElementPlus.ElMessage.warning("设置月份不能小于当前月")
return
}
if (moment(start).isBefore(now)) {
ElementPlus.ElMessage.warning("开始日期不能小于当前日期")
return
}
if ( moment(end).isBefore(now)) {
ElementPlus.ElMessage.warning("结束日期不能小于当前日期")
return
}
// 开始请求接口
}
} catch (error) {
}
};
</script>
使用函数式编程代码
<template>
<el-form ref="postRef" :model="postForm" >
<el-form-item label="月份" prop="month">
<el-date-picker v-model="postForm.month" type="month" placeholder="请选择" format="YYYYMM" />
</el-form-item>
<el-form-item label="设置日期区间" prop="range">
<el-date-picker v-model="postForm.range" type="daterange" placeholder="请选择" start-placeholder="开始日期" end-placeholder="结束日期" />
</el-form-item>
</el-form>
<el-button @click="submitForm">提交</el-button>
</template>
<script setup lang="ts">
//列表查询
import { reactive, toRefs} from 'vue'
import submitFlow from './hooks.ts'
const state = reactive({
postForm: {
month: "",
range:[]
},
postRef: null
});
const { postForm, postRef } = toRefs(state)
const submitForm = () => {
postRef.value.validate((baseValid) => {
const now = new Date(); // 获取当前日期
const startOfMonth = new Date((new Date(now.getFullYear(), now.getMonth(), 1)).getTime()); // 获取月初时间
const requestFn = () => {
// 开始请求接口
}
baseValid && submitFlow({ postForm : postForm.value, now:startOfMonth },requestFn);
})
};
</script>
hooks.ts
import {ElMessage} from 'element-plus'
const enumType = { month: 'month' , range: 'range' };
const compose = (...pipes) => {
return pipes.reduceRight((result, next) => {
const temp = (...args) => {
const subResult = next(result(...args));
return subResult;
};
return temp;
});
};
const checkDateBuilder = (type: string, endpoint: number | undefined, fn: () => void) => (input: { postForm: any; now: any; }) => {
if (!input) return undefined;
const { postForm, now } = input;
let compareVal = postForm[enumType[type]];
if (endpoint !== undefined) {
compareVal = compareVal[endpoint];
}
const result = Date.parse(compareVal) >= Date.parse(now);
//console.log("compareVal",compareVal," now",now)
if (result) return { postForm, now };
fn();
return undefined;
};
const warningHandlerBuilder = msg => () => {
//console.log('>>>', msg);
ElMessage.warning(msg);
};
const messages = {
monthChecker: '设置月份不能小于当前月',
monthCheckStart: '开始日期不能小于当前日期',
monthCheckEnd: '结束日期不能小于当前日期',
};
const funcs = Object.keys(messages).reduce((result, next) => {
result[next] = warningHandlerBuilder(messages[next]);
return result;
}, {});
const checkMonth = checkDateBuilder('month', undefined, funcs['monthChecker']);
const checkStart = checkDateBuilder('range', 0, funcs['monthCheckStart']);
const checkEnd = checkDateBuilder('range', 1, funcs['monthCheckEnd']);
const submitBuilder = (input,fn) => {
if (!input) return;
console.log('开始提交');
fn()
};
const pipes = [
checkEnd,
checkStart,
checkMonth
];
const submitCompose = compose(...pipes);
const submitFlow = ({ postForm, now },requestFn) => submitBuilder(submitCompose({ postForm, now }),requestFn);
export default submitFlow;
2.动态表格校验
面向过程编程代码
<template>
<el-table :data="props.tableData" :row-key="props.rowKey">
<el-table-column
v-for="column in props.columns"
:key="column.property"
:label="column.label"
:prop="column.property"
>
...
</el-table-column>
</el-table>
</template>
<script setup lang="tsx">
import {ElMessage} from 'element-plus'
import {computed} from 'vue'
interface Props {
rowKey: string;
tableData: Array<any>;
columns: Array<any>;
rules?: any;
}
const props = defineProps<Props>();
//校验当前行
const validate = (row:any,ignore:Array<string>) => {
for (let i = 0; i < props.columns.length; i++) {
const col = props.columns[i];
if(ignore && ignore.includes(col.property)) {
continue;
}
if(col.validate) {
if(col.type === 'input' && col.attr && col.attr.type === 'number') {
if(col.attr.min === -Infinity) { //包含负数
}else {
if(row[col.property] < 0) {
ElMessage.error(`${col.label}不能小于0`)
return false
}
}
if(row[col.property] === '' || row[col.property] == null ) {
ElMessage.error(`${col.label}不能为空`)
return false
}
}else {
if(!row[col.property]) {
ElMessage.error(`${col.label}不能为空`)
return false
}
}
}
}
return true
}
defineExpose({
validate
})
</script>
使用函数式编程代码
<template>
<el-table :data="props.tableData" :row-key="props.rowKey">
<el-table-column
v-for="column in props.columns"
:key="column.property"
:label="column.label"
:prop="column.property"
>
...
</el-table-column>
</el-table>
</template>
<script setup lang="tsx">
import {ElMessage} from 'element-plus'
import {computed} from 'vue'
interface Props {
rowKey: string;
tableData: Array<any>;
columns: Array<any>;
rules?: any;
}
const props = defineProps<Props>();
//校验当前行
const validate = (row:any,ignore:Array<string> = []) => {
const isIgnore = (val:string) => ignore.includes(val)
const isEmpty = (val:any) => val === '' || val == null
const showMsg = (msg:string) => {ElMessage.error(msg) ;return false}
const isNumberInput = (col:any) => (col.attr || {}).type === 'number';
const validateColumn = (col:any) => {
let value = row[col.property]
const validateFuns = {
'input' : () => {
if(isNumberInput(col) && ['positive','float'].includes(col.attr.number)) {
return value < 0 ? showMsg(`${col.label}不能小于0`) : true;
}
return isEmpty(value) ? showMsg(`${col.label}不能为空`) : true;
},
'default' : () => !value ? showMsg(`${col.label}不能为空`) : true
}
return (validateFuns[col.type] || validateFuns.default)()
}
return props.columns.filter(col => !isIgnore(col.property) && col.validate).every(validateColumn)
}
defineExpose({
validate
})
</script>