# 函数式编程的特性
- 函数是一等公民
- 高阶函数
- 闭包
# 函数是一等公民
- 函数可以存储在变量或数组,对象中等
- 函数可以作为参数进行传递
- 函数可以作为返回值
在 Javascript 中,函数就是一个普通对象。因此可以把函数存储在变量或数组,对象中等。可以作为参数或返回值。
函数是一等公民,是学习高阶函数,函数柯里化的基础。
# 高阶函数
什么是高阶函数?
- 把函数作为一个参数传递给另一个函数
- 把函数做为另一个函数的返回结果
# 函数作为参数
函数作为参数的好处:
- 通过将函数作为参数,使用回调函数可以处理自定义逻辑,使函数更加灵活
- 调用 forEach 函数,屏蔽了内部实现细节
- forEach 函数名称更加语义化
// 模拟forEach 遍历数组
function forEach(array,fn){
for (let i = 0; i < array.length; i++){
fn(array[i])
}
}
//模拟filter 过滤数组
function filter(array, fn) {
let result = [];
for (let i = 0; i < array.length; i++) {
let item = array[i];
if (fn(item)){
result.push(item)
}
}
return result;
}
// 1 2 3 4
forEach([1, 2, 3, 4], function (item) {
console.log(item)
})
const result = filter([1, 2, 3, 4], function (item) {
return item > 2;
})
// [3,4]
console.log(result)
# 函数作为返回值
// 函数作为返回值
// 避免重复调用多次pay方法
function once(fn){
let done = false;
return function(){
if(!done){
done = true;
return fn.call(this,...arguments)
}
}
}
// 支付5
const pay = once(function(value){
console.log(`支付${value}`)
})
pay(5)
pay(5)
pay(5)
pay(5)
# 使用高阶函数的意义
- 高阶函数可以抽象通用的逻辑
- 抽象可以帮助我们屏蔽实现细节,只需要关注最终目标与结果
- 通过将函数作为参数,使用回调函数可以处理自定义逻辑,使函数更加灵活
# 常用的高阶函数
- forEach
- map
- filter
- every
- some
- find/findIndex
- reduce
- sort
- ...
/**
*
* @param {*} array
* @param {*} fn
* map模拟实现 遍历数组 对数组元素进行相关操作 返回一个新的数组
*/
function map(array, fn){
let result = [];
for (let i = 0; i < array.length;i++){
result.push(fn(array[i]));
}
return result;
}
const result1 = map([1, 2, 3, 4],function(item){
return item*2
})
// [ 2, 4, 6, 8 ]
console.log(result1);
/**
*
* @param {*} array
* @param {*} fn
* 模拟every实现 遍历数组 如果数组中的每个元素都匹配条件 则返回true 否则返回false
*/
function every(array, fn) {
let flag = true;
for (let i = 0; i < array.length; i++) {
// 条件不成立
if(!fn(array[i])){
flag = false;
break;
}
}
return flag;
}
const result2 = every([1, 2, 3, 4], function (item) {
return item >= 1;
})
// true
console.log(result2);
/**
*
* @param {*} array
* @param {*} fn
* 模拟some实现 遍历数组 如果数组中有一个元素匹配条件 则返回true 如果都不匹配则返回false
*/
function some(array, fn) {
let flag = false;
for (let i = 0; i < array.length; i++) {
// 条件成立
if (fn(array[i])) {
flag = true;
break;
}
}
return flag;
}
const result3 = some([1, 2, 3, 4], function (item) {
return item >= 4;
})
// true
console.log(result3);
# 闭包
- 闭包有两部分组成,一个是当前的执行上下文A,一个是在该执行上下文中创建的函数B
- 当B执行的时候引用了当前执行上下文A中的变量就会产出闭包
- 当一个值失去引用的时候就会会标记,被垃圾收集回收机回收并释放空间
- 闭包的本质就是在函数外部保持内部变量的引用,从而阻止垃圾回收
function one() {
var a = 1;
var b = 2;
function two() {
var c = 3;
// 引用了外部变量
console.log(a, c);
}
return two;
}
let two = one();
two(); //1,3
# 纯函数
函数式编程中的函数指的是 纯函数。
# 纯函数的概念
- 函数的返回值只与参数有关,相同的输入产生相同的输出。并且不会对外部产生副作用
- 纯函数类似数学中的函数(用来描述输入与输出的关系)。y = f(x)
- Lodash 是一个纯函数的功能库,提供了一系列对数组,对象,函数等的操作方法
- 数组中的 slice 方法是纯函数,splice 不是纯函数
# 纯函数的好处
- 可缓存:相同的输入产生相同的输出,所以可以把纯函数的结果缓存起来
/**
*
* @param {*} fn
* 模拟 Lodash memoize方法实现 将结果缓存起来
*/
function memory(fn){
let cache = {};
return function(){
const key = JSON.stringify(arguments); // 获取参数并转换成字符串
cache[key] = cache[key] || fn.call(fn, ...arguments); // 缓存计算结果
return cache[key]; // 返回结果
}
}
function getArea(r){
console.log("触发getArea")
return Math.PI * r * r;
}
const getAreaWithMemeoy = memory(getArea);
// 触发一次 getArea 执行
getAreaWithMemeoy(4)
getAreaWithMemeoy(4)
getAreaWithMemeoy(4)
- 可测试:纯函数始终有输入和输出,相同的输入产生相同的输出。单元测试就是断言函数的执行结果,所以纯函数是可测试的。
- 方便并行处理
- 在多线程情况下并行操作共享的内存数据(如:全局变量),变量的值不确定,很可能出现意外的情况
- 纯函数只依赖参数,不需要访问共享的数据,所以在并行环境下可以任意运行纯函数(web worker)
# 副作用
/**
*
* @param {*} age
* 不是纯函数 外部变量 mini 会影响函数的执行结果
*/
function checkAge(age) {
return age > mini;
}
/**
*
* @param {*} age
* 是纯函数 通过硬编码实现,可以通过函数柯里化进行优化
*/
function checkAge2(age) {
const mini = 12;
return age > mini;
}
副作用 使函数不纯,纯函数始终接收相同的输入产生相同的输出。如果函数依赖外部变量,就无法保证输出相同,就会带来 副作用 。
# 副作用的来源
- 配置文件
- 数据库
- 获取用户输入数据
- ...
所有外部交互都可能产生 副作用 , 副作用 使方法的通用性下降,不利于程序的扩展以及复用。同时 副作用 也会给程序带来安全隐患,带来不可确定性。但是 副作用 不可能完全消除,需尽可能控制 副作用 在可控范围之类产生。
# 函数柯里化
# 函数柯里化的概念
是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
- 先传递一部分产生调用它(这部分参数以后不变)
- 然后返回接受余下的参数而且返回结果的新函数
使用函数柯里化解决上面硬编码的问题。
/**
*
* @param {*} mini
* 是纯函数 通过函数柯里化进行实现
*/
function checkAge(mini) {
return function (age){
return age > mini;
}
}
const checkAge12 = checkAge(12);
console.log(checkAge12(11))
# Lodash 中的柯里化函数
- _.curry(func, [arity=func.length])
- 创建一个函数,该函数接收 func 的参数,要么调用func返回的结果,如果 func 所需参数已经提供,则直接返回 func 所执行的结果。或返回一个函数,接受余下的func 参数的函数,可以使用 func.length 强制需要累积的参数个数。
- 参数:需要柯里化的函数
- 返回值:柯里化后的函数
var abc = function(a, b, c) {
return [a, b, c];
};
var curried = _.curry(abc);
curried(1)(2)(3);
// => [1, 2, 3]
curried(1, 2)(3);
// => [1, 2, 3]
curried(1, 2, 3);
// => [1, 2, 3]
const match = _.curry(function (reg, str){
return str.match(reg)
});
// 匹配字符串有空格
const haveSapce = match(/\s+/g);
// 匹配字符串有数字
const haveNumber = match(/\d+/g);
# 柯里化函数模拟实现
/**
*
* @param {*} fn
* 模拟 curry 函数实现
*/
function curry(fn){
// ...arg 保存上次调用的参数
return function curriedFn(...arg){
// 判断实参和形参的个数
// fn.length 需要柯里化的函数的形参的个数
if (arguments.length < fn.length){
// 实参个数小于形参个数 需要返回一个新的函数接收剩余的参数
return function(){
// arguments 保存本次调用的参数 从第二次开始
// arguments 是伪数组,需要转成数组
// 拼接每次调用的参数 执行柯里化函数
return curriedFn(...arg.concat(Array.from(arguments)))
}
}
// 实参个数大于等于形参个数 执行需要柯里化的函数 并传递参数 arg
return fn.call(fn, ...arg)
}
}
function getSum(a, b, c){
return a + b + c;
}
var curried = curry(getSum);
console.log(curried(1, 2, 3)); // 6
console.log(curried(1)(2)(3)); // 6
console.log(curried(1)(2, 3)); // 6
console.log(curried(1, 2)(3)); // 6
# 函数柯里化总结
- 函数柯里化让我们给函数传递较少参数,得到一个记住某些固定参数的新函数
- 通过闭包,对函数参数进行缓存
- 让函数变得更灵活,颗粒度更小
- 可以把多元函数(多个参数的函数)转成一元函数,可以通过组合生成功能更佳强大的函数
- 可以解决函数硬编码相关问题
# 函数组合
- 纯函数和函数柯里化容易写出洋葱代码 h(g(f(x)))
- 获取数组中最后一个元素再转换为大写字母
_.toUpper(_.first(_.reverse(array)))
- 获取数组中最后一个元素再转换为大写字母
- 函数组合可以避免洋葱代码的出现,可以让我们把细粒度的函数重新组合成一个新的函数
# 数据的管道
给fn函数传入参数a,返回结果b,可以想象a数据通过一个管道得到了数据b。
当fn函数比较复杂时,我们可以把fn函数拆解为几个小的函数。此时多了中间过程产生的m和n。
可以想象把 fn 这个管道拆解 f1,f2,f3。数据a通过管道f3得到结果m,m再通过管道f2得到结果n,n再通过管道f1得到最终结果b。
- fn = compose(f1,f2,f3)
- b = fn(a)
# 函数组合概念
如果一个数据需要经过多个函数处理才能得到最终结果,这个时候可以把中间过程的函数合并成一个函数。同时通过灵活组合不同函数,可以实现不同的功能。
组合的方式就是抽象单一功能的函数,然后再组成复杂功能。这种方式既锻炼了你的抽象能力,也给维护带来巨大的方便。
- 函数就像是数据的管道,函数组合就是将这些管道连接起来,让数据穿过多个管道形成最终的结果
- 函数组合默认 从右向左执行
/**
*
* @param {*} f
* @param {*} g
* 函数组合简单演示
* 翻转数组 然后返回数组第一个元素
*/
function compose(f, g){
return function(x){
return f(g(x))
}
}
function first(array){
return array[0];
}
function reverse(array) {
return array.reverse();
}
const composeFn = compose(first, reverse);
console.log(composeFn([1, 2, 3, 4])) //4
# Lodash 中的组合函数
- .flow([funcs])**和 **.flowRight([funcs]) 都是组合函数,可以组合多个参数
- _.flow([funcs]) 从左往右执行
- _.flowRight([funcs]) 从右往左执行,使用频率更高
function square(n) {
return n * n;
}
// 先执行 _.add,再执行square
var addSquare = _.flow([_.add, square]);
addSquare(1, 2);
// => 9
function square(n) {
return n * n;
}
// 先执行 _.add,再执行square
var addSquare = _.flowRight([square, _.add]);
addSquare(1, 2);
// => 9
# 组合函数模拟实现
/**
*
* @param {...any} args
* 模拟 flowRight 实现
*/
function flowRight(...args) {
return function (value) {
// 翻转args数组
// 确保从右往左执行函数 并将每次的结果 作为参数传递给下一个函数
return args.reverse().reduce((result, currentFn) => {
return currentFn(result);
}, value)
}
}
var str = 'function program'
function stringToUpper(str) {
return str.toUpperCase()
}
function stringReverse(str) {
return str.split('').reverse().join('')
}
var toUpperAndReverse = flowRight(stringReverse, stringToUpper)
console.log(toUpperAndReverse(str))
function stringToArray(str) {
return str.split('')
}
var toUpperAndArray = flowRight(stringToArray, stringToUpper)
console.log(toUpperAndArray(str))
上面的代码,我们可以通过 flowRight 组合不同的函数,生成各种复杂的逻辑,来满足不同的业务需求。可以看到,组合的方式就是抽象单一功能的函数,然后再组成复杂功能。这种方式既锻炼了你的抽象能力,也给维护带来巨大的方便。
# 函数组合满足结合律
我们即可以把 g 和 h 组合,也可以把 f 和 g 组合,结果是一样的。
flowRight(flowRight(f, g), h) = flowRight(f, flowRight(g, h)) = flowRight(f, g, h)
function first(array) {
return array[0];
}
function reverse(array) {
return array.reverse();
}
function toUpper(str) {
return str.toUpperCase()
}
/**
*
* @param {...any} args
* 模拟 flowRight 实现
*/
function flowRight(...args){
return function(value){
// 翻转args数组
// 确保从右往左执行函数 并将每次的结果 作为参数传递给下一个函数
return args.reverse().reduce((result, currentFn)=>{
return currentFn(result);
}, value)
}
}
// 翻转数组 获取第一个元素 然后转换成大写字母
const composeFn1 = flowRight(toUpper,first, reverse);
const composeFn2 = flowRight(flowRight(toUpper, first), reverse);
const composeFn3 = flowRight(toUpper, flowRight(first, reverse));
console.log(composeFn1(['a','b', 'c', 'd'])) // 'D'
console.log(composeFn2(['a', 'b', 'c', 'd'])) // 'D'
console.log(composeFn3(['a', 'b', 'c', 'd'])) // 'D'
# 如何调试组合函数
function logger(value){
console.log(value);
return value;
}
const composeFn1 = flowRight(toUpper, logger, first, logger, reverse);
console.log(composeFn1(['a','b', 'c', 'd']))
// [ 'd', 'c', 'b', 'a' ]
// d
// D
优化打印日志
/**
* 通过函数柯里化返回一个新的函数
* 可以通过 tag 更加清晰的描述打印信息及流程
*/
const trace = curry((tag, value) =>{
console.log(tag, value);
return value;
})
const composeFn1 = flowRight(toUpper, trace('first之后'), first, trace('reverse之后'), reverse);
console.log(composeFn1(['a', 'b', 'c', 'd']))
// reverse之后 [ 'd', 'c', 'b', 'a' ]
// first之后 d
// D
# Lodash 中的函数编程模块
当我们使用 函数组合 解决问题时,会使用 Lodash 中提供的一些方法。但是如果这些方法有 多个参数 的时候,我们需要对这些方法进行 柯里化 处理,需要重新包装一些方法,满足 函数组合 的要求(接收一个参数),比较麻烦。
Lodash 中的函数编程模块(Function Program),提供了对 函数式编程 友好的方法。
- 这些方法是 不可变的,而且是已经 柯里化 的
- 如果一个方法的参数是函数,那么 函数优先,并且 数据滞后
- 先传递函数,再传递数据
Lodash 模块中的方法一般是先传递数据,再传递函数。Lodash 中的函数编程模块,提供的方法是先传递函数,再传递数据。
# Point Free
在函数式编程的世界中,有这样一种很流行的编程风格。这种风格被称为 tacit programming,也被称作为 point-fre,point 表示的就是 形参,意思大概就是 没有形参 的编程风格。
我们可以把数据的处理过程定义成与数据无关的合成过程,不需要代表数据的参数。只要把简单的运算步骤合成在一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
- 不需要指明处理的数据,没有形参
- 只需要组合成运算过程
- 需要定义一些辅助的基本运算函数
使用函数组合解决问题,就是一种 pointfree 模式。
有参的函数的目的是得到一个结果,而 pointfree 的函数的目的是得到另一个函数。
// 这就是有参的,因为 word 这个形参
var snakeCase = word => word.toLowerCase().replace(/\s+/ig, '_');
// 这是 pointfree,没有任何形参
// 函数组合合成为一个新的函数 不关心数据
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
函数式编程 的核心是将 运算过程 抽象为 函数,pointfree 模式是将抽象出来的函数再合成为一个新的函数。这个合成的过程又是一个抽象的过程,这个抽象的过程中是不需要关心数据的。
const fp = require('lodash/fp');
// 把字符串的首字母提取,并转换成大写,然后使用. 拼接
// world wide web W. W. W.
const firstLetterToUpper1 = fp.flowRight(fp.join(". "), fp.map(fp.first), fp.map(fp.toUpper), fp.split(' '));
// 优化 避免多次循环
const firstLetterToUpper1 = fp.flowRight(fp.join(". "), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '));
# Functor 函子
函数式编程 的核心是将 运算过程 抽象为 函数,将来可以最大化复用这些函数。函数式编程 是建立在 数学基础 上的,比如,纯函数就是数学中的 函数。Functor 是建立在数学中的 范畴论 的基础上的。
Functor 可以把副作用控制在可控范围之内,同时可以 处理异常,和 异步操作 等等。
# 什么是函子
- 容器:包含值和值的变形关系(这个变形关系就是函数)
- 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)
class Functor{
constructor(val){
this._value = val;
}
map(fn){
// 处理值 并返回一个新的函子
return new Functor(fn(this._value));
}
// 静态方法 返回函子对象
static of(val) {
return new Functor(val);
}
}
console.log(new Functor(5).map(x => x * 2).map(x => console.log(x)))
// 10
// Functor { _value: undefined }
console.log(Functor.of(5).map(x => x * 2))
// Functor { _value: 10 }
不要直接修改 _value 的值,通过 map 方法修改 _value 的值。
# Functor 函子 总结
- 函数编程的运算不直接操作值,而是通过 函子 完成,
- 函子是一个实现 map契约 的对象
- 我们可以把函子想象成一个容器,这个容器封装了一个值
- 想要处理容器中的值,我们需要给 map 方法传递一个处理值的函数(纯函数),由这个函数对值进行处理
- 最终 map 方法,返回一个包含新值的容器(函子)
- 可以通过 map 方法进行链式调用
- 因为我们可以把不同 运算方法 封装在函子中,所以可以衍生出不同类型的 函子,有多少 运算 就有多少 函子
- 最终可以使用 函子 解决实际的问题
# Maybe 函子
- 我们在编程中可能遇到各种错误,需要对这些错误进行相应的处理
- Maybe 函子,增加了对空值的判断,若是函子内部的值为空,则直接返回一个内部值为空的函子(控制副作用在可控范围内)
class Maybe {
constructor(val) {
this._value = val;
}
/**
*
* @param {*} fn
* 如果值为 null 或 undefined 返回值为 null 的函子
*/
map(fn) {
console.log(this._value)
return !this.isNothing() ? Maybe.of(fn(this._value)) : Maybe.of(null);
}
// 静态方法 返回函子对象
static of(val) {
return new Maybe(val);
}
// 判断传入的值是否合法
isNothing() {
return this._value === undefined || this._value === null;
}
}
当使用 Maybe 函子时传入空值则不会报错
console.log(Maybe.of(null).map(toUpper)); // Maybe { _value: null }
console.log(Maybe.of('hello').map(toUpper).map(x=>null).map(first)); // Maybe { _value: null }
Maybe 函子 可以处理 null 和 undefined,但是多次调用 map 方法,哪次出现 null 值,是不太明确的。
# Either 函子
Either 函子是指内部有分别有左值(left)和右值(right),正常情况下会使用右值,而当右值不存在的时候会使用左值的函子。
- Either 两者中的任意一个,类似于 if...else 的处理
- 使用 Maybe 函子时,当我们传递 null 时,仅仅返回一个值为 null 的函子。不会给出任何有效的信息,对异常信息吗,没有有效提示。
- 异常会使函数不纯,Either 函子可以用来做异常处理,并给出有效提示信息。
- 正常情况使用 Right 函子,异常情况使用 Left 函子
class Left {
constructor(val) {
this._value = val;
}
map(fn) {
// 直接返回当前对象
return this;
}
// 静态方法 返回函子对象
static of(val) {
return new Left(val);
}
}
class Right {
constructor(val) {
this._value = val;
}
map(fn) {
// // 处理值 并返回一个新的函子
return Right.of(fn(this._value));
}
// 静态方法 返回函子对象
static of(val) {
return new Right(val);
}
}
function parseJson(str) {
try {
// 正常情况使用 Right 函子
return Right.of(JSON.parse(str))
} catch (e) {
// 异常情况使用 Left 函子
return Left.of({ error: e.message})
}
}
const result = parseJson('{name:"111"}')
console.log(result)
// Left { _value: { error: 'Unexpected token n in JSON at position 1' } }
# IO 函子
- IO 函子内部的 _value 是一个函数,因为函数是一等公民,这里把函数当做值处理。
- IO 函子可以把不纯的操作存储在 _value 中,函子内部不会调用这个函数,因此可以延迟执行这些不纯的操作(惰性执行),保证当前的操作是纯的
- 把不纯的操作交给调用者来处理
const fp = require('lodash/fp')
class IO {
constructor(fn) {
// _value是一个函数
this._value = fn;
}
map(fn) {
// 帮当前的 _value 和 fn 组合成一个新的函数
return new IO(fp.flowRight(fn, this._value));
}
// 静态方法 返回函子对象
static of(val) {
return new IO(function(){
return val;
});
}
}
const result = IO.of(process).map(p=>p.execPath)
console.log(result._value()) // node执行进程的路径 result._value是不纯的函数
IO 函子内部传递的函数有可能是不纯的操作,但是 IO 函子执行返回的结果,始终是一个纯的操作。IO 函子可能包裹了不纯的操作,调用 map 方法始终返回一个 IO 函子。
IO 函子中的 value 属性,会组合很多函数,这些函数可能是不纯的操作。把这些不纯的操作延迟到了调用者来处理。
通过 IO 函子,控制了副作用在可控范围内发生。