函数式✅
函数式编程了解多少?
答案
函数式编程是一种将程序视为函数组合的编程范式,其中函数是基本构建单元。
核心概念:
-
纯函数:输出仅取决于输入,无副作用,相同输入总是产生相同输出
// 纯函数示例
const add = (a, b) => a + b -
不可变性:数据不允许被修改,避免副作用
// 不可变操作示例
const addToArray = (arr, item) => [...arr, item] // 返回新数组而非修改原数组 -
函数组合:将简单函数组合成复杂函数
const compose = (f, g) => x => f(g(x))
const addOne = x => x + 1
const double = x => x * 2
const addOneThenDouble = compose(double, addOne) -
高阶函数:可接收或返回其他函数的函数,如
map
、filter
、reduce
-
柯里化:将多参数函数转换为一系列单参数函数,实现参数复用
-
惰性计算:仅在必要时执行计算,提高性能
JavaScript中的函数式编程:
- 原生支持高阶函数:
Array.prototype.map
,filter
,reduce
等 - 流行库:Lodash/fp, Ramda, RxJS
- React函数式组件和Hooks体现了函数式思想
函数式编程 vs 面向对象编程:
- 函数式:强调数据不可变和无状态操作
- OOP:强调对象封装状态和行为
实际应用:
- 状态管理(Redux)
- 异步流程控制
- 数据转换和验证
- 事件处理
优势:
- 代码可读性和可维护性更高
- 减少bug,特别是由副作用引起的错误
- 更容易测试,因为函数行为可预测
- 简化并发编程,避免状态共享问题
- 促进代码复用和模块化
其他:
- 对性能有潜在影响(如创建大量小对象)
函数式编程通过强调纯函数和不可变数据,使代码更可靠、更易于理解和维护。
函数柯里化是什么?
答案
-
柯里化的定义 柯里化(Currying)它将一个接受多个参数的函数转换成一系列使用一个参数的函数。简单来说,就是把形如
f(a, b, c)
的函数转换为f(a)(b)(c)
的形式,使得函数的参数可以逐步传入而非一次性传入。 -
柯里化的作用
-
参数复用:可以预先固定一些参数,减少重复代码
const multiply = curry((a, b) => a * b)
const double = multiply(2) // 固定第一个参数为2
const triple = multiply(3) // 固定第一个参数为3 -
延迟计算:将函数调用分批进行,在需要的时候才执行计算
const curriedFilter = curry((predicate, arr) => arr.filter(predicate))
const filterGreaterThanThree = curriedFilter(num => num > 3) // 创建特定过滤函数
// 在需要时才传入数据执行计算
const result = filterGreaterThanThree([1, 2, 3, 4, 5]) // [4, 5] -
代码组合与复用:更容易构建模块化、可复用的代码
const curriedMap = curry((fn, arr) => arr.map(fn))
const doubleAll = curriedMap(double)
const tripleAll = curriedMap(triple) -
提高可读性:使代码更加函数式和模块化,每个函数更加聚焦
-
-
如何实现柯里化
function currying (fn, ...args) {
return args.length >= fn.length
? fn(...args)
: currying.bind(null, fn, ...args)
}
function add (a, b, c) {
return a + b + c
}
// 使用柯里化函数
const curriedAdd = currying(add)
console.log(curriedAdd(1)(2)(3)) // 6
console.log(curriedAdd(1, 2)(3)) // 6
console.log(curriedAdd(1)(2, 3)) // 6
console.log(curriedAdd(1, 2, 3)) // 6
什么是偏函数 ?
答案
偏函数(Partial Application)它指的是通过固定一个函数的一个或多个参数,从而产生一个新的函数。
偏函数与柯里化的区别:
- 偏函数:预先填充原函数的部分参数,返回一个新函数处理剩余参数
- 柯里化:将多参数函数转换为一系列单参数函数的链式调用
偏函数的特点:
-
参数固定:预先固定一个或多个参数,但不一定是第一个参数
function add (a, b, c) {
return a + b + c
}
// 偏函数:固定第一个参数为10
const add10 = partial(add, 10)
add10(5, 7) // 22 (10 + 5 + 7)
// 偏函数:固定第二个参数为10
const addSecond10 = partial(add, undefined, 10)
addSecond10(5, 7) // 22 (5 + 10 + 7) -
简化复杂函数:减少函数调用时所需参数数量
-
创建特定用途的函数:基于通用函数创建特定场景下的函数
偏函数的实现:
function partial (fn, ...args) {
return (args.length >= fn.length && args.every(el => el !== undefined))
? fn(...args)
: (...leftArgs) => {
const allArgys = Array.from({ length: fn.length }).map((el, index) => {
return args[index] !== undefined ? args[index] : leftArgs.shift()
})
return partial(fn, ...allArgys)
}
}
// 使用示例
function greet (greeting, name) {
return `${greeting}, ${name}!`
}
const sayHello = partial(greet, 'Hello')
console.log(sayHello('World')) // "Hello, World!"
const greetJohn = partial(greet, undefined, 'John')
console.log(greetJohn('Hi')) // "Hi, John!"
实际应用:
- 配置API调用时预设参数
- 简化事件处理函数
- 函数组合中固定部分参数
- 创建更具语义的函数名称
什么是 Compose ?
答案
Compose 是一种将多个函数组合成一个函数的技术或者模式。 在 Compose 过程中,一个函数的输出会作为下一个函数的输入,最终将多个函数串联起来,形成一个流水线,对数据进行连续处理。
Compose 的特点
-
从右到左执行
Compose 组合的函数,从右到左的顺序依次执行。也就是最右边的函数先执行,然后将结果传递给左边的函数,依次类推。
-
数据流向
数据像管道一样,从右向左依次流经各个函数,经过每个函数的处理转换,最终得到结果。
-
函数组合 Compose 的核心在于将多个函数组合成一个函数,使得代码更加简洁、可读性更高。
Compose 的实现
function compose (...fns) {
// 1. 兜底返回空函数
if (fns.length === 0) return () => undefined
return (...args) => fns.reverse().reduce((res, fn) => {
const inputArgs = [].concat(res)
// 2. 多个参数的话需要 deepmerge
const afterRes = fn(...inputArgs.concat(args.slice(inputArgs.length)))
return afterRes
}, args)
}
// 使用示例
function add (a) {
return a + 10
}
function multiply (a) {
return a * 10
}
const composedFunction = compose(multiply, add)
console.log(composedFunction(5)) // (5 + 10) * 10 = 150
Compose 的实际应用
-
简化复杂操作
Compose 可以将多个简单的函数组合成一个复杂的函数,简化代码逻辑,提高可读性。
-
中间件
在 Koa.js 等框架中,Compose 被广泛应用于中间件的实现,将多个中间件函数组合成一个函数,依次处理请求。
-
数据处理
Compose 可以用于对数据进行连续处理,例如先对数据进行格式化,然后进行验证,最后进行转换等操作。
什么是管道 (Pipeline)?
答案
管道(Pipeline)按照从左到右的顺序将多个函数组合在一起,形成一个数据处理的流水线。每个函数接收前一个函数的输出作为输入,最终返回结果。
管道的特点:
- 顺序执行:管道中的函数按照顺序依次执行,一个函数的输出作为下一个函数的输入。
- 数据流向:数据像流水一样,依次流经各个函数,经过每个函数的处理转换,最终得到结果。
- 链式调用:管道通常通过链式调用的方式实现,使得代码更加简洁、易读。
管道的实现:
function pipeline (...fns) {
// 1. 兜底返回空函数
if (fns.length === 0) return () => undefined
return (...args) => fns.reduce((res, fn) => {
const inputArgs = [].concat(res)
// 2. 多个参数的话需要 deepmerge
const afterRes = fn(...inputArgs.concat(args.slice(inputArgs.length)))
return afterRes
}, args)
}
// 使用示例
function add (a) {
return a + 10
}
function multiply (a) {
return a * 10
}
const pipeProcess = pipeline(add, multiply)
const result = pipeProcess(5) // (5 + 10) * 10 = 150
console.log(result)
管道 vs Compose:
特性 | 管道(Pipeline) | Compose |
---|---|---|
执行顺序 | 从左到右 | 从右到左 |
参数传递 | 前一个函数的输出作为下一个函数的输入 | 后一个函数的输出作为前一个函数的输入 |
适用场景 | 数据需要依次经过多个函数处理的场景 | 需要将多个函数组合成一个函数的场景 |
实现方式 | 通常使用 reduce 方法实现 | 通常使用 reduceRight 或 reverse 实现 |
代码可读性 | 更符合直觉,易于理解数据的处理流程 | 需要一定的函数式编程基础才能理解 |
组合方式 | 将函数作为参数传递给管道函数 | 通过 compose 函数将多个函数组合成一个新函数 |
什么是函子?
答案
函子(Functor) 是一种拥有 map
方法的数据结构,它能让我们在**不改变上下文(结构)**的前提下对其中的值进行变换。 JavaScript 中的 Array
、Promise
本身就是天然的函子。
函子的特点:
- 容器:函子是一个容器,它可以包含任何类型的值。
- 提供
map
方法,map(fn)
会返回一个新的函子; - 遵守两个 Functor 定律:恒等律 和 组合律。
- 恒等律:
f.map(x => x)
应该等于f
- 组合律:
f.map(x => fn(g(x)))
应该等于f.map(g).map(fn)
- 恒等律:
函子的实现:
class Functor {
// 支持持有一个值,注意这个值可以是任意类型,且不可变
constructor (value) {
this.value = value
}
// 提供一个映射方法,注意这个方法会返回一个新的 Functor
map (fn) {
return new Functor(fn(this.value))
}
}
// 使用示例
const functor = new Functor(5)
const newFunctor = functor.map(x => x + 10)
console.log(newFunctor.value) // 15
函子的实际应用:
- 处理 null 或 undefined 值:可以使用
Maybe
函子来避免空指针异常。 - 处理异步操作:可以使用
Promise
函子来处理异步操作的结果。 - 数据转换:可以使用函子来对数据进行转换,例如将字符串转换为数字。
一些典型的函子
- Maybe:一个能处理可空值的函子。它根据是否存在值(Just vs Nothing)决定是否执行
map
中的函数。 - Either:处理可能失败的操作。它有两个分支:
Left
表示错误,Right
表示成功。map
只作用于Right
分支。
什么是 Monad?
答案
Monad 是一种设计模式,用于将一系列操作以链式的方式组合起来,尤其适用于处理具有上下文的计算,如异步操作、错误处理或可空值等。
Monad 是对 Functor 的扩展,除了具备 map 方法外,还提供了两个核心操作:
unit(或 of)
:用于将一个普通值封装到 Monad 容器中。flatMap(或 bind、chain)
:用于将一个返回 Monad 的函数应用于容器中的值,并将结果“扁平化”,避免嵌套的 Monad。
通过这两个操作,Monad 允许我们以纯函数的方式处理带有上下文的计算,保持代码的可组合性和可预测性。
Monad 的核心特性:
- 封装值:Monad 是一个容器,它可以包含任何类型的值。
- unit/return 方法:Monad 必须提供一个
unit
或return
方法,该方法接收一个值作为参数,并将该值封装到 Monad 容器中。 - bind/flatMap 方法:Monad 必须提供一个
bind
或flatMap
方法,该方法接收一个函数作为参数,该函数接收 Monad 容器中的值作为参数,并返回一个新的 Monad 容器。bind
方法将该函数应用到 Monad 容器中的值上,返回一个新的 Monad 容器,该 Monad 容器包含应用函数后的结果。 - 一个符合 Monad 定义的类型应满足以下三个定律:
- 左单位律:
unit(a).flatMap(f)
等价于f(a)
- 右单位律:
m.flatMap(unit)
等价于m
- 结合律:
m.flatMap(f).flatMap(g)
等价于m.flatMap(x => f(x).flatMap(g))
- 左单位律:
以下是一个简单的 Maybe Monad 实现,用于处理可能为 null 或 undefined 的值::
class Maybe {
constructor (value) {
this.value = value
}
static of (value) {
return new Maybe(value)
}
isNothing () {
return this.value === null || this.value === undefined
}
map (fn) {
return this.isNothing() ? this : Maybe.of(fn(this.value))
}
flatMap (fn) {
return this.isNothing() ? this : fn(this.value)
}
getOrElse (defaultValue) {
return this.isNothing() ? defaultValue : this.value
}
}
// Maybe Monad 通过 map 和 flatMap 方法,允许安全地对可能为空的值进行操作,避免了显式的空值检查。
const result = Maybe.of(5)
.map(x => x + 1)
.flatMap(x => Maybe.of(x * 2))
.getOrElse(0)
console.log(result) // 输出: 12
Monad 的实际应用:
- 异步操作:Promise 是 JavaScript 中的一个 Monad,用于处理异步计算。通过 then 方法(类似于 flatMap),我们可以将异步操作串联起来。
- 错误处理:Either Monad 允许我们在不使用异常的情况下处理错误。它通常有两个分支:Right 表示成功,Left 表示失败。
- 可空值处理:Maybe Monad 提供了一种优雅的方式来处理可能为 null 或 undefined 的值,避免了繁琐的空值检查。
- 日志记录和状态管理:Writer 和 State Monad 可用于在不引入副作用的情况下进行日志记录和状态管理。
λ 演算是什么
答案
λ 演算(Lambda Calculus)是一种形式系统,用于研究函数定义、函数应用和递归。它可以被认为是世界上最小的编程语言。
λ 演算的特点:
- 函数是头等公民:在 λ 演算中,函数可以作为参数传递给其他函数,也可以作为返回值返回。
- 匿名函数:λ 演算使用匿名函数来定义函数。
- 变量替换:λ 演算使用变量替换来执行函数。
λ 演算的实现:
λ 演算可以使用 JavaScript 来实现:
// 变量
const variable = name => ({
type: 'variable',
name
})
// 抽象
const abstraction = (param, body) => ({
type: 'abstraction',
param,
body
})
// 应用
const application = (func, arg) => ({
type: 'application',
func,
arg
})
// 示例
// λx.x
const identity = abstraction(variable('x'), variable('x'))
// (λx.x) y
const applicationExample = application(identity, variable('y'))
λ 演算的实际应用:
- 编程语言设计:λ 演算是许多编程语言的基础,例如 Haskell、Lisp 和 JavaScript。
- 编译器设计:λ 演算可以用于编译器设计,例如将高级语言转换为机器代码。
- 理论计算机科学:λ 演算是理论计算机科学的重要组成部分,例如用于研究可计算性理论。