文章内容输出来源:拉勾教育前端高薪训练营
1. 什么是函数式编程
- 函数式编程,缩写FP,是一种编程范式,也是一种编程风格,亦或是一种思维模式,和面向对象是并列的关系,面向对象是对事物的抽象,函数式是对运算过程的抽象
- 函数式编程不是指的函数Function,而是指的数学中的函数对应关系,比如:y = sin(x),y和x的关系,相同的x下永远会得到相同的y(纯函数)
2. 高阶函数
需要一个函数作为参数,屏蔽细节,不用关心实现细节,可以用来抽象通用问题
2.1 函数作为参数
// 模拟forEach
function forEach (arr, fn) {
for (let i = 0; i < arr.length; i += 1) {
fn(arr[i], i)
}
}
const arr = [1, 3, 5, 7, 9]
// 测试forEach
forEach(arr, function (item, index) {
console.log(item, index)
})
// 模拟filter
function filter (arr, fn) {
let results = [];
for (let i = 0; i < arr.length; i += 1) {
if (fn(arr[i])) {
results.push(arr[i])
}
}
return results
}
// 测试filter
const r = filter(arr, function (item) {
return item % 3 === 0
})
console.log(r)
2.2 函数作为返回值
function makeFn () {
let msg = 'hello world'
return function () {
console.log(msg)
}
}
const fn = makeFn()
fn()
makeFn()() // hello world
// once函数
function once (fn) {
let flag = true
return function () {
if (flag) {
flag = false
return fn.apply(this, arguments)
}
}
}
const r = once(function (arg) {
console.log(arg)
})
r(22) // 22
r(22) // 不执行
r(22) // 不执行
2.3 模拟常用的高阶函数
// 模拟高阶函数 map、every、some
let arr = [1, 2, 3]
const map = (arr, fn) => {
let results = []
for (let value of arr) {
results.push(fn(value))
}
return results
}
const fMap = map(arr, v => v * v)
console.log(fMap)
const every = (arr, fn) => {
let result = true
for (let value of arr) {
result = fn(value)
if (!result) {
break
}
}
return result
}
const fEvery = every(arr, v => v >= 1)
console.log(fEvery)
const some = (arr, fn) => {
let result = false
for (let value of arr) {
result = fn(value)
if (result) {
break
}
}
return result
}
const fSome = some(arr, v => v % 2 === 0)
console.log(fSome)
3. 闭包
闭包就是能够读取其他函数内部变量的函数,内层函数引用了外层函数作用域下的变量,并且内层函数在全局下可访问
function makeSalary (base) {
return function (performance) {
return base + performance
}
}
let salaryLevel1 = makeSalary(12000)
let salaryLevel2 = makeSalary(15000)
console.log(salaryLevel1(3000)) // 15000
console.log(salaryLevel1(5000)) // 17000
console.log(salaryLevel2(5000)) // 20000
4. 纯函数
相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
const arr = [1, 2, 3, 4, 5]
// 纯函数slice
console.log(arr.slice(0, 3))
console.log(arr.slice(0, 3))
console.log(arr.slice(0, 3))
// 不纯的函数splice,改变了原数组
console.log(arr.splice(0, 3))
console.log(arr.splice(0, 3))
console.log(arr.splice(0, 3))
// 纯函数
function getSum (a, b) {
return a + b
}
console.log(getSum(1, 2))
console.log(getSum(1, 2))
console.log(getSum(1, 2))
5. lodash和loadsh/fp
- lodash是一个函数功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法
- loadsh/fp提供了函数式编程模块
- loadsh/fp提供了不可变auto-curried iteratee-first data-last的方法
- lodash是数据优先,函数置后;lodash/fp是函数优先,数据置后
const _ = require('lodash')
const fp = require('lodash/fp')
// lodash方法
const r1 = _.map(['a', 'b', 'c'], _.toUpper)
// lodash/fp方法
const r2 = fp.map(fp.toUpper, ['a', 'b', 'c'])
const r3 = fp.map(fp.toUpper)(['a', 'b', 'c'])
console.log(r1)
console.log(r2)
console.log(r3)
const r4 = fp.flowRight(fp.join('-'), fp.map(fp.toUpper), fp.split(' '))
console.log(r4('hello my world'))
// _和fp map方法的区别
console.log(_.map(['23', '8', '10'], parseInt))
console.log(fp.map(parseInt, ['23', '8', '10']))
// _.map中接受三个参数,value, index/key, collection
// parseInt第二个参数为进制,相当于如下执行,0为10进制,1不存在,2为2进制,最后输出[ 23, NaN, 2 ]
// parseInt('23', 0, ['23', '8', '10'])
// parseInt('8', 1, ['23', '8', '10'])
// parseInt('10', 2, ['23', '8', '10'])
6. 柯里化
为了解决硬编码问题,把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数,可以让函数变得更灵活,使用场景更广泛
const _ = require('lodash')
function getSum (a, b, c) {
return a + b + c
}
const curried = _.curry(getSum)
console.log(curried(1, 2, 3))
console.log(curried(1)(2, 3))
console.log(curried(1, 2)(3))
模拟柯里化
function curry (fn) {
const fnLen = fn.length
return function _curry (...args) {
if (args.length < fnLen) {
return function () {
return _curry(...args.concat(Array.from(arguments)))
}
}
return fn(...args)
}
}
const match = curry(function (reg, str) {
return str.match(reg)
})
const haveSpace = match(/\s+/g)
const haveNumber = match(/\d+/g)
const filter = curry(function (fn, arr) {
return arr.filter(fn)
})
const findSpace = filter(haveSpace)
console.log(findSpace(['d_ddd', 'd dd']))
7. 函数组合
函数组合可以让我们把细粒度的函数重新组合生成一个新的函数,避免写出洋葱代码
作用:
- 单一功能的小函数更好维护
- 通过组合,将单一功能的小函数串联起来,完成复杂的功能
- 复用性更好,硬编码更少
模拟函数组合
const _ = require('lodash')
function compose (...funcs) {
return function (val) {
return funcs.reverse().reduce(function (acc, fn) {
return fn(acc)
}, val)
}
}
const setArrFirstUpper = compose(_.toUpper, log('first'), _.first, log('reverse'), _.reverse)
const r = setArrFirstUpper(['aaa', 'bbb', 'ccc'])
console.log(r)
// 函数组合结合律,任意组合函数组合内的函数,结果都一样
const setArrFirstUpper1 = compose(compose(_.toUpper, log('first'), _.first), log('reverse'), _.reverse)
const r1 = setArrFirstUpper1(['aaa', 'bbb', 'ccc'])
console.log(r1)
8. point free
- point free 是指一种函数式的编程风格,实现方式实际是函数组合
- 不需要指明处理的数据,只需要合成运算过程,需要定义一些辅助的基本运算函数
const fp = require('lodash/fp')
// 非point-free模式
function f (word) {
return word.toLowerCase().replace(/\s+/g, '-')
}
// point-free模式
const pf = fp.flowRight(fp.replace(/\s+/g, '-'), fp.toLower)
const word = 'HELLO woRD'
console.log(f(word))
console.log(pf(word))
9. 函子(Functor)
容器:包含值和值的变形关系(这个变形关系就是函数)
函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)
- 函数式编程的运算不直接操作值,而是由函子完成
- 函子就是一个实现来map契约的对象
- 我们可以把函子想象成一个盒子,这个盒子里封装了一个值
- 想要处理盒子中的值,我们需要给盒子的map方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
- 最终map方法返回一个包含新值的盒子(函子)
几种常见的函子
/* MayBe函子,对外部空值进行处理 */
class MayBe {
static of (value) {
return new MayBe(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
}
isNothing (value) {
return this._value === null || undefined
}
}
console.log(MayBe.of(null).map(x => x.toUpperCase()))
console.log(MayBe.of('null').map(x => x.toUpperCase()))
/* Either函子类似于if...else...,异常会让函数变得不纯,Either函子可以用来做异常处理 */
class Left {
static of (value) {
return new Left(value)
}
constructor (value) {
this._value = value
}
map () {
return this
}
}
class Right {
static of (value) {
return new Right(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return Right.of(fn(this._value))
}
}
function parseJson (str) {
try {
return Right.of(JSON.parse(str))
} catch (e) {
return Left.of({ error: e.message })
}
}
console.log(parseJson('{ name: zs }'))
console.log(parseJson('{ "name": "zs" }'))
/*
IO函子
IO函子中的_value是一个函数,这里把函数作为值来处理
IO函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作(惰性执行),包装当前的操作
把不纯的操作交给调用者来处理
*/
class IO {
static of (value) {
return new IO(function () {
return value
})
}
constructor (fn) {
this._value = fn
}
map (fn) {
return new IO(fp.flowRight(fn, this._value))
}
}
console.log((IO.of(process).map(p => p.execPath))._value())
// IO函子存在嵌套函子调用的不方便,._value()._value()
const readFileIO = function (filename) {
return new IO(function () {
return fs.readFileSync(filename, 'utf-8')
})
}
const print = function (x) {
return new IO(function () {
console.log(x)
return x
})
}
const cat = fp.flowRight(print, readFileIO)
console.log(cat('package.json')._value()._value())
/*
解决IO函子嵌套问题:Monad函子
可以变扁的Pointed函子,IO(IO(x))
一个函子如果具有join和of两个方法并遵守一些定律就是一个Monad
*/
class IOMonad {
static of (value) {
return new IOMonad(function () {
return value
})
}
constructor (fn) {
this._value = fn
}
map (fn) {
return new IOMonad(fp.flowRight(fn, this._value))
}
join () {
return this._value()
}
flatMap (fn) {
return this.map(fn).join()
}
}
const readFileIOMonad = function (filename) {
return new IOMonad(function () {
return fs.readFileSync(filename, 'utf-8')
})
}
const printMonad = function (x) {
return new IOMonad(function () {
console.log(x)
return x
})
}
const catMonad = readFileIOMonad('package.json')
.map(x => fp.toUpper(x))
.flatMap(printMonad)
.join()
console.log('<----------------catMonad---------------->')
console.log(catMonad)
/* Folktale中的compose,curry */
let ff = curry(2, (x, y) => x + y)
console.log(ff(1, 2))
console.log(compose(fp.toUpper, fp.first)(['one', 'two']))
/* Folktale中的task函子,执行异步任务 */
function readFile (filename) {
return task(resolver => {
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) resolver.reject(err)
resolver.resolve(data)
})
})
}
readFile('package.json')
.map(fp.split('\n'))
.map(fp.find(x => x.includes('version')))
.run()
.listen({
onRejected: err => {
console.log(err)
},
onResolved: value => {
console.log(value)
}
})
/* Point函子是实现来of静态方法的函子,of方法是为了避免使用new来创建对象,更深层的含义是of方法用来把值放到上下文Context(把值放到容器中,使用map来处理值),上文的的函子都是Point函子 */