函数式编程范式(二)

函数式编程范式(二)

纯函数

什么是纯函数?

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

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

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

下面举一个 纯函数 和 非纯函数 的例子 Array.prototype.sliceArray.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. 易于测试:组合的基础函数可单独测试,减少整体逻辑的测试复杂度。
相关推荐
J2虾虾2 小时前
通过Web界面来访问和操作MySQL数据库的开源项目
前端·数据库·mysql
羊吖2 小时前
Vue3 + Electron 实现纯本地人脸识别登录一体机(离线可用、无云端、带页面跳转)
前端·javascript·electron
德莱厄斯2 小时前
比阿里开源的 page-agent 更强?AutoPilot: 网页内置一个真正能"稳定跑完"的智能体
前端·agent·浏览器
新缸中之脑2 小时前
Chrome DevTools MCP
前端·chrome·chrome devtools
卸载引擎2 小时前
NTP 授时(Network Time Protocol)核心解读,工控机electron程序自动联网授时案例
前端·javascript·electron
xiaokangzhe2 小时前
web技术与nginx网站环境部署
运维·前端·nginx
小奶包他干奶奶2 小时前
什么是原型链(Prototype Chain)?proto和prototype的关系与区别是什么?
前端·javascript
Access开发易登软件2 小时前
在 Access 实现标签输入控件:VBA + HTML 混合开发实战
前端·数据库·信息可视化·html·excel·vba·access
૮・ﻌ・2 小时前
Nodejs - 02:模块化、npm、yarn、cnpm
前端·npm·node.js·express·yarn·cnpm·包管理工具