函数式编程

November 22, 2025
2 min read
Article

JS函数式编程的三个特征#

  • 拥抱纯函数, 隔离副作用
  • 函数是一等公民
  • 避免对状态的改变

命令式: 由具体步骤组成的命令序列, 就是一段命令式程序 声明式: 不关心具体的过程只关心输入和输出 (函数式编程是声明式的一种)

纯函数和副作用#

==question:==

  • 纯函数, 副作用的内涵
  • 纯函数/非纯函数的辨析
  • 从数据流的角度理解纯和不纯的本质
  • 纯函数解决了什么问题

什么是纯函数#

满足两个条件

  1. 对于同样的输入能够得到同样的输出
  2. 在执行过程中没有语义上可观察的副作用

什么是副作用#

除了可能的返回值之外, 对==主调用函数==产生的附加的影响

除了计算不该搞别的

什么是函数是一等公民?#

头等函数 == 函数是一等公民

头等函数的核心特征是可以被当做变量一样使用

  1. 可以被当做参数给另一个函数使用
  2. 可以作为函数的返回值
  3. 可以被赋值给一个变量

一等公民的本质#

JS函数是可以执行的对象

使得 --> 以函数为基本单位构建应用程序称为可能

函数组合思想(compose/pipe)#

将待组合的函数放入一个数组当中然后调用这个函数数组, 就能够创建一个多个函数组成的工作流

如何实现一个经典的Pipe函数?

js
function pipe(funcs) { // 通过reduce实现流式调用, 先定义reduce所需的callback函数 function callback(accumlator, currentValue) { // 这里accumlator就是需要进入迭代的值, currentValue就是需要链式调用的当前函数 return currentValue(initialValue); } // 这里的param相当于就是initialValue return function (param) { funcs.reduce(callback, param) } }

==面相对象的核心是继承, 函数式编程的核心在于组合==

多元函数解决方案: 从==编码工具==视角看偏函数&柯里化#

在JavaScript中,使用...params(也称为剩余参数语法)作为函数的形参,允许函数接收任意数量的参数作为一个数组。这种语法在函数定义时使用,使得函数能够以更灵活的方式处理不确定数量的参数。

参数个数的对齐是组合链能够运转的前提

像是之前定义的compose函数: 链上的函数都只能够接受一个参数, 如果传入的函数需要一个以上的参数, 会导致参数不齐

柯里化#

将1个n元函数改造成n个相互嵌套的一元函数的过程

fn(a, b, c) ====> fn(a)(b)(c)

偏函数#

部分应用 通过固定一部分的参数, 生成一个参数量更少的函数的过程

==通过高阶函数封装偏函数能够避免大量重复代码,省去读函数,理解函数的过程,只需要关注函数的入参==

柯里化和偏函数#

柯里化: n元函数变成n个一元函数 偏函数: n元函数编程一个m元函数(m<n)

如何打造一个通用的curry函数#

通用柯里化函数的实现(重难点)(再看一下背一下)#

curry函数内部结合入参的情况自动判断需要套娃的层数(不管传入多少个参数, 都能够分析出参数的数量)

js
function carry(func, arity = func.length) { function generateCurried(prevArgs) { return function curried(nextArgs) { // 统计已记忆和未记忆的参数数量 const args = [...prevArgs, nextArg] // 如果已经大于回归函数元数, 则认为已经记忆了所有的参数 if(args.length >= arity) { return func(...args); } else { return generateCurried(args); } } } // 初始为一个空数组, 表示还没有记忆任何参数 return generatedCurried([]); }

范畴论启发下的函数设计模式(范畴论相关Intro)#

范畴论在JS当中的应用, 本质上是为了解决函数组合的问题

范畴论相关概念#

  • 对象--数据
  • 态射--函数

函数能够进行复合运算 g(x).f(x)

需要注意的是

  • 函数运行的顺序
  • 函数满足结合律

函子: 能够将一个范畴映射到另一个范畴的东西

关于盒子模式#

盒子有哪些规律:

  1. 盒子是一个存放数据的容器
  2. 数据总是以盒子入参的形式传入
  3. 盒子内部能够定义一系列操作数据的函数

盒子当中的Map函数需要有能够创建并且返回新盒子的能力: map函数的作用就是实现数据的映射

image.png

image.png

Functor: 盒子模式构造函数组合链#

Array就是一个Functor(函子)#

  • 本身带有盛放的数据
  • map()方法
    • 返回一个新的box
    • 做了数据的映射

Maybe Functor: 识别空数据#

如果上一个盒子产生了一个空数据, 还要想下一个进行传递, 那么下一个可能无法正常进行数据的操作, 可能会导致JS报错, 后续程序无法执行

一个MaybeFunctor的实现

js
const isEmpty = x => x === undefined || x ===null; const Maybe = x => { map: f => isEmpty(x) ? Maybe(null) : Maybe(f(x)), valueOf: () => x, inspect: () => `Maybe {${x}}` }

通过使用isEmpty函数能够实现空对象的判断, 如果像是传入的f是toString方法在null身上进行调用, 会报错, 但是通过MaybeFunctor的实现能够直接断掉不会实际运行f()函数

Functor需要遵守的规则#

  • 恒等性
    • map方法创建出来的盒子应该和原来的盒子等价
  • 可组合性
    • 要求Functor能够将嵌套的函数拆解为平行的链式调用

Monad: 嵌套盒子问题解法#

Monad就是"单子": 是一种特殊的函子Functor

嵌套盒子问题#

两种情况

  • 线性计算场景: Functor作为另一个Functor的==计算中间态==出现
  • 非线性计算场景: ==两个Functor共同作为计算入参出现==

Thanks for reading!