我记得我最开始接触到函数式编程是在使用redux的compose函数,当时我看了compose的源码还非常的懵逼。知道后来知道了函数式编程才明白其中奥义。
如果大家有用过redux应该都会很熟悉下面的代码。
let store = createStore(
counter,
compose(applyMiddleware(logger))
);
有些时候在实际开发中只是创建store其实并不满足我们的开发需求这时候compose就可以用来对store进行增强比如添加一个打印日志之类的日志。
官方源码如下:
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
如果大家不了解函数式编程可能就不太清楚上面这个函数的意义, 如果知道了函数式编程在阅读起这些源码应该是非常好理解的。在React以及Vue 3也都使用了函数式编程这种编程范式。所以大家学习函数式编程还是非常有必要的。
什么是函数式编程
函数式编程是一种编程范式,和我们平时最常使用的命令式编程相比较,函数式编程主要是关注于数据的映射。 这里要提的一点是函数式编程中的函数并不是指程序中的函数,而是数学关系中的映射关系.且每一次相同的输入都要得到相同的输出(纯函数)。
函数式编程中的概念
在js中有很多函数概念,如果了解了的话会更利于理解函数式编程。
函数是一等公民
在编程语言中一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量。而在JavaScript中函数也是一等公民。
高阶函数
当函数作为参数传递时或者函数被当作返回值时都被称为高阶函数。
纯函数
用于描述输入和输出之间的关系。同样的输入一定会返回同样的输出。
函数副作用
当函数依赖外部变量会产生函数副作用让函数变的不纯。外部扩展都会产生副作用。
const age = 18
// 这个函数的返回值依赖外部变量age
function checkAge() {
return age > 18
}
副作用会使一个函数无法预测所以应该尽量避免副作用的产生。
闭包
概念: 函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。
个人理解:
当在外部函数(test)的内部调用了内部函数(cc),并且内部函数(cc)引用了外部函数(test)的变量,此时会产生闭包。
闭包用于存储外部函数(test)变量,当内部函数(cc)被返回出去并且被调用时,由于闭包引用了外部函数(test)变量,所以这个变量(a)依然不会被销毁。
未被引用不产生闭包。
函数柯里化
函数柯里化(currying)又称部分求值。一个 currying 的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
function getAge(env, age) {
if (env === 'dev') {
return age > 18
}
if (env === 'prod') {
return age > 25
}
return age > 30
}
// 每次调用都会去传入dev
console.log(getAge('dev', 30))
假设我想一直使用dev的但又不想多次传入dev就可以把函数柯里化后执行
// 用于将函数柯里化
function curry(func) {
return function curryFn(...args) {
if (args.length >= func.length) {
return func(...args)
}
return function(...args2) {
return curryFn(...args.concat(args2))
}
}
}
const getAge = curry(function (env, age) {
if (env === 'dev') {
return age > 18
}
if (env === 'prod') {
return age > 25
}
return age > 30
})
// 这样做可以避免每次调用多传入dev
const getDevAge = getAge('dev')
console.log(getDevAge(30))
// 也可以同时传入多个
console.log(getAge('prod',30))
函数组合
如果仅仅使用纯函数和柯里化非常容易写出洋葱代码:a(b(c()))这样不容易读懂所以需要用到函数组合.
函数组合可以把小颗粒度的函数组合在一起返回一个新的函数,上面提到的compose就是使用的函数组合
function compose(...funcs) {
return function(value) {
return funcs.reduce((prev, fn) => fn(prev), value)
}
}
function toUpper(str) {
return str.toUpperCase()
}
function first(str) {
return str[0]
}
// 从右往左执行,右边函数的返回值为左边函数的参数
console.log(compose(toUpper, first)('hello'))
函子
作用:函子主要用于把副作用控制在可控范围之类
可以把函子理解为一个特殊的容器内部包含有一个value且可以通过map来对这个value进行操作
class Container {
static of (value) {
return new Container(value)
}
constructor(value) {
this.value = value
}
map(func) {
return Container.of(func(this.value))
}
}
Container.of(1)
.map((v) => {
console.log(v)
return v + 1
})
.map((v) => {
console.log(v)
})
函子存在的主要作用就是抽离副作用,让函子去处理异常和IO
Maybe函子
maybe函子在函数处理过程中返回了null会避免null去调用方法而照成的错误
class Maybe {
static of(value) {
return new Maybe(value)
}
constructor(value) {
this.value = value
}
isNothing() {
return this.value === null || this.value === undefined
}
map(func) {
return this.isNothing() ? Maybe.of(null) : Maybe.of(func(this.value))
}
}
const t = Maybe.of(1).map((v) => {
return null
}).map((v) => {
// 如果使用Container会在这里报错
v.split('')
})
console.log(t)
IO函子
IO函数=子可以把不纯的操作抽离到执行时传入
let _ = require('lodash');
class IO {
static of(value) {
return new IO(function() {
return value
})
}
constructor(value) {
this.value = value
}
map(func) {
return new IO(_.flowRight(func, this.value))
}
}
const t = IO.of(process).map(p => p.execPath)
console.log(t.value())
今日学习总结
- 学习了函数式编程概念
- 高阶函数,闭包,函数是一等公民的概念
- 函数柯里化,纯函数,函数组合灯函数式编程基础
- 函子
在学习过程中有写到组合和柯里化的一些原理,但是在实际使用中其实并不需要自己去写。比如说使用现成的函数式编程库:lodash,folktale之类的
let _ = require('lodash');
// 函数柯里化
let abc = (a, b, c) => a + b + c;
let curried = _.curry(abc);
let result = curried(1)(2)(3);
console.log(result);
const { toUpper, first } = require('lodash');
let _ = require('lodash');
// 函数组合
const getFirstUpper = _.flowRight(toUpper, first)
console.log(getFirstUpper('hello'))
如果想要更加系统和详细的学习函数式编程的使用可以参阅书籍:JavaScript函数式编程指南