函数式编程的好处
- 函数式编程是伴随着react的流行受到了广泛关注
- 函数式可以抛弃this
- 打包过程可以更好的利用tree shaking过滤无用代码
- 方便测试,方便并行处理
函数式编程的概念
- 把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)
- 函数式编程中的"函数"并不是程序中的函数(方法),而指的是数学中的函数即映射关系
- 相同的输入要得到相同的结果(纯函数)
- 不应该修改输入的值
- 函数式编程用来描述数据(函数)之间的关系
函数是一等公民,高阶函数,闭包
- 函数式一等公民
- 首先函数可以存储在变量中
- 函数可以作为参数传递
- 函数可以作为返回值
在js中函数就是一个普通对象,我们可以把函数存储到变量/数组之中,他还可以作为另一个函数的参数和返回值
-
高阶函数
- 可以把函数作为参数传递给另一个函数
- 可以把函数作为另一个函数的返回值
function once(fn) { let done = false return function() { done = true return fn.apply(this, arguments) } } let pay = once(function(money) { console.log(money) }) pay(199) pay(199) pay(199) // 仅执行一次
- 高阶函数的意义在于抽象屏蔽实现细节,抽象通用问题
// 最直观的就是foreach和for循环 for(let i = 0; i < arr.length; i++) { } foreach(array, item => { })
- 常用高阶函数
- foreach,map,filter,every,some,find.reduce,sort …
-
闭包
- 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域的成员
- 闭包会造成内存泄露,也就是说被引用的变量不会被垃圾回收机制释放
- 闭包的本质:函数在执行的时候会被放到一个执行栈上当函数执行完毕后从执行栈移除,但是堆上的作用域成员因为外部引用不会被释放,因此内部函数仍然可以访问外部函数的成员
function makePower(power) {
return function (num) {
return Math.pow(num, power)
}
}
let power2 = makePower(2)
let power3 = makePower(3)
console.log(power2(3))
console.log(power2(5))
console.log(power3(3)) // 一眼就能看出闭包的好处,减少重复的代码编写
纯函数
- 概念: 相同的输入永远得到相同的输出,而且没有任何副作用的产生,类似数学中的函数(用来描述输入和输出之间的关系)
- 可缓存
- 因为纯函数对相同的输入始终有相同的输出结果,所以可以把结果缓存起来
function getArea (r) { return Math.PI * r * r } let getAreaWithMemoize = _.memoize(getArea) console.log(getAreaWithMemoize(4)) console.log(getAreaWithMemoize(4)) // 直接从缓存中获取结果 console.log(getAreaWithMemoize(4)) console.log(getAreaWithMemoize(4))
// 模拟memoize实现 function memoize(fn) { let cache = {} return function () { let key = JSON.stringify(arguments) cache[key] = cache[key] || fn.apply(fn, arguments) return cache[key] } }
- 可测试
- 纯函数让测试更加方便
- 并行处理
- 在多线程环境下并行操作共享的内存数据可能会出现意外情况
- 纯函数不需要访问共享的内存数据,所以在多线程下可以任意运行纯函数(web worker)
副作用
- 副作用相对于纯函数来说,就是函数依赖外部的状态导致输入和输出的结果并不是确定的,就会带来副作用
- 副作用并不能完全消除,主要来源于配置文件,数据库,获取用户的输入 等等
柯里化
function checkAge(min) {
return function(age) {
return age >= min
}
}
const checkAge18 = checkAge(18)
const checkAge20 = checkAge(20)
- 概念:当一个函数有多个参数时,可以对该函数进行改造,调用一个函数只传递部分参数再返回一个函数,简单的说将一个不确定的函数通过闭包形式转变为一个确定的函数。
- 通过lodash的curry函数可以直接生成柯里化函数
function checkAge(min, age) {
return age >= min
}
const curried = _.curry(checkAge)
const checkAge18 = curried(18)(20)
- 柯里化案例(主要是为了复用)
function match(reg, str) {
return str.match(reg)
}
const _match = _.curry(match)
const noSpace = _match(/\s+/g) //去除所有空白字符
const noNumber = _match(/\d+/g) //去除数字
- 让函数变的更加灵活,让函数的颗粒度更小
- 可以把多元函数转换为一元函数,可以组合使用函数产生强大的功能
函数的组合
- 函数的组合可以让我们把细粒度的函数重新生成一个新的函数
- 如果一个函数要经过多个函数的处理才能得到最终值,这个时候可以把中间过程函数组合成一个函数
- 函数就像数据的管道,函数组合就是把这些管道连接起来(链式调用)
- 可以通过lodash或者ramda组合多个函数
const reverse = arr => arr.reverse() const first = arr => arr[0] const toUpper = str => str.toUpperCase() const fn = _flowRight(toUpper, first, reverse) //从右到左进行调用
- 函数的组合要满足结合律
- 我们既可以把g和h组合,也可以f和g组合,结果都是一样的
let f = compose(f,g,h) let flag = compose(compose(f, g),h) == compose(f, compose(g,h)) // true
Point Free
- Point Free: 我们可以把数据处理的过程定义与数据无关的合成运算,不需要用到代表数据的那个参数,只要简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算。
- 不需要指明处理的数据
- 只需要合成运算过程
- 需要定义一些辅助的基本运算函数
// 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'))
Functor(函子)
- 容器:包含值和值的变形关系就是函数
- 函子:是一个特殊容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形处理)
class Container {
static of (value) {
return new Container(value)
}
constructor (value) {
this._value = value
}
map(fn) {
return Container.of(fn(this._value))
}
}
let r = Container.of(5).map((x) => x + 1).map(x => x * x) // 36
总结
- 从思想出发,核心思想抽象运算过程为函数,面向函数编程。
- 柯里化的使用,函数组合,管道,复用函数。
- 使用好lodash和ramda工具库
- 不要过度追求函数式编程,工具是为了更加便捷的实现,而不是制约。
- 函数式编程也有着很多缺点并且对性能也会有一定的影响。