函数式编程范式(二)

函数式编程范式(二)

纯函数

什么是纯函数?

函数式编程中的函数就是纯函数。

具体特征就是,相同的输入永远会得到相同的输出,而且没有任何可观察的副作用。

纯函数就类似于数学中函数(用来描述输入与输出之间的关系),y = f(x)

下面举一个 纯函数 和 非纯函数 的例子 Array.prototype.slice 和 Array.prototype.splice

typescript 复制代码
let arr = [1, 2, 3, 4, 5, 6, 7, 8]
// 纯函数
console.log(arr.slice(0, 2)) // [ 1, 2 ]
console.log(arr.slice(0, 2)) // [ 1, 2 ]
console.log(arr.slice(0, 2)) // [ 1, 2 ]
// 非纯函数
console.log(arr.splice(0, 2)) // [ 1, 2 ]
console.log(arr.splice(0, 2)) // [ 3, 4 ]
console.log(arr.splice(0, 2)) // [ 5, 6 ]

函数式编程不会保留中间的结果,所以变量是不可变的(无状态的)。

我们可以将一个函数的执行结果交给另外一个函数去处理。

纯函数的好处
  1. 可预测性

    纯函数的「相同输入→相同输出」特性,让代码行为完全可预测 ------ 这是纯函数最核心的价值。

  2. 可缓存性(Memoization),提升性能

    因为纯函数的输出只依赖输入,所以可以把「输入→输出」的映射缓存起来,重复调用时直接返回缓存结果,避免重复计算。

  3. 可测试性

    纯函数无需依赖外部环境,测试时只需关注 "输入→输出",不用模拟 / 搭建复杂的测试环境。

  4. 可并行执行

    纯函数不依赖外部状态,也不修改外部状态,因此可以安全地在多线程 / 并发环境中执行(比如 Web Worker、Node.js 多进程),无需担心 "竞态条件"(多个线程同时修改同一变量)。

  5. 无 副作用 ,避免隐藏 bug

    纯函数不会修改外部状态,因此不会引发 "意外的变量修改""状态污染" 等隐蔽 bug。

副作用

我们再纯函数的优点中提到了 副作用,那究竟什么是副作用呢?

副作用是函数 "本职工作(计算返回值)" 之外的 "额外操作",这些操作会影响函数外部的状态或环境,导致函数的行为不可预测、依赖外部上下文(不纯)。

常见的副作用来源:

  • 函数修改了自身作用域之外的变量(全局变量、闭包变量、函数参数中的引用类型)
  • 依赖随机值 / 时间等不可控因素
  • 修改函数参数的原始值(尤其引用类型)
  • 操作 DOM(修改页面元素、绑定事件)
  • 发起网络请求
  • 读写文件
  • 操作本地存储

实际开发中,我们不可能完全避免副作用(比如前端必须操作 DOM、必须发网络请求),我们只能尽可能的让副作用 "可控、可追踪"。

柯里化

柯里化 是将一个 接收多个参数 的函数,转换成 一系列只接收单个参数 的函数的过程。最终通过依次调用这些单参数函数,逐步收集参数,直到所有参数收集完毕后执行原逻辑并返回结果。

有点抽象我们用一个例子来演示一下:

typescript 复制代码
// 普通函数:一次接收3个参数
const add = (a: number, b: number, c: number) => {
  return a + b + c
}
add(1, 2, 3) // 直接传所有参数,返回6

// 柯里化函数:分步接收参数
const curryAdd = (a: number) =>
  // 接收第一个参数a,返回接收第二个参数的函数
  (b: number) => 
    // 接收第二个参数b,返回接收第三个参数的函数
    (c: number) => 
      // 接收最后一个参数c,执行原逻辑
      a + b + c

// 调用方式1:分步传参(核心特征)
curryAdd(1)(2)(3) // 依次传参,返回6

// 调用方式2:也可以部分传参(偏应用)
const add1 = curryAdd(1) // 固定第一个参数为1
const add1And2 = add1(2) // 固定第二个参数为2
add1And2(3) // 传第三个参数,返回6
add1And2(4) // 复用:传新的第三个参数,返回7
柯里化的核心用途
  • 参数复用(最常用)

    可以固定函数的部分参数,生成一个新函数,后续复用这个新函数时无需重复传固定参数。

  • 延迟执行(控制执行时机)

  • 柯里化函数不会立即执行原逻辑,而是等参数收集完毕后才执行,适合需要 "先收集参数、后执行" 的场景(如事件处理、防抖节流)。

  • 函数粒度细化(符合 "单一职责")

    把多参数函数拆分成多个单参数函数,每个函数只负责处理一个参数,逻辑更清晰,也更容易组合(函数组合是函数式编程的核心)。

柯里化通用函数

上面的柯里化示例中,可以看出来我们是利用了高阶函数和闭包的特性,手动嵌套函数实现了函数的柯里化,但是这种方法并不通用。

lodash 中有提供一个通用的柯里化函数

typescript 复制代码
import { curry } from 'lodash-es'
// 普通函数:一次接收3个参数
const add = (a: number, b: number, c: number) => {
  return a + b + c
}

const curryAdd = curry(add)

curryAdd(1)(2)(3) // 6

const add1 = curryAdd(1)
const add1And2 = add1(2)
add1And2(3) // 6
add1And2(4) // 7

// 还可以这样调用
curryAdd(1, 2, 3)
curryAdd(1, 2)(3)
curryAdd(1)(2, 3)

为了便于更好的理解柯里化,我们手动模拟一下 _.curry(func) 的实现

typescript 复制代码
const curry = <T extends (...arg: any[]) => any>(func: T) => {
  return function curriedFn(...args: any[]) { 
    // 判断实参和形参的个数
    if (args.length < func.length) {
      // 只传部分参数,返回传递剩余参数的函数
      return function (...nextArgs: any[]) {
        // 拼接后续传递参数,直至实参个数大于等于行程
        return curriedFn(...args.concat(nextArgs))
      }
    }
    // 直接传递全量的参数
    return func(...args)
  }
}

函数组合

函数组合(Function Composition)是函数式编程中的核心概念,它的本质是将多个单功能的函数组合成一个新函数,让数据依次通过这些函数处理,最终得到结果。

我们如果利用纯函数和柯里化很容易写出洋葱代码 f(g(h(x))),例如:

typescript 复制代码
// 转大写
const toUpperCase = (str: string) => str.toUpperCase();
// 去除空格
const trim = (str: string) => str.trim();
// 加感叹号
const addExclamation = (str: string) => `${str}!`;

const result = toUpperCase(trim(addExclamation("  hello world  "))); // 缺点:阅读顺序是从内到外,不直观

我们只要保证这些过程是纯函数,那么就可以利用函数组合,将这个复杂的过程组合成一个函数,这样我们就只需要调用这个组合函数就可以实现功能,不用关心中间函数的输入输出。

typescript 复制代码
const processStr = compose(addExclamation, trim, toUpperCase);
const result = processStr("  hello world  "); // "HELLO WORLD!!"
通用组合函数
typescript 复制代码
// flow 从左往右执行 flowRight从右往左执行
import { flow , flowRight } from 'lodash-es'

// 转大写
const toUpperCase = (str: string) => str.toUpperCase();
// 去除空格
const trim = (str: string) => str.trim();
// 加感叹号
const addExclamation = (str: string) => `${str}!`;

const fn = flow(toUpperCase, trim, addExclamation)

console.log(fn('  hello world  '))


export default {}

模拟实现 compose:

typescript 复制代码
/**
 * 函数组合:从右到左执行
 * @param funcs 要组合的函数数组
 * @returns 组合后的新函数
 */

const compose = (...funcs: any[]) => {
  return (value: any) => {
    return funcs.reverse().reduce((acc, fn) => fn(acc), value)
  }
}

函数组合默认是从右到左(compose),而管道函数(pipe)是从左到右,更符合日常阅读习惯

pipe (管道函数),去掉 reverse() 即可。

结合律(associativity)

函数的组合要满足结合律:单独组合某些函数,结果都是一样的

typescript 复制代码
import { flowRight, toUpper, first, reverse} from 'lodash-es'

// 取得数组最后一项并转为大写
// 下面三种写法 结果一样
const fn1 = flowRight(toUpper, first, reverse)
const fn2 = flowRight(toUpper, flowRight(first, reverse))
const fn3 = flowRight(flowRight(toUpper, first), reverse)

console.log(fn1(['one', 'two', 'three'])) // THREE
console.log(fn2(['one', 'two', 'three'])) // THREE
console.log(fn3(['one', 'two', 'three'])) // THREE
调试组合函数

我们可以通过插入调试函数的方式,来打印调试上一步执行的结果。

typescript 复制代码
import { flowRight, toUpper, first, reverse} from 'lodash-es'

const log = (value: string) => {
  console.log(value)
  return value
}

// 取得数组最后一项并转为大写
const fn = flowRight(toUpper, first, log, reverse)

fn1(['one', 'two', 'three']) // [ 'three', 'two', 'one' ]

我们也可以改造一下 log 函数,增加一个 tag 参数,方便查看。

typescript 复制代码
const trace = curry((tag: string, value: any) => {
  console.log(tag + ': ' + value)
  return value
})

const fn1 = flowRight(toUpper, first, trace('after reverse'), reverse)

Point Free

Point Free(也译作 "无值风格" 或 "无参风格")是函数式编程中的一种编码范式,核心思想是:定义函数时不直接提及函数所操作的参数(即 "点"),而是通过组合已有的函数来实现逻辑。

简单来说,普通写法会显式写出参数,而 Point Free 写法完全隐藏参数,让代码更简洁、更专注于 "做什么" 而非 "对谁做"。

非 Point Free 模式

typescript 复制代码
// 显式声明参数 str(这就是"点")
const processStr = (str) => str.trim().toUpperCase();

// 调用
console.log(processStr("  hello world  ")); // 输出:HELLO WORLD

Point Free 模式

typescript 复制代码
// 先定义基础函数
const trim = (str) => str.trim();
const toUpper = (str) => str.toUpperCase();

// 组合函数,完全不提及参数
const processStr = (str) => toUpper(trim(str));
// 更极致的 Point Free(借助函数组合工具)
const compose = (f, g) => (x) => f(g(x)); // 简单的组合函数
const processStrPointFree = compose(toUpper, trim);

// 调用(结果和普通写法一致)
console.log(processStrPointFree("  hello world  ")); // 输出:HELLO WORLD
为什么要用 Point Free?
  1. 代码更简洁:去掉冗余的参数声明,聚焦核心逻辑;
  2. 复用性更高:通过组合已有函数实现新功能,避免重复写逻辑;
  3. 可读性更好 :函数名直接体现逻辑意图(比如 compose(toUpper, trim) 一眼就能看出 "先去空格再转大写");
  4. 易于测试:组合的基础函数可单独测试,减少整体逻辑的测试复杂度。
相关推荐
miss5 分钟前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan43 分钟前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真1 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment1 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan1 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么1 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模
上海懒图科技1 小时前
工厂厂区电子地图定位导航:厂区人车精准定位,安全合规双保障
前端·arcgis·vue
西安小哥2 小时前
端侧跑大模型,真正的门槛不在参数量:一次 596 秒到 10 秒的压缩路径复盘
前端·人工智能
陈随易2 小时前
Cloudflare 出钱,Vite 团队挑人,44 个维护者分 30 万美元
前端·后端·程序员
恋猫de小郭3 小时前
Flutter THLC 的 WebView 滚动终于从 70 FPS 提升到 120 FPS
android·前端·flutter