函数式编程(Functional Programming,FP),FP是编程范式之一,我们常说的编程范式还有面向对象编程、面向过程编程。
面向对象编程的思维方式:把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的联系
函数式编程的思维方式:把现实世界的事物和事件之间的联系抽象到程序世界(对运算过程进行抽象)
程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多输入和输出的函数。
x -> f(联系、映射) -> y, y = f(x)
函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如:y = sin(x),x和y的关系
相同的输入始终要得到相同的输出(纯函数)
函数式编程用来描述数据(函数)之间的映射
MDN First-class Function
在Javascript中函数就是一个普通的对象(可以通过new Function()
),我们可以把函数存储到变量/数组中,它还可以作为另一个函数的参数或者返回值,甚至我们可以在程序运行的时候,通过new Function('alert(1)')
来构造一个新的函数。
// 把函数赋值给变量
let fn = function () {
console.log('hello First-class Function')
}
// 一个示例
const BlogConstroller = {
index (posts) { return Views.index(posts) },
show (posts) { return Views.show(posts) },
create (attrs) { return Db.create(attrs) },
update (posts, attrs) { return Db.update(posts, attrs) },
destroy (posts) { return Db.destroy(posts) },
}
// 优化
const BlogConstroller = {
index: Views.index,
show: Views.show,
create: Db.create,
update: Db.update,
destroy: Db.destroy,
}
高阶函数(Higher-order function)
函数作为参数
// forEach
function forEach(Array, fn){
for(let i = 0; i < Array.length; i++){
fn(Array[i])
}
}
const arr = [1, 2, 3, 4, 5, 6, 7, 8]
forEach(arr, function(item){
console.log(item)
})
// filter
function filter(Array, fn){
let arr = []
for(let i = 0; i < Array.length; i++){
if(fn(Array[i])){
arr.push(Array[i])
}
}
return arr
}
const arr = [1, 2, 3, 4, 5, 6, 7, 8]
let result = filter(arr, function(item){
return item % 2 === 0
})
console.log(result)
function makeFn () {
const msg = "woailili"
return function(){
console.log(msg)
}
}
// const fn = makeFn()
// fn()
makeFn()()
//once
function once (fn) {
let done = false
return function () {
if(!done){
done = true
return fn.apply(this, arguments)
}
}
}
const pay = once(function (money) {
console.log(`支付${money}元`)
})
pay(1)
pay(1)
pay(1)
// once函数是一个高阶函数,它接收一个函数作为参数,作用就是返回一个函数,返回的这个函数不管执行几次,它只会调用一次这个参数函数。
// 一句话说就是,生成一个只会执行一次的函数
函数式编程的核心思想,函数式编程的核心是将运算过程抽象成函数,然后可以在任何使用的情况下复用这些函数。调用的时候可以不去关心它实现的细节,只需要了解自己的目标就可以。
想前面封装的forEach函数,它的作用就是循环一个数组,并按照传入的函数取处理循环的每一个数组元素。我们在调用的时候不需要去关系循环的细节,只需要关系想要对数组进行怎样的处理就可以了。
map
// map
const map = (Array, fn) => {
let arr = []
for(let value of Array){
arr.push(fn(value))
}
return arr
}
const arr = [1, 2, 3, 4, 5, 6, 7, 8]
let result = map(arr, item => item * 2)
console.log(result)
// every
const every = (Array, fn) => {
for(let value of Array){
if(!fn(value)){
return false
}
}
return true
}
const arr = [1, 2, 3, 4, 5, 6, 7, 8]
let result = every(arr, item => item > 2)
console.log(result)
// some
const some = (Array, fn) => {
for(let value of Array){
if(fn(value)){
return true
}
}
return false
}
const arr = [1, 2, 3, 4, 5, 6, 7, 8]
let result = some(arr, item => item > 100)
console.log(result)
闭包(Closure):函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。
可以在另一个作用域中调用一个函数(1)的内部函数(2)并访问到该函数(1)的作用域中的成员。
闭包的本质:函数在执行的时候会放到一个执行栈上,当函数执行完之后从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此当内部函数调用的时候依然可以访问到外部函数的成员。
function makeFn () {
const msg = "woailili"
return function(){
console.log(msg)
}
}
// const fn = makeFn()
// fn()
makeFn()()
//once
function once (fn) {
let done = false
return function () {
if(!done){
done = true
return fn.apply(this, arguments)
}
}
}
const pay = once(function (money) {
console.log(`支付${money}元`)
})
pay(1)
pay(1)
pay(1)
const makePower = function (power) {
return function(number){
return Math.pow(number, power)
}
}
// 求平方的函数
const power2 = makePower(2)
// 求立方的函数
const power3 = makePower(3)
console.log(power2(2))
console.log(power3(2))
纯函数:相同的输入永远得到相同的输出,而且没有任何可观察的副作用。
纯函数就类似于数学中的函数(用来描述输入和输出之间的关系),y=f(x)。
lodash是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法。
数组的 slice 和 splice 分别是纯函数和不纯的函数。
slice 返回数组中的指定部分,不会改变原数组。
splice 对数组进行操作返回该数组,会改变原数组。
函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)
我们可以把一个函数的执行结果交给另一个函数去处理。
https://www.lodashjs.com/
yarn init -y
yarn add lodash --save
const _ = require('lodash')
const arr = [1, 'a', 3, 'b', 5, 'c']
console.log(_.first(arr))
console.log(_.last(arr))
console.log(_.toUpper(_.last(arr)))
console.log(_.reverse(arr))
const result = _.each(arr, (item, index) => {
console.log(index, item)
})
console.log(result)
console.log(_.includes(arr, 1))
console.log(_.findIndex(arr, item => {
return item === 1
}))
可缓存(可以提高函数的行能)
const _ = require('lodash')
function getArea (r) {
console.log(r)
return Math.PI * r * r
}
const getAreaWithMemory = _.memoize(getArea)
console.log(getAreaWithMemory(2))
console.log(getAreaWithMemory(2))
console.log(getAreaWithMemory(2))
//模拟实现memoize纯函数
/**
* 分析memoize函数接收一个函数,并返回一个新的函数,
* 这个新的函数可以将传进来的函数的结果缓存起来。
*/
const memoize = function (fn) {
let cache = {}
return function () {
const key = JSON.stringify(arguments)
cache[key] = cache[key] || fn.apply(fn, arguments)
return cache[key]
}
}
可测试
因为单元测试就是在断言程序的执行结果,而纯函数始终都有输入和输出,所以所有的纯函数都是可以测试的函数。
并行处理
因为纯函数是一个封闭的空间,它只依赖于自己的参数,不需要访问共享的内存数据,所以在并行的情况下依然可以任意的运行纯函数。
ES6以后新增的webwork可以开启多个线程。但是在js多数的情况下还是以单线程为主。
// 不纯的
let mini = 18
function checkAge (age) {
return age >= mini
}
//纯的(有硬编码,可以通过函数柯里化解决)
function checkAge (age) {
let mini = 18
return age >= mini
}
副作用让一个函数变得不纯(如上例),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态,就无法保证输出相同,就会带来副作用。
副作用来源:
所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患,给程序带来不确定性,但是副作用不可能完全禁止,但要尽可能控制他们在可控范围内发生。
// 普通函数
// function checkAge (mini, age) {
// return age >= mini
// }
// console.log(checkAge(18, 20))
// console.log(checkAge(18, 22))
// console.log(checkAge(18, 25))
// console.log(checkAge(18, 26))
// mini 为18可能会经常用到,我已我们就可以使用函数式编程封装一下这个映射过程的一部分
// 函数柯里化
// const checkAge = mini => {
// return function (age) {
// return age >= mini
// }
// }
// 箭头函数最简版
const checkAge = mini => (age => age >= mini)
const checkAgeHas18 = checkAge(18)
console.log(checkAgeHas18(20))
console.log(checkAgeHas18(22))
console.log(checkAgeHas18(25))
柯里化:
_.curry(func)
功能:创建一个函数,该函数接收一个或多个func的参数,如果func所需要的参数都被提供,则执行func并返回执行的结果。否则继续返回该函数并等待接收剩余的参数。
参数:需要柯里化的函数
返回值:柯里化后的函数
const _ = require('lodash')
// lodash函数柯里化方法
const getSum = (a, b, c) => 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))
const _ = require('lodash)
// 柯里化案例
const match = _.curry((reg, string) => string.match(reg))
const hasSpace = match(/\s+/g)
const haveNumber = match(/\d+/g)
// console.log(hasSpace("hello world")) // [ ' ' ]
// console.log(haveNumber("hello world1")) // [ '1' ]
// 案例值filter
const filter = _.curry((fn, array) => array.filter(fn))
const findSpace = filter(hasSpace)
console.log(filter(hasSpace, ['John Connor', 'John_Donne']))
console.log(filter(findSpace, ['John Connor', 'John_Donne']))
console.log(findSpace(['John Connor', 'John_Donne']))
// 柯里化的实现原理
function getSum (a, b, c) {
return a + b + c
}
const curriedGetSum = curry(getSum)
console.log(curriedGetSum(1, 2, 3))
console.log(curriedGetSum(1)(2, 3))
console.log(curriedGetSum(1, 2)(3))
function curry (fn) {
return function curriedFunc (...args) {
if(args.length < fn.length){
// return function (...args1) {
// return curriedFunc(...[...args,...args1])
// }
return function () {
return curriedFunc (...args.concat(Array.from(arguments)))
}
}
return fn(...args)
}
}
纯函数和柯里化很容易写出洋葱代码 h(g(f(x)))
函数组合可以让我们把细粒度的函数重新组合生成一个新的函数。
下面这张图表示程序中使用函数处理数据的过程,给fn函数输入一个参数a,返回结果b。可以想想a数据通过一个管道得到一个b数据。
当fn函数比较复杂的时候,我们可以把函数fn拆分成多个小函数,此时多了中间运算过程中产生的m和n。
下面这张图可以想象成把fn这个管道拆分成了3个管道f1,f2,f3,数据a通过管道f3得到结果m,m通过管道f2得到结果n,n通过管道f1得到结果b。
组合函数会忽略小管道处理数据的过程中产生的中间值,只关心开始的输入和最终的输出。
fn compose(f1, f2, f3)
b = fn(a)
函数组合(compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数。
// 函数组合演示
function compose (f, g) {
return function (value) {
return f(g(value))
}
}
const composeFn = compose(_.first, _.reverse)
console.log(composeFn([1, 2, 3, 4])
)
//lodash的组合函数
const _ = require("lodash")
const reverse = array => array.reverse()
const first = array => array[0]
const toUpper = string => string.toUpperCase()
const composeFn = _.flowRight(toUpper, first, reverse)
console.log(composeFn(['one', 'two', 'three']))
const reverse = array => array.reverse()
const first = array => array[0]
const toUpper = string => string.toUpperCase()
// const compose = function (...args) {
// return function (value) {
// return args.reduceRight(function (acc, fn) {
// return fn(acc)
// }, value)
// }
// }
// 箭头函数的写法
const compose = (...args) => value => args.reduceRight((acc, fn) => fn(acc), value)
const composeFn = compose(toUpper, first, reverse)
console.log(composeFn(['one', 'two', 'three']))
// 结合律(associativity)
let f = compose(f, g, h)
let associative = compose(compose(f, g), h) == compose(f, compose(g, h))
// true
// NEVER SAY DIE ---> never-say-die
const _ = require('lodash')
let str = 'NEVER SAY DIE'
// // 先柯里化(函数组合只能组合一元函数)
// const split = splitChar => str => _.split(str, splitChar)
// const splitStrWidthSpace = split(' ')
// const join = joinChar => array => _.join(array, joinChar)
// const joinArrayWithHyphen = join('-')
// // 函数组合
// const formattingStr = _.flowRight(_.toLower, joinArrayWithHyphen, splitStrWidthSpace)
// console.log(formattingStr(str))
// 老师的思路(姜还是老的辣一点点)
const split = _.curry((splitChar, str) => _.split(str, splitChar))
const join = _.curry((joinChar, array) => _.join(array, joinChar))
const formattingStr = _.flowRight(_.toLower, join('-'), split(' '))
console.log(formattingStr(str))
函数组合不会对外暴露计算的中间值,在外面并不能打印中间值来检查在处理数据的那一步出了问题。
可以定义一个打印中间值的一元函数,将这个函数加到组合函数中,打印我们想要的组合函数中间值。
const log = middleValue => {
console.log(middleValue)
return middleValue
}
为了清楚的知道打印的中间值是函数组合中的哪一步的中间值,可以给 log 函数添加一个标签参数,调用的时候给打印的中间值添加一个标签。
const trace = _.curry((tag, middleValue) => {
console.log(tag, middleValue)
return middleValue
})
前面的练习中可以看到,在使用 lodash 函数进行函数组合处理的时候,很多情况都需要对lodash的二次封装,就很烦。
lodash 的 fp 模块提供了实用的对函数式编程友好的方法
// NEVER SAY DIE ---> never-say-die
const _ = require('lodash')
const fp = require('lodash/fp')
const formattingStr = _.flowRight(_.toLower, fp.join('-'), fp.split(' '))
let str = 'NEVER SAY DIE'
console.log(formattingStr(str))
const _ = require('lodash')
console.log(_.map(['23', '8', '10'], parseInt)) // [ 23, NaN, 2 ]
原因:
map:
Creates an array of values by running each element in collection through iteratee. The iteratee is invoked with three arguments: (value, index|key, collection).
parseInt:
function parseInt(s: string, radix?: number): number
Converts a string to an integer.
@param
s — A string to convert into a number.
@param
radix — A value between 2 and 36 that specifies the base of the number in numString. If this argument is not supplied, strings with a prefix of ‘0x’ are considered hexadecimal. All other strings are considered decimal.
const fp = require('lodash/fp')
console.log(fp.map(parseInt, ['23', '8', '10'])) // [ 23, 8, 10 ]
原因:
首先,Point Free是一种编程风格。
**Point Free:**我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这个模式之前我们需要定义一些辅助的基本函数。
_.flowRight(_.toLower, fp.join('-'), fp.split(' '))
函数式编程的核心,把运算过程抽象成函数。Point Free模式就是把抽象出来的函数合成一个新的函数,合成的过程不涉及具体数据,只关心运算过程,也是一个抽象的过程。
// Hello World ----> hello_world
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
console.log(f('Hello World'))
在合成 f 的时候并不需要关系传入的具体数据是什么,只需要关心运算过程,然后把每一步的运算过程组合起来就可以了。比如,第一步将字符串转换成小写,然后将字符串中的空格替换成下划线。
// 把一个字符串中的首字母提取并转换成大写,使用.作为分隔符
// world wild web ==>W.W.W
const fp = require('lodash/fp')
// const f = fp.flowRight(fp.toUpper, fp.join('.'), fp.map(fp.first), fp.split(' '))
const f = fp.flowRight(fp.join('.'), fp.map(fp.flowRight(fp.toUpper, fp.first)), fp.split(' '))
console.log(f('world wild web'))