函数式编程
函数式编程
November 2, 2024
函数式编程两大基石
纯函数(Pure Function)
- 给定相同输入,永远返回相同输出
- 没有副作用(不修改外部状态)
- 不依赖外部状态
副作用 Side effect
在执行一个函数时,除了返回函数值外,还对调用函数产生了附加的影响,比如修改了全局变量、修改参数或改变外部的存储。
不可变性(Immutability)
- 这是一个数据处理的概念
- 确保数据创建后不被修改
- 任何修改都返回新的数据副本
让我们来看个具体的例子加深理解:
// 原始数据
let users = [
{ name: 'alice', age: 19 },
{ name: 'bob', age: 17 },
{ name: 'charlie', age: 20 },
]
// 非函数式写法
let result = []
for(let user of users) {
if(user.age >= 18) {
result.push({
...user,
name: user.name.charAt(0).toUpperCase() + user.name.slice(1)
})
}
}
现在用函数式的方式改写:
// 1. 纯函数:检查是否成年
const isAduit = user => user.age >= 18
// 2. 纯函数:格式化名字(保持不可变性)
const formatName = user => ({ ...user, // 创建新对象而不是修改原对象
name: user.name.charAt(0).toUpperCase() + user.name.slice(1)
});
// 3. 最终处理流程
const processUser = users => users.filter(isAduit).map(formatName)
// 使用
const result = processUsers(users);
console.log(result);
// 输出:
// [
// { name: 'Alice', age: 19 },
// { name: 'Charlie', age: 20 }
// ]
高阶函数(Higher-order Functions)
- 定义:接收或返回函数的函数
- 作用:提供了处理函数的抽象方法
函数组合(Function Composition)
- 定义:将多个函数连接起来,前一个函数的输出作为后一个函数的输入
- 本质:是高阶函数的一种应用
对于组合函数,同样也举个例子:
比如我们有一个处理字符串的任务:
- 去除空格
- 转为小写
- 获取前5个字符
// 首先我们有三个独立的纯函数
const removeSpaces = str => str.replace(/\s/g, '');
const toLowerCase = str => str.toLowerCase();
const take5 = str => str.slice(0, 5);
// 不用组合函数,我们需要这样嵌套调用
const input = "Hello World";
const result = take5(toLowerCase(removeSpaces(input)));
// "hello"
这种嵌套调用会有几个问题:
- 可读性差
- 不够灵活
- 不容易复用
使用函数组合来改进:
// 1. 创建一个函数组合
const compose = (...fns) => x => fns.reduceRight((val, fn) => fn(val), x)
// 或者从左到右的 pipe
const pipe = (...fns) => x => fns.reduce((val, fn) => fn(val), x)
// 2. 组合这些函数
const processString = compose(take5, toLowerCase, removeSpaces);
// 或用 pipe
const processString2 = pipe(removeSpaces, toLowerCase, take5);
// 3. 使用
const result = processString("Hello World") // hello
函数组合的优势
- 代码更清晰,从左到右或从右到左顺序执行
- 可以轻松调整函数顺序
- 组合后的函数可以复用
- 每个步骤都是独立的,容易测试和维护
柯里化(Currying)
- 定义:将多参数函数转换为单参数函数序列
- 本质:也是高阶函数的一种应用
下面我们通过对比来说明函数组合和柯里化的区别:
// 1. 函数组合:多个函数组合成一个新函数
const compose = (...fns) => x => fns.reduceRight((val, fn) => fn(val), x);
const add1 = x => x + 1;
const multiply2 = x => x * 2;
const subtract3 = x => x - 3;
const composed = compose(subtract3, multiply2, add1);
composed(5); // ((5 + 1) * 2) - 3 = 9
// 2. 柯里化:将多参数函数转换为单参数函数序列
const curry = (fn) => {
return function curried(...args) {
if(args.length >= fn.length) {
return fn.applay(this, args)
}
return (...args2) => curried.apply(this, args.concat(args2))
}
}
// 使用示例
function add(a, b, c) { // add.length 为 3
return a + b + c
}
const curriedAdd = curry(add)
// 执行过程分析
curriedAdd(1)(2)(3)
// 第一次调用: args=[1], 1 < 3, 返回新函数
// 第二次调用: args=[1,2], 2 < 3, 返回新函数
// 第三次调用: args=[1,2,3], 3 >= 3, 执行 add(1,2,3)
curriedAdd(1, 2)(3)
// 第一次调用: args=[1,2], 2 < 3, 返回新函数
// 第二次调用: args=[1,2,3], 3 >= 3, 执行 add(1,2,3)
curriedAdd(1, 2, 3)
// 第一次调用: args=[1,2,3], 3 >= 3, 直接执行 add(1,2,3)
splice
和 toSpliced
splice
是一个具有副作用的方法,因为它会直接修改原始数组。我们来详细分析:
const arr = [1, 2, 3, 4];
arr.splice(1, 2); // 返回被删除的元素 [2, 3]
console.log(arr); // [1, 4] - 原数组被修改了!
从函数式编程的角度来看:
- 违反了纯函数原则:
- 修改了外部状态(原数组)
- 有副作用
- 相同输入可能产生不同输出(取决于数组当前状态)
- 违反了不可变性原则:
- 直接修改原始数据结构
- 不返回新的数组副本
所以,splice
是典型的命令式编程,而不是函数式编程。
toSpliced()
是 JavaScript 数组的一个新方法,它是 splice()
的不可变版本。
// 1. splice() - 有副作用,会修改原数组
const array = [1, 2, 3, 4];
array.splice(1, 2); // 返回被删除的元素 [2, 3]
console.log(array); // [1, 4] - 原数组被修改!
// 2. toSpliced() - 无副作用,返回新数组
const array2 = [1, 2, 3, 4];
const newArray = array2.toSpliced(1, 2); // 返回新数组 [1, 4]
console.log(array2); // [1, 2, 3, 4] - 原数组不变
console.log(newArray); // [1, 4]
toSpliced()
符合函数式编程理念,因为:
- 不修改原数组(不可变性)
- 相同输入总是返回相同输出(纯函数)
- 没有副作用
toSpliced()
是 JS 数组新增的一系列不可变方法之一,其他还包括:
toReversed()
toSorted()
with()
这些方法都遵循了函数式编程的原则