- 作者:老汪软件技巧
- 发表时间:2024-11-18 10:04
- 浏览量:
一、起因
在写业务代码的时候,经常会在思考应该怎么组织自己的逻辑,按哪些步骤,不同逻辑之间的关系应该是怎么样的。这很重要,合理,清晰的组织逻辑能够较为准确的表达你在做什么,同时可以体现你对业务的理解程度。那是不是对业务理解十分透彻就能把代码组织好呢,显然是需要技巧的。函数式编程思想就提供一种思路帮助我们在写代码过程中如何巧妙的去组织逻辑,以及处理它们之间的关系。
1、函数式编程是什么
函数式说明了所有的逻辑组织都是以函数为中心的。
函数对于任何语言来说都是一个核心的概念。很多时候,我们会定义一个函数,专门处理某一个业务逻辑。
函数式编程是一种编程范式,它将逻辑运算过程视为数学函数的组合,强调不可变数据、纯函数和高阶函数的使用等。
2、实现一个函数应该关心什么
设计就是要实现函数签名,在js语言里面,函数签名可以理解包含函数名、参数列表、返回值。
这一点至关重要,函数名要语义化,一看就可以知道大致是做什么功能的;参数列表要明确,参数类型是怎样的;这个函数有没有返回值,要么让函数始终都返回一个值,要么永远不要返回值,否则某种情况下有返回值,某种情况下没有返回值,会给调试带来困难。
函数内部的实现就是基于你的业务逻辑而定。
在很大程度上,函数设计决定了函数的调用。调用一个函数的时候,往往关心的是这个函数要传递什么参数,如果有返回值的情况,只关心得到的结果,对于内部的实现也许不会过于关注。如果没有返回值的情况下才会关心内部实现的业务逻辑。
二、函数设计1、arguments对象
在函数中,参数是用一个数组来表示的。在函数体内可以使用arguments对象来访问,它的对象长度是由传入的参数个数决定,不是由定义函数时的命名参数个数决定。
在非严格模式下,arguments的值与对应命名参数的值保持一致,但可以修改:
function doAdd(num1,num2) {
console.log(arguments[1]) // 20
arguments[1] = 10
console.log(arguments[1]) // 10
console.log(arguments[0]+num2)
}
doAdd(10,20) // 20
在严格模式下重写arguments的值会导致语法错误。
2、参数默认值
es6支持在声明函数时同时初始化参数的值,在调用函数的时候只有传入的值为undefined默认值才会生效
function foo(a, b=2, c=3) {
console.log(a,b,c)
}
foo() // undefined,2,3
foo(1, null, undefined) // 1,null,3
另外一种情况极端的情况,函数参数默认值会受到暂时性死区的影响
function foo(a=b, b=222) {
console.log(a)
console.log(b)
}
foo(undefined, 111) // ReferenceError: Cannot access 'b' before initialization
function bar(a=getValue(b),b) {
console.log(a,b)
}
bar(undefined,1) // error
null会被识别成值
function foo(a=b, b=222) {
console.log(a)
console.log(b)
}
foo(null, undefined) // null 222
foo(null, null) // null null
因此应该避免出现函数默认值与其他参数存在关联或则某种映射关系
3、剩余参数
function calNum(a,b,c,d) {
let sum = 0
for(let i = 0;i<arguments.length;i++) {
sum = sum + arguments[i]
}
}
calNum(1,2,3,4)
如果想传入一个type参数,用于决定是求和还是求积
function calNum(type,a,b,c,d) {
let sum = 0
for(let i = 0;i<arguments.length;i++) {
if (type === 'sum') {
sum = sum + arguments[i]
}
if (type === 'quadrature') {
sum = sum * arguments[i]
}
}
console.log(sum)
}
如果在这种在这种情况下,一个函数后面几个参数含义,地位是一样的话,那么可以把他们统一收纳为一个数组中作为为剩余参数
function calNum(type,...num) {
let sum = 0
for(let i = 0;ilength;i++) {
if (type === 'sum') {
sum = sum + num[i]
}
if (type === 'quadrature') {
sum = sum * num[i]
}
}
console.log(sum)
}
calNum('sum',1) // 1
calNum('sum',1,2) // 3
calNum('sum',1,2,3) // 6
calNum('sum',1,2,3,4) // 10
4、对象参数
函数传参一定是按顺序传参的,如果有多个参数,在调用函数时,你只想传递某些参数,比如:
function getInfo(userId,userName,age,sex) {
return {
userId,
userName,
age,
sex
}
}
getInfo(undefined,'zhangsan',undefined,'male')
为了更加优雅的传值,也为了避免顺序不对传错参数,可以把参数定义成一个对象,函数体获取参数使用解构
// 函数签名注释...
function getInfo(option) {
const {userId,userName,age,sex} = option
return {
userId,
userName,
age,
sex
}
}
getInfo({userName:'zhangsan',sex:'male'})
5、参数归一化
在设计,实现一个函数的时候,你可能会想着为了满足各种调用的情况,于是你在函数中定义了好几个不同的参数,方便实现过程中进入不同的分支,处理不同的场景。为了避免定义的参数越来越多,函数内部实现越来越复杂,可以通过一些辅助函数,将参数进行归一化,从而使得函数更加优雅,通用。
假设我们有一个函数,用于计算用户的年龄是否符合某个活动的年龄要求。这个函数需要处理多种输入情况,包括:
用户年龄直接以数字形式给出。用户年龄以字符串形式给出。用户信息以对象形式给出,其中包含年龄字段。
function checkAgeEligibility(user, activityAgeRequirement) {
const userAge = normalizeUserAge(user);
if (userAge >= activityAgeRequirement) {
console.log('User is eligible for the activity.');
} else {
console.log('User is not eligible for the activity.');
}
}
创建一个参数归一化辅助函数
function normalizeUserAge(user) {
// 检查用户是否为对象,并包含年龄字段
if (typeof user === 'object' && user !== null && 'age' in user) {
// 递归调用 normalizeUserAge 处理对象中的年龄字段
return normalizeUserAge(user.age);
}
// 检查用户是否为字符串,并尝试转换为数字
if (typeof user === 'string') {
const age = parseInt(user, 10);
// 检查转换后是否为有效数字
if (!isNaN(age)) {
return age;
}
}
// 检查用户是否为数字
if (typeof user === 'number' && isFinite(user)) {
return user;
}
// 如果输入无效,抛出错误
throw new Error('Invalid user age format');
}
实际调用
// 用户年龄为数字
checkAgeEligibility(25, 18);
// 用户年龄为字符串
checkAgeEligibility('30', 18);
// 用户信息为对象
checkAgeEligibility({ age: '28' }, 18);
6、回调函数
指的是一个函数作为参数传递给另一个函数,然后在特定的时间点或条件满足时被调用。
这种机制允许程序在某个操作完成时得到通知,并执行相应的回调函数来进行后续处理。
假设声明一个函数,传入参数a, b,但是具体做什么,在调用的时候决定,具体的逻辑是一个函数,作为参数传入
function excuteFn(a,b,fn) {
//....执行了很多前置逻辑
return fn(a,b)
}
// 调用函数excuteFn得到的结果就是执行传入的匿名函数后得到结果
const total = excuteFn(1,2,function(a,b){
return a+b
})
console.log(total) // 3
三、函数实现1、纯函数
定义一个函数,把围绕一个中心逻辑的多条语句放到一起作为函数体,相同的输入总是产生相同的输出。不产生副作用,不依赖外部状态。这个函数要么有返回值,要么没有返回值。
假设要反转输入的字符串:
let resStr = ''
function reverseString(str) {
return str.split('').reverse().join('');
}
resStr = reverseString('hell world')
let resStr = ''
function reverseString(str) {
resStr = str.split('').reverse().join('');
}
reverseString('hell world')
2、高阶函数
它指的是接受函数作为参数、返回函数作为结果或者两者兼有的函数。
因此,高阶函数通常有这几个特点:
根据这些特点,有以下几种常见的应用
接收函数作为参数
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(function(number) {
return number * 2;
});
// doubled 的值将是 [2, 4, 6, 8]
返回函数作为结果
function greet() {
const prefix = 'Hello';
return function(name) {
return prefix + ' ' + name;
};
}
const greetJohn = greet();
console.log(greetJohn('John')); // 输出 "Hello John"
闭包的使用
makeAdder 函数返回了一个闭包,这个闭包记住了 x 的值,并将其与传入的 y 值相加
function makeAdder(x) {
return function(y) {
return x + y;
};
}
const add5 = makeAdder(5);
console.log(add5(3)); // 输出 8
函数组合
在逻辑构建过程中,希望每一个函数都有足够合理的颗粒度,尽可能遵循单一职责的设计模式。因此函数组合的目标是,在需要的时候组合用于完成复杂的操作,而不需要直接修改原有的函数。
假设我们有两个函数:
double(x):将输入值翻倍。increment(x):将输入值增加1。
function double(x) {
return x * 2;
}
function increment(x) {
return x + 1;
}
现在,我们想要创建一个新的函数 compose,它接受两个函数作为参数,并返回一个新的函数,这个新函数将第一个函数应用于第二个函数的结果:
function compose(f, g) {
return function(x) {
return f(g(x));
};
}
使用 compose 函数,我们可以组合 double 和 increment:
console.log(doubleIncrement(3)); // 输出 8,因为 (3 + 1) * 2 = 8
装饰器
装饰器可以让我们可以在不改变原始函数代码的情况下,给函数增加额外的处理逻辑。
// 定义一个装饰器函数
function myDecorator(func) {
// 返回一个新的函数,这个函数包装了原始函数
return function() {
console.log("Before function");
func(); // 调用原始函数
console.log("After function");
};
}
// 使用装饰器
function sayHello() {
console.log("Hello!");
}
// 应用装饰器
sayHello = myDecorator(sayHello);
// 调用被装饰的函数
sayHello(); // 输出:Before function, Hello!, After function
柯里化函数
柯里化(Currying)是一种将多参数函数转换成一系列使用一个或多个参数的函数的过程。
下面结合柯里化函数的应用来介绍:
参数复用
假设我们有一个计算折扣价格的函数,它接受三个参数:原价、折扣率和数量。不同的产品享受同样的折扣率,因此可以固定折扣率参数。
function calculateDiscountPrice(originalPrice, discountRate, quantity) {
return (originalPrice * (1 - discountRate)) * quantity;
}
// 柯里化
function curryCalculateDiscountPrice(discountRate) {
return function(originalPrice, quantity) {
return calculateDiscountPrice(originalPrice, discountRate, quantity);
};
}
// 使用柯里化函数
const discountRate = 0.1; // 10% 折扣
const calculatePriceWithDiscount = curryCalculateDiscountPrice(discountRate);
// 现在我们可以为不同的商品和数量计算折扣价格
console.log(calculatePriceWithDiscount(100, 2)); // 计算原价100,数量2的商品的折扣价格
假设我们有一个电子商务平台,需要根据不同的用户等级(例如:普通用户、VIP用户、超级VIP用户)来应用不同的折扣策略。每个用户等级对应一个固定的折扣率。我们需要一个函数来计算用户购买商品后的最终价格。
// 原始的计算价格函数,接受原价、折扣率和数量
function calculatePrice(originalPrice, discountRate, quantity) {
return originalPrice * (1 - discountRate) * quantity;
}
// 柯里化函数,根据用户等级返回一个计算价格的函数
function curryByUserLevel(userLevel) {
// 根据不同的用户等级设置不同的折扣率
const discountRates = {
normal: 0.05, // 普通用户5%折扣
vip: 0.1, // VIP用户10%折扣
svip: 0.15 // 超级VIP用户15%折扣
};
const discountRate = discountRates[userLevel];
// 返回一个新的函数,这个函数接受商品的原价和数量
return function(originalPrice, quantity) {
return calculatePrice(originalPrice, discountRate, quantity);
};
}
// 使用柯里化函数,相当固定了不同类型的vip的折扣率
const calculatePriceForNormalUser = curryByUserLevel('normal');
const calculatePriceForVipUser = curryByUserLevel('vip');
const calculatePriceForSvipUser = curryByUserLevel('svip');
// 计算不同用户等级的购买价格
console.log(calculatePriceForNormalUser(100, 1)); // 普通用户购买价格
console.log(calculatePriceForVipUser(100, 1)); // VIP用户购买价格
console.log(calculatePriceForSvipUser(100, 1)); // 超级VIP用户购买价格
提高代码的可读性和可维护性
// 日志记录函数
function log(type, message) {
console.log(`[${message}] ${type}`);
}
// 柯里化 log 函数
// 柯里化函数curry后面实现
const curriedLog = curry(log);
// 创建不同级别的日志记录函数
const infoLog = curriedLog('INFO');
const errorLog = curriedLog('ERROR');
// 使用不同级别的日志记录函数
infoLog('This is an info message.');// [This is an info message.] INFO
errorLog('This is an error message.');// [This is an error message.] ERROR
柯里化函数的实现
要理解科柯里化函数的实现,先理解柯里化的定义以及闭包
最简单的柯里化
// 原函数
const add = function(a,b) {
return a + b
}
// 柯里化后
const curriedAdd = function(a) {
return function(b) {
return a + b
}
}
// 调用柯里化
const total = curriedAdd(1)(2)
console.log(total) // 3
柯里化就是一步一步拆解传入的参数,并利用闭包的特性使得先前传入的变量依旧可以访问。
下面看一个闭包的使用
function curry() {
let count = 0
return function curried(...args) {
console.log(...args,'args')
++count
console.log(count, 'count')
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
};
}
const afn = curry()
const bfn = afn(1)
const cfn = bfn(2)
const cdfn = cfn(3)
// 输出
// 1 'args'
// 1 'count'
// 1 2 'args'
// 2 'count'
// 1 2 3 'args'
// 3 'count'
闭包: curried 函数形成了一个闭包,它捕获并记住了每次调用时传入的参数 args。这意味着即使 curried 函数被返回并用于后续的调用,它也能保持对之前传入参数的引用。
参数累加: 每次调用 curried 时,我们都将新的参数 args2 与之前的参数 args 结合起来args.concat(args2)。
避免无限递归: 如果 curried 是一个匿名函数,那么在递归调用时,它将不断地创建新的匿名函数,通过使用一个固定的名称 curried,我们可以确保递归调用的是同一个函数引用。
通过上面两个例子的抛转引玉,很容易就知道,停止递归,执行真正的业务逻辑的条件就是,传入的参数累计收集的个人等于原业务逻辑函数的参数个数
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}