函数式编程
关键词:三种编程范式、函数一等公民、高阶函数、高阶组件、闭包、调试小技巧、纯函数、lodash库、副作用、函数科里化、函数组合、Point free、函子(Maybe、Either、IO、Pointed、Monad)、folktale函子库
为什么要学习函数式编程:
react带动关注,高阶组件使用高阶函数实现,就是函数式编程的体现;
vue2源码中也大量使用高阶函数。
面向对象语言:java、c++;原型、原型链、模拟面向对象的特性;this
函数式编程甚至可以抛弃this,打包tree-shaking、方便测试、并行处理。
函数式编程概念:
functional program,一种编程范式(风格),面向对象(抽象、封装、类、多态)、面向过程(执行过程)也是其中一种。
面向对象:抽象现实事物
函数式编程:抽象运算过程x-f->y;函数指数学中的函数,即映射关系,如何由输入得到输出,固定输入得到固定输出。好处:代码进行重用,细粒度函数组合功能强大的。
函数是一等公民、高阶函数、闭包
函数是一等公民
1、函数可以存储在变量中
2、函数作为参数
3、函数作为返回值
函数是一个普通的对象,new Function()构建。
参数和返回值一样,赋值方法非调用,不带();
高阶函数
多级调用
1、应用
1.1、以函数为参数,如forEach\filter;
好处:是函数更灵活,内部实现细节可为插槽用户自定义。
function forEach(arr, fn){
for(let i = 0; i < arr.length; i++){
// 调用fn处理数组中的每一个元素
fn(arr[i])
}
}
let arr = [2,5,3,7];
forEach(arr, function(item){
console.log(item);
})
function filter(arr, fn){
let res = [];
for(let i = 0; i < arr.length; i++){
// 调用fn处理数组中的每一个元素
if(fn(arr[i])){
res.push(arr[i])
}
}
return res
}
let arr2 = filter(arr, function(item){
return item % 2 === 0
});
console.log(arr2)
1.2、以函数为返回值
function makeFn(){
let msg = 'hhh';
return function(){
console.log(msg);
}
}
makeFn()();
// 模仿lodash的once,不论调用多少次只执行一次
function once(fn){
let done = false;
return function(){
if(!done){
done = true;
return fn.apply(this, arguments); // arguments 参数
}
}
}
let pay = once(function(money){
console.log(`支付${money}`)
})
pay(10);
pay(10);
pay(10);
// 只打印一次 10
2、意义
1、抽象可以屏蔽细节,抽象通用问题,只需关注目标
2、函数灵活简洁
3、常用的高阶函数
forEach、filter、map、every、some、reduce、find/findIndex、sort等数组需要传递函数为参数的函数
// 模拟map函数
const map = (arr, fn) => {
let res = []
for(let value of arr){
res.push(fn(value))
}
return res
}
let arr = [2,5,3,7];
let arr2 = map(arr, v => v*v);
console.log('arr2 ---> ', arr2);
// 模拟every函数
const every = (arr, fn) => {
let res = true
for(let value of arr){
res = fn(value)
if(!res) break
}
return res
}
let arr = [2,5,3,7];
let r = every(arr, v => v > 5);
console.log('r ---> ', r);
// 模拟some函数
const some = (arr, fn) => {
let res = false
for(let value of arr){
res = fn(value)
if(res) break
}
return res
}
let arr = [2,5,3,7];
let r = some(arr, v => v > 5);
console.log('r ---> ', r);
闭包Closure
函数及其周围的状态(词法环境)的引用捆绑在一起形成闭包,可在另一个作用域调用一个函数的内部函数并访问到该函数作用域中的成员。如once。
闭包本质:函数在执行时会放到一个执行栈上,当函数执行完毕后会从执行栈上移除,但堆上的作用域成员因被外部引用不能释放,因此内部函数依然可以访问函数外部的成员。(出栈入堆)
控制台:
Call Stack:调用栈
Scope:作用域
Script:let定义变量
Global:全局var定义变量
F10调试,单步执行,不进入调用的其它函数;
F11调试,单步执行,进入调用的其它函数;
纯函数概念
纯函数:相同输入得相同输出,无任何可观察副作用()
函数式编程不会保留中间计算结果,变量不可变(无状态的);可以把一个函数执行结果交给另一个函数去处理
1、数组中纯slice(不改变数组)、不纯splice(改变数组);
let arr = [1,2,3,4,5]
console.log(arr.slice(0,3));
console.log(arr.slice(0,3));
console.log(arr.slice(0,3)); // 纯函数,多次结果相同
console.log(arr.splice(0,3));
console.log(arr.splice(0,3));
console.log(arr.splice(0,3)); // 不纯会改变数组,多次结果不同
2、lodash纯函数功能库,提供对数组、数字、对象、字符串、函数等操作的一些方法
便捷工具、函数式 https://www.lodashjs.com/
const lodash = require('lodash');
const arr = ['jsdf','fsd','werw']
console.log(lodash.first(arr))
console.log(lodash.last(arr))
console.log(lodash.toUpper(arr))
console.log(lodash.reverse(arr)) // 内部调用数组的reverse,不是原函数
console.log('arr ---> ', arr);
const r = lodash.each(arr, (item, index) => {
console.log(item, index)
})
console.log(r); //数组本身
3、纯函数好处
1)可缓存,提高性能
2)可测试(单元测试,断言函数结果)
3)并行处理:
在多线程环境下并行操作共享的内存数据很可能会出现意外情况(同时修改一个变量,结果不确定);纯函数不需要访问共享内存数据,只依赖参数,所以并行下可以随意运行(es6之后Web Worker可以开启多线程)
const lodash = require('lodash');
function getArea(r){
console.log('r ---> ', r);
return Math.PI *r *r
}
// 模拟memoize
function memoize(fn){
let cache = {}
return function(){
let key = JSON.stringify(arguments);
cache[key] = cache[key] || fn.apply(fn, arguments) // arguments每一项展开调用
return cache[key]
}
}
// let getAreaWithMemory = lodash.memoize(getArea);
let getAreaWithMemory = memoize(getArea);
console.log('getAreaWithMemory ---> ', getAreaWithMemory(4));
console.log('getAreaWithMemory ---> ', getAreaWithMemory(4));
console.log('getAreaWithMemory ---> ', getAreaWithMemory(4));
// r只打印了一次
4、副作用
如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。
依赖的外部数据,常硬编码,可以通过函数科里化解决
副作用来源:
配置文件、数据库、获取用户的输入等所有外部交互,使得方法通用性下降不适合扩展和可重用性,同时副作用会给程序带来安全隐患(跨站脚本攻击)及不确定性,但却不能完全禁止,尽可能控制在可控范围。
5、函数科里化
应用方式:
当一个函数有多个参数先传递一部分参数调用它(这部分参数以后永远不变),返回一个新的函数接收剩余的函数,然后返回结果
好处:
1、把多元函数转换成一元函数
2、使用闭包记住了固定参数的新函数,将其返回,方便之后复用(一次定义,多处使用,就作为公共应用包);
let min = 18;
function checkAge1(age){
return age >= min
}
// 重复输入min
function checkAge2(min,age){
return age >= min
}
// 科里化
function checkAge3(min){
return function(age){
return age >= min
}
}
let checkAge18 = checkAge3(18);
let checkAge20 = checkAge3(20);
checkAge18(50);
checkAge18(29); // 基准值不在调用,接收剩余参数
lodash中的科里化函数:_.curry(func),curry本身是个纯函数,参数:需要科里化函数,返回:一个待输入剩余参数的科里化函数
const _ = require('lodash');
function getSum(a,b,c){
return a+b+c
}
const curried = _.curry(getSum);
console.log('curried(1,2,3) ---> ', curried(1,2,3));
console.log('curried(1) ---> ', curried(1));
// curried(1,2,3) ---> 6
// curried(1) ---> function getSum(a,b,c){
// /* [wrapped with _.curry & _.partial] */
// return a+b+c
// }
console.log('curried(1)(2)(3) ---> ', curried(1)(2)(3)); // 6
console.log('curried(1,2)(3) ---> ', curried(1,2)(3)); // 6
console.log('curried(1,2)(3,4) ---> ', curried(1,2)(3,4)); // 6
console.log('curried(1,2)(3,4) ---> ', curried(1,2)(3)(4)); // 报错 curried(...)(...) is not a function
// 模拟curry,考虑参数
function curry(func){
return function curriedFn(...args){
// 判断实参和形参的个数,小于要保留等待之后调用
if(args.length < func.length){
return function(){
return curriedFn(...args.concat(Array.from(arguments)))
}
}
return func(...args)
}
}
6、函数组合
纯函数和科里化很容易写出洋葱代码:h(g(f(x))),函数组合把细粒度的函数重新组合成新的函数。
函数组合:
compose:如果一个函数要经过多个函数处理得最终值,这时将中间过程的函数合并成一个函数。
特点:默认从右向左执行;满足结合律;
// 1、函数组合
function compose(f, g){
return function(value){
return f(g(value))
}
}
// 2、更多函数组合新的函数 lodash包
// flow() 从左到右执行;flowRight() 从右到左执行,使用较多
const _ = require('lodash');
const reverse = arr => arr.reverse();
const first = arr => arr[0];
const toUpper = s => s.toUpperCase();
const f = _.flowRight(toUpper, first, reverse);
console.log(f(['o','t','th'])); // TH
// 模拟flowRight
function compose(...funcs){
return function(value){
return funcs.reverse().reduce(function(acc, fn){
// acc为每次运行初始值,fn为funcs.reverse()的数组元素
return fn(acc)
}, value) // value为acc初始值
}
}
// 转换为箭头函数,代码更简洁
const composeA = (...funcs) => value => funcs.reverse().reduce((acc, fn) => fn(acc), value);
const f2 = composeA(toUpper, first, reverse);
console.log(f2(['o','t','th'])); // TH
调试组合函数:
// 辅助函数查看打印结果, tag标记想要查看的位置
const trace = _.curry((tag, v) => {
console.log(tag, v)
return v;
})
lodash/fp模块(functional program):
正常数组map、join、lodash中的函数等均为“数据优先,函数之后”,需自行转换函数科里化为“函数优先,数据之后”在调用的时候再传入数据。
lodash的FP模块提供了使用的对函数式编程友好的方法,不需要自行包装
const fp = require('lodash/fp');
// const f = _.flowRight(join('-'), map(_.toLower), split(' ')); // map\join\split均为自行封装后的科里化
const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))
// lodash及lodash/fp的map方法("数据或函数优先")区别演示:
console.log(_.map(['23', '5', '3'], parseInt)); // [ 23, NaN, NaN ]
// parseInt('23', 0, array); // parseInt的第2个参数为转化为几进制,0默认为10
// parseInt('5', 1, array);
// parseInt('3', 2, array); // 3无法用二进制表示,10可以
console.log(fp.map(parseInt, ['23', '5', '3'])); // [ 23, 5, 3 ]
7、Point free
不需指明处理的数据,只需合成运算过程,需要定义一些辅助的基本运算函数;(即:函数组合)
抽象出的函数合成新的函数;
pipe通道
8、函子functor
涉及“范畴论”,可以帮助控制副作用
容器:包含值和值的变形关系(函数)
函子:特殊的容器,通过一个普通的对象实现,该对象具有map(fn)方法,fn对值进行处理(变形关系)
函子为一个盒子,盒子中封装一个值,实现了一个参数为处理值的函数、返回值为包装新值的盒子的map函数;不输出数值
class Container {
static of(value){
return new Container(value)
}
constructor(value){
this._value = value;
}
map(fn) {
return Container.of(fn(this._value))
}
}
let res = Container.of(5) // 链式编程,维护一个值
.map(x => x + 1)
.map(x => x * x)
console.log('res ---> ', res); // res ---> Container { _value: 36 }
// null、undefined的问题,报错非预期效果,Maybe函子解决
Container.of(5).map(x => x.toUpperCase())
8.1、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(){
return this._value === null || this._value === undefined
}
}
let res = Maybe.of(undefined)
.map(x => x.toUpperCase()) // 多次调用不清楚null来源
console.log('res ---> ', res); // res ---> Maybe { _value: null }
8.2、Either函子作用:用来处理异常
class Left {
static of(value){
return new Left(value)
}
constructor(value){
this._value = value;
}
map(fn) {
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})
}
}
let r = parseJSON('{"name":"zs"}')
.map(x => x.name.toUpperCase()).map(x => x.split(''));
console.log('r ---> ', r); // r ---> Right { _value: 'ZS' }
let r2 = parseJSON('{"name:"zs"}')
.map(x2 => x2.name.toUpperCase()).map(x2 => x2.split(''));
console.log('r2 ---> ', r2); // r2 ---> Left { _value: { error: 'Unexpected token z in JSON at position 8' } }
8.3、IO函子:
_value是不纯的动作函数(把不纯的操作交给调用者来处理),延迟执行该不纯的操作(惰性执行)
可以操作回调,地狱性回调??Task函子避免回调的嵌套
const fp = require('lodash/fp');
class IO {
static of(x){
return new IO(function(){
return x // 返回值
})
}
constructor(fn){
this._value = fn;
}
map(fn) {
// 当前value与fn组成一个新的函数
return new IO(fp.flowRight(fn, this._value))
}
}
let r = IO.of(['xx','yy']).map(p => p).map(p => p.join(','));
console.log('r ---> ', r); // r ---> IO { _value: [Function] }
console.log(r._value()); // xx,yy 将副作用延迟到调用的时候发生
8.4、folktale
标准的函数式编程库,只提供了函数式处理的操作,如compose、curry等,一些函子Task、Either、Maybe等
// 1、compose\curry的使用
const {compose, curry} = require('folktale/core/lambda');
const {toUpper, first} = require('lodash/fp');
let f = curry(2, (x, y) => {
return x+y;
})
console.log('f(1,2) ---> ', f(1,2));
console.log('f(1)(2) ---> ', f(1)(2));
let f2 = compose(toUpper, first);
console.log('f2(["xx","yy"]) ---> ', f2(["xx","yy"]));
// 2、Task异步执行
// folktale 1.x与2.x版本差别大,可看文档
const {task} = require('folktale/concurrency/task');
const fs = require('fs');
const {split, find} = require('lodash/fp');
function readFile(filename){
return task(resolver => {
fs.readFile(filename, 'utf-8', (err, data) => {
if(err){
resolver.reject(err)
}
resolver.resolve(data)
})
})
}
readFile('blog/lagou-web/code/package.json')
.map(split('\n'))
.map(find(x => x.includes('version')))
.run()
.listen({
onRejected: err => {
console.log(err);
},
onResolved: value => {
console.log(value); // "version": "1.0.0",
}
})
8.5、Pointed函子
实现了of静态方法的函子,of方法避免使用new来创建对象,更深层含义是of方法用来把值放到上下文Context(把值放到容器中,使用map来处理)
8.6、Monad函子
IO函子,多层嵌套的话,会一直不停的调用;
Monad函子是可以变扁的Pointed函子,如果一个函数同时具有join、of两个方法并遵守一些定律就是一个Monad函子
当一个函数返回一个函子时,当返回函子可以调用flatMap,当返回值map处理
const _ = require('lodash');
const fp = require('lodash/fp');
const fs = require('fs');
class IO {
static of(x){
return new IO(function(){
return x // 返回值
})
}
constructor(fn){
this._value = fn;
}
map(fn) {
// 当前value与fn组成一个新的函数
return new IO(fp.flowRight(fn, this._value))
}
join() { // 调用value
return this._value();
}
flatMap(fn) {
return this.map(fn).join();
}
}
let readFile = function(filename){
return new IO(function(){
return fs.readFileSync(filename, 'utf-8');
})
}
let print = function(x){
return new IO(function(){
console.log(x)
return x;
})
}
let r = readFile('blog/lagou-web/code/package.json')
.map(fp.toUpper)
.flatMap(print)
.join()
console.log('r ---> ', r);
8.7、总结
有助于使用vue、react等使用了函数式编程的框架;