1. 函数式编程
函数式编程(Function Programming)是编程范式之一 (其中常说的还有面向对象编程、面向过程编程)
-
面向对象编程: 把现实世界中的事物抽象成程序中的对象和类,利用继承、多态和封装来表现事物事件之间的联系
-
函数式编程: 把现实世界中的事物和事件之间的联系抽象到程序世界(通过对运算过程进行抽象)
- 程序的本质:是根据通过某种运算获取相应输出
- x -> f(联系、映射) -> y ===> y = f(x)
- 函数式编程中的函数指的不是程序中的函数(方法),而且是数据之间的映射关系(例如数学中的 y = sin(x))
- 相同输入要始终得到相同输出
- 函数式编程用来描述数据(函数)之间的映射。
// 非函数式 let num1 = 1 let num2 = 2 let sum = num1 + num2 console.log(sum) // 函数式编程 function add (a, b) { return a + b } let sun = add(1, 2) console.log(sum)
高阶函数
什么是高阶函数
-
高阶函数(Highter order function)
- 可以把函数作为参数传递给另一个函数
- 可以把函数作为另一个函数的返回值
// 函数作为参数 function forEach(arr, fn) { for(let i = 0; i < arr.length; i++) { fn(arr[i]) } } // 函数作为返回值 function makeFn () { let msg = '测试' return function () { console.log(msg) } } // 例子 ---- 函数只执行一次 function once (fn) { let done = false return function () { if (!done) { return fn.apply(this, arguments) } } } -
高阶函数的意义
- 抽象可以帮我们屏蔽细节,只需要关注目标
- 高阶函数是用来抽象通用的问题
纯函数
一个函数,如果符合以下两个特点,那么他就可以称之为: "纯函数"
- 对于相同输入,永远会得到相同的输出(不会依赖外部状态,只根据自身函数)
- 没有任何可观察到的副作用(不会对外部的状态进行修改)
纯函数的优点
- 可缓存性
- 可测试性
- 纯函数让测试更方便
- 并行处理
- 在多线程环境下并行操作共享的内存数据很可能会出现意外情况
- 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意执行纯函数
// 可缓存性 -- 因为纯函数对相同的输入始终会有相同的结果,所以可以进行缓存起来
// 模拟lodash中的 memoize 函数(记忆函数)
function getArea(r) {
return Math.PI * r * r
}
function memoize (fn) {
let cache = {}
return function () {
let key = JSON.stringify(arguments)
cache[key] = cache[key] || fn.apply(fn, arguments)
return cache[key]
}
}
副作用
- 对于相同输入,永远会得到相同的输出,没有任何可观察到的
副作用的意思是,这个函数的运行,不会修改外部的状态。
// 不纯的函数
let greeting = 'Hello'
function greet (name) {
return greeting + ' ' + name
}
console.log(greet('World')) // Hello World
// 修改之后
function greet (greeting, name) {
return greeting + ' ' + name
}
console.log(greet('Hi', 'Savo')) // Hi Savo
副作用让一个函数变得不纯,如果一个函数依赖外部的状态就无法保证输入相同,输出相同。
副作用的来源:
- 配置文件
- 数据库
- 获取用户的输入
- ......
函数的柯里化
- 当一个函数有多个参数的时候,先传递一部分参数调用它(这部分参数以后永远不变)
- 然后返回一个新函数去接受剩余的参数,然后返回结果。
// 实例
function checkAge (age) {
let min = 18
return age >= min
}
// 变成纯函数
function checkAge (age, min) {
return age >= min
}
// 柯里化
function makeCheckAge (min) {
return function (age) {
return age >= min
}
}
// 柯里化的ES6写法
let checkAge = min => (age => age => min) // 建议还是使用 es5 写法,通俗易懂。
-
Lodash 中的柯里化
lodash.curry(fn)- 功能:创建一个新的函数,该函数接受一个或者多个
fn的参数,如果 fn 所有的参数都已经被提供,则函数立即执行并且返回执行结果,否者继续等待剩余参数。 - 参数: 需要被柯里化的函数
- 结果: 返回柯里化之后的新函数
const _lodash = require('lodash') function getSum (a, b, c) { return a + b + c } // 柯里化的使用 const curried = _lodash.curry(getSum) // 输入全部参数,直接执行并且获得结果 curried(1, 2, 3) // 6 // 输入部分参数,返回一个新的函数,等待剩余参数,然后再执行。 const getCurried = curried(1, 2) getCurried(3) // 6 getCurried(10) // 13- 柯里化的手写实现
function curry (fn) { // 返回柯里化过后的一个新函数 return function curryFn (...args) { // 判断fn形参和curryFn实参的个数对比 if (fn.length > args.length) { // 如果实参小于形参个数,要返回一个新函数 return function () { return curryFn(...(args.contat(Array.form(arguments)))) } } else { return fn(...args) } } } -
总结
- 柯里化可以让我们给函数传递较少的参数得到一个已经记住了某些固定参数的新函数
- 柯里化是对函数参数的一种缓存(使用了闭包进行缓存)
- 可以将多元函数转换成一元函数,可以组合使用函数,实现更强大的功能。
- 可以让函数变得更加灵活。
函数组合
-
函数组合(compose): 如果一个函数需要通过多个函数的处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数。
- 函数就像数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
- 函数组合执行顺序默认为从右到左执行
// 函数组合的演示
function compose (fn1, fn2) {
return function (value) {
// 执行顺序从右往左执行
return fn1(fn2(value))
}
}
// 获取数组中的最后一个值
function reverse(array) {
return array.reverse()
}
// 获取第一个值
function first(array) {
return array[0]
}
const last = compose(first, reverse)
console.log(last([1, 2, 3, 4]))
-
lodash 中的组合函数
- lodash 中组合函数
flow()和flowRight()都可以组合多个函数 flow()是从左往右执行flowRight()是从右往左执行
- lodash 中组合函数
-
组合函数的原理模拟
// 模拟 lodash 中的 flowRight 函数
function flowRight(...args) {
return function (value) {
// 返回一个最终的结果值
// 从右到左去执行参数内的额每一个函数
// 所有第一步需要翻转args --> 利用函数的reduce函数
return args.reverse().reduce(function (acc, fn) {
return fn(acc)
}, value)
}
}
函数结合律
-
函数的组合要满足结合律
- 即我们可以先将 f 和 g 组合起来然后和 h 组合 和 先将 g 和 h 组合起来然后和 f 组合 结果是一样的
let l = compose(f, g, h) compose(compose(f, g), h) == compose(f, compose(g, h))
Functor(函子)
为什么要学函子?
已经学习过了函数式编程的一些基础,但是并不知道如何在函数式编程中把副作用控制在可控的范围之内、异常处理、异步处理等。。。