介绍
本文是 JavaScript 高级深入浅出系列的第八篇
,将认识到 JS 中函数式编程相关知识
正文
函数式编程是 JS 中的一种编程范式
1. JS 中的纯函数
函数式编程中有一个非常重要的概念:纯函数,JS 符合函数式编程范式,所以也有纯函数的概念
- 在 react 开发中纯函数是多次被提及的
- 比如 react 中组件就被要求像是一个纯函数(为什么像,因为还有 class 组件),redux 中有一个 reducer 的概念,也是要求必须是一个纯函数
- 所以掌握纯函数对于理解很多框架的设计是非常重要的
纯函数维基百科定义:
- 在程序设计中,若一个函数符合以下条件,那么这个函数就被称为纯函数:
- 此函数在相同的输入值时,需产生相同的输出
- 函数和输出和输入值以外的其他隐藏信息或状态无关,也和由 I/O 设备产生的外部输出无关
- 该函数不能有语义上可观察的函数副作用,诸如**“触发事件”**,使输出设备输出,或更改输出值以外物件的内容等
简单的理解来说,一个纯函数内部的任何处理都是对于输入参数的处理,不可对函数之外做任何操作
1.1 副作用的理解
- 副作用(side effect)本身是一个医学的概念,比如吃了什么药,本来是为了治病,但是造成了其他的副作用
- 在计算机科学的领域中,副作用泛指在执行函数时,除了返回函数值之外,还对调用函数产生了附加影响,例如修改了全局变量,修改参数或者修改外部的存储
纯函数在执行过程中是不允许出现副作用的:
- 副作用往往是产生 bug 的“温床”
我们为了能够编写健壮的函数,最好让一个函数负责一个功能
1.2 纯函数的例子
Array.prototype.slice
:对数组进行截取,并返回一个截取的新数组(是一个纯函数)Array.prototype.splice
:对数组进行截取,返回截取的新数组,并更改原数组(不是一个纯函数)
// 这个不是一个纯函数,这个是有一点容易混淆的,因为 bar 函数将参数更改了,间接把外部的值也修改了
function bar(info) {
info.age = 20
}
let obj = { age: 18 }
bar(obj)
// 这个函数严格意义上来说也不算是一个纯函数,因为纯函数是要除了 return 之外不能对外界有任何输出
// 但是可以理解为一个纯函数,因为这个函数没有副作用
function printInfo(info) {
console.log(info.name)
}
1.3 纯函数的优势
为什么纯函数式编程中这么重要:
- 可以安心的编写和使用
- 在写的时候保证了纯度,专注于逻辑即可,不用担心是否对外界造成影响
- 在用的时候,确定此函数不会造成副作用,并且确定的输入一定会有确定的输出
- 所有 React 组件都必须像纯函数一样保护它们的 props 不被修改
2. JS 柯里化
柯里化是函数式编程的重要概念。
柯里化维基百科定义:
- 在计算机科学中,柯里化(Currying)是把接收多个参数的函数,变成接收一个单个参数(最初函数的第一个参数)的函数,并且返回接收余下的参数,而且返回结果的新函数的技术
- 柯里化声称“如果你固定某些参数,你将得到接受余下参数的一个函数”
简单来说就是:
- 只传递给函数一部分参数,让它返回一个函数处理剩余的参数。这个过程称为柯里化
// 普通的函数
function sum(x, y, z) {
return x + y + z
}
sum(10, 20, 30)
// 柯里化函数
function sum2(x) {
return function(y) {
return function(z) {
return x + y + z
}
}
}
sum2(10)(20)(30)
简化柯里化代码
const sum2 = x => y => z => x + y + z
2.1 柯里化的作用一:让函数的职责单一
为什么需要柯里化?
- 在函数式编程中,我们往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理。
- 那么我们是否就可以将每次传入的参数在单一的函数中处理,处理完后在下一个函数中再使用处理后的结果
比如上面的例子参数,我们想让 1 函数 + 2,2 函数 * 2,3 函数 ** 2
function sum(x, y, z) {
x = x + 2
return function(y) {
y = y * 2
return function(z) {
z = z * z
return x + y + z
}
}
}
console.log(sum(10)(20)(30))
2.2 柯里化作用二:逻辑复用
// 比如我们有一个 log 函数
function log(date, type, msg) {
console.log(`[${date.getHours()}:${date.getMinutes()}][${type}][${msg}]`)
}
log(new Date(), 'DEBUG', '查找到代码 bug')
log(new Date(), 'DEBUG', '查找到代码 bug2')
log(new Date(), 'DEBUG', '查找到代码 bug3')
// 柯里化的优化
const curryingLOG = date => type => msg =>
console.log(`[${date.getHours()}:${date.getMinutes()}][${type}][${msg}]`)
// 这样可以做到逻辑复用
const DEBUG_LOG = msg => curryingLOG(new Date())('DEBUG')(msg)
DEBUG_LOG('查找到代码 bug')
2.3 自动柯里化函数的实现
前置知识:
function foo(x, y, z, m, n) {}
console.log(foo.length) // 该函数接受参数的个数,结果为 5
实现:
// 需求:传入一个函数,通过调用自定方法获取到柯里化的函数
function add(num1, num2, num3) {
return console.log(num1 + num2 + num3)
}
function fnCurrying(fn) {
return function curried(...args) {
// 1. 当判断传入的参数 >= 需要传的参数 时,直接执行函数
if (args.length >= fn.length) {
return fn.apply(this, args)
} else {
// 2. 没有达到参数时,需要递归检查是否达到了参数的个数
return function(...args2) {
return curried.apply(this, [...args, ...args2])
}
}
}
}
const curriedAdd = fnCurrying(add)
// curriedAdd(10, 20, 30) // 60
// curriedAdd(10, 20)(30) // 60
// curriedAdd(10)(20)(30) // 60
3. 组合函数
组合(compose)函数是在 JS 开发过程中对于函数的一种使用技巧。
- 比如我们现在需要对某个数据进行函数调用,执行函数
fn1
、fn2
,这两个函数依次执行 - 但是如果我们每次都要调用这两个函数,就显得非常重复
- 我们就可以将这两个函数组合起来,调一次就可以依次执行两个函数
- 对于函数的组合,加作组合函数(Compose Function)
function double(n) {
return n * 2
}
function square(n) {
return n ** 2
}
let num = 10
// 需要每次调用两个函数,就很繁琐
num = square(double(num))
// 写一个工具函数
function composeFn(m, n) {
return function(num) {
return m(n(num))
}
}
const fn = composeFn(double, square)
fn(10)
// 一个复杂的案例,不知道具体会传入几个参数
function _componse(...fns) {
fns.map(fn => {
if (typeof fn !== 'function') {
throw new TypeError('Expected arguments are functions')
}
})
return function(...args) {
let result = null
// 这里核心逻辑就是,index = 0 也就是 第一个方法,通过传入的参数来调用函数
// 之后的函数都是通过上一个函数的返回值作为参数去调用
// 使用 call 而不是直接调用,防止隐式或显式修改了 this 指向
fns.map((fn, index) => {
result = index ? fn.call(this, result) : fn.call(this, ...args)
})
return result
}
}
function add(num) {
return num + 1
}
function double(num) {
return num * 2
}
const fn = _componse(add, double)
console.log(fn(2))
总结
本文中,你学到了 3 个知识点:
- 了解到函数式编程范式,并学习了纯函数相关知识
- 了解了柯里化与柯里化的作用,并手写自动柯里化函数
- 了解了组合函数,并手写了组合函数工具函数