深刻理解JavaScript函数式编程

函数式编程的思惟方式是把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象). (本篇文章内容输出来源:《拉钩教育大前端训练营》部分参考书籍:《JavaScript忍者秘籍》《你不知道的JavaScript 卷一》关于函数部分的讲解 进行总结)javascript

本章重点掌握Javascript中的高阶函数知识以及函数式编程.html

为何要学习函数式编程?前端

  • vue/react 开始拥抱函数式编程
  • 函数式编程能够抛弃this
  • 打包过程当中能够更好的利用tree shaking过滤无用的代码
  • 方便测试、方便并行处理
  • 有不少库能够帮助咱们进行函数式开发:loadsh、underscore、ramda

什么是函数式编程

什么是函数式编程(Functional Programming, FP):FP 是编程范式之一.(还有面向过程编程、面向对象编程)vue

面向对象编程的思惟方式: 把现实世界中的事物抽象成程序世界中的类和对象,经过封装、继承和多态来演示事物事件的联系java

函数式编程的思惟方式是把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象).node

  • 程序的本质:根据输入经过某种运算得到相应的输出,程序开发过程当中会涉及不少有输入和输出的函数
  • x ->f(联系、映射)->y,y=f(x)
  • 函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如:​y=sin(x),x和y的关系
  • 相同的输入始终要获得相同的输出
  • 函数式编程用来描述数据(函数)之间的映射
function test(x){
	return x * x;
}
复制代码

在Javascript中函数是一等公民,函数能够存储在变量中、函数做为参数、函数能够做为返回值.react

JavaScript中的高阶函数

高阶函数程序员

函数做为参数,以下代码实现的是循环遍历数组,经过传递参数回调函数能够拿到每一个数组遍历的值在回调函数中进行相应的处理web

//模拟forEach
function forEach(array, fn) {
    for (let index = 0; index < array.length; index++) {
        const element = array[index];
        fn(element);
    }
}
复制代码

函数做为返回值,以下函数能够做为返回值,以下代码通常来讲函数做为返回值是闭包的表现,关于闭包的概念会在后面详细的学习数据库

function test(x){
	return function(y){
				return x + y;
	}
}
let a = test(1)(2);//3
复制代码

高阶函数的意义

  • 抽象帮助咱们屏蔽细节,只须要关注咱们的目标
  • 高阶函数是用来抽象通用的问题

面向过程方式与函数式编程方式对比

经常使用高阶函数,下面来模拟JavaScript中的自带的高阶函数,以下代码经常使用的高阶函数大量都使用了以函数做为参数,进行回调。只须要拿到结果进行处理便可。

  • forEach - 函数做为参数
//模拟forEach
function forEach(array, fn) {
    for (let index = 0; index < array.length; index++) {
        const element = array[index];
        fn(element);
    }
}
复制代码
  • filter - 函数做为参数
//模拟filter
function filter(array, fn) {
    let result = [];
    for (let index = 0; index < array.length; index++) {
        const element = array[index];
        if (fn(element)) {
            result.push(element);
        }
    }
    return result;
}
复制代码
  • every-函数做为参数
//every 数组的全部元素进行某种操做所有为真匹配条件才返回真 不然只要有一个不成立就会返回false假
const every = (arr, fn) => {
    let result = false;
    for (const iterator of arr) {
        result = fn(iterator);
        //只要有一个返回为false就不成立
        if (!result) {
            break;
        }
    }
    return result;
}
复制代码
  • some-函数做为参数
//模拟some函数 数组中的元素只要有一个元素匹配条件返回为true,只有全部元素所有不匹配条件才会返回false
const some = (arr, fn) => {
    let result = false;
    for (const value of arr) {
        result = fn(value);
        if (result) {
            break;
        }
    }
    return result;
}
复制代码
  • once - 函数做为参数
//模拟once函数 只能执行一次
function once(fn) {
    let done = false;
    return function () {
        if (!done) {
            done = true;
            return fn.apply(this, arguments);//调用function() 传递的参数 传递到fn
        }
    }
}

let pay = once((money) => {
    console.log(`支付了${money} RMB`);
});
复制代码
  • map - 函数做为参数
//模拟map函数 对数组中对每个元素遍历改变每个元素的值 使用const 不但愿函数被修改定义为常量
const map = (array, fn) => {
    let results = [];
    for (const value of array) {
        results.push(fn(value));//获得的是fn的处理的结果
    }
    return results;
}
复制代码

闭包

闭包:函数和其周围的状态(词法环境)的引用捆绑在一块儿造成闭包.

  • 闭包能够在另外一个做用域中调用一个函数的内部函数并访问到该函数的做用域中的成员

如上述的once函数,返回的新的函数依然能够调用once()函数中的内部变量done

function once(fn) {
    let done = false;
    return function () {
        if (!done) {
            done = true;
            return fn.apply(this, arguments);//调用function() 传递的参数 传递到fn
        }
    }
}
复制代码
  • 闭包的本质:函数在执行的时候会放到一个执行栈上当函数执行完毕以后会从执行栈上移除,可是堆上的做用域成员由于被外部引用不能释放,所以内部函数依然能够访问外部函数的成员.

闭包的深刻理解

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script> /* 闭包的案例 */ Math.pow(4,2);//4的二次方 5的二次方 //经过一个函数来简化求平方 function makePow(power){ //返回一个函数求传递的数的power次幂 return function(value){ return Math.pow(value,power); } } //求平方 let power2 = makePow(2); //求三次方 let power3 = makePow(3); console.log(power2(2)); console.log(power2(4)); console.log(power3(4)); </script>
</body>
</html>
复制代码

下面咱们经过调试上述的代码,来看一下闭包的过程

以下图,重点关注的有两个地方,一个设置调试点而后刷新页面能够看到右侧的调试工具,重点关注右侧的Call Stack(调用栈)以及Scope(做用域)能够看到目前所处的做用域在Global全局做用域中.

Untitled.png

按F11或command + ; 执行下一步以下结果此时执行makePow函数,能够看到调用栈Call Stack的栈顶为makePow,而Scope做用域多了一个Local就是局部做用域里面存储着powerthis:Window 经过调试咱们能够看到不少有用的信息,帮助咱们去理解程序.

Untitled 1.png

而后咱们让程序执行到log的步骤执行的状况,看下面的视图,能够看到Scope中有一个Script的做用域存储着let变量的值,也就是let有一个单独的做用域Script.

Untitled 2.png

后面的重点来了,而后咱们继续往下执行一步,以下视图能够看到调用栈会执行power2()匿名函数,那么这个匿名函数中power是从哪里来的呢?看Scope部分多了一个Closure(makePow)它就是一个闭包,引用了makePowpower:2. 上述中讲到的当闭包发生后外部函数会从调用栈移除掉,可是与闭包相关的变量会被缓存下来,这个例子缓存下来的就是power.

Untitled 3.png

在看一下执行power3的状况,一样缓存下来power:3 .这样就是闭包的一个完整的过程.经过调试这样就能够很清晰的了解闭包的概念以及实现的过程比理解纯理论上的东西要容易的多,因此所学习更多的是要掌握方法.

Untitled 4.png

纯函数

纯函数:相同的输入永远会获得相同的输出,并且没有任何可观察的反作用

  • 纯函数就相似数学中的函数(用来描述输入和输出之间的关系),y=f(x);
  • lodash 是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操做的一些方法.
  • 数组的slice和splice分别是:纯函数和不纯的函数
    • slice 返回数组中的指定部分,不会改变原数组
    • splice 对数组进行操做返回该数组,会改变原数组
let array = [1,2,3,4,5];
console.log(array.slice(0,3));
console.log(array.slice(0,3));
console.log(array.slice(0,3));
//输入相同 输出也相同就是一个纯函数
//[ 1, 2, 3 ]
// [ 1, 2, 3 ]
// [ 1, 2, 3 ]

//splice 就不是一个纯函数 由于输入相同可是每次的输出结果不一样
console.log(array.splice(0,3));
console.log(array.splice(0,3));
console.log(array.splice(0,3));
//splice 相同的输入 每次输出的结果不相同 那么就是一个不纯的函数
//[ 1, 2, 3 ]
//[ 4, 5 ]
//[]

//写一个纯函数
function getSum(n1,n2){
    return n1 + n2;
}
console.log(getSum(1,2));
console.log(getSum(1,2));
console.log(getSum(1,2));
// 3
// 3
// 3
复制代码
  • 函数式编程不会保留计算中间的结果 因此变量是不可变的(无状态的)
  • 咱们能够把一个函数的执行结果交给另外一个函数去处理

Lodash 纯函数的表明

lodash库的使用,须要在nodejs的环境下引入lodash库

//first last toUpper reverse each includes find findIndex
const _=require('lodash');
const array = ['jake','tom','lucy','kate'];

console.log(_.first(array));//jake 纯函数
console.log(_.last(array));//kate 纯函数
console.log(_.toUpper(_.first(array)));//JAKE 纯函数

console.log(_.reverse(array));//[ 'kate', 'lucy', 'tom', 'jake' ] 注意:内部调用的是数组的reverse 而数组的reverse 会改变原有数组不是一个纯函数的方法

const r = _.each(array,(item,index)=>{
    console.log(item,index);
});
console.log(r);

const l = _.find(array,(item)=>{
    return item === 'jake';
});
console.log(l,array);
复制代码

纯函数的好处

  • 可缓存:由于纯函数对相同对输入始终有相同的结果,因此能够把纯函数的结果缓存起来

lodash的memoize函数

const _ = require('lodash');

function getArea(r) {
    console.log(r);
    //计算圆的面积
    return Math.PI * r * r;
}
//lodash的memoize方法 接收一个纯函数 对纯函数的结果缓存 返回一个带有记忆功能的函数
// let getAreaWithMemory = _.memoize(getArea);
// console.log(getAreaWithMemory(4));
// console.log(getAreaWithMemory(4));
// console.log(getAreaWithMemory(4));
/* 4 表示getArea这个函数只执行了一次 50.26548245743669 50.26548245743669 50.26548245743669 */
复制代码

手动实现memoize函数

//模拟memoize方法的实现
function memoize(fn){
    let cache = {};
    return function(){
        //1 判断cache是否有这个fn的结果
        let key = JSON.stringify(arguments);//将传递的参数做为key
        cache[key] = cache[key] || fn.apply(fn,arguments);//若是没有值调用fn() 结果做为值
        return cache[key];
    }
}
let getAreaWithMemory = memoize(getArea);
console.log(getAreaWithMemory(4));
console.log(getAreaWithMemory(4));
console.log(getAreaWithMemory(4));
/* 结果以下: 4 50.26548245743669 50.26548245743669 50.26548245743669 */
复制代码
  • 可测试 纯函数让测试更方便
  • 并行处理
    • 在多线程环境下并行操做共享的内存数据极可能会出现意外状况
    • 纯函数不须要访问共享的内存数据,因此在并行环境下能够任意运行纯函数(Web Worker)

反作用

  • 纯函数:对于相同的输入永远会获得相同的输出,并且没有任何可观察的反作用
//不纯的函数 一旦mini的值发生了改变就会是函数变的不纯 正是对外部的依赖致使的反作用
let mini = 18;
function checkAge(age){
    return age >= mini;
}

//纯的 (硬编码 后续会经过柯里化解决)
function makeCheckAge(age){
    let mini = 18;
    return age >= mini;
}
复制代码

反作用让一个函数变的不纯,纯函数的根据相同的输入返回相同的输出,若是函数依赖于外部的状态就没法保证输出相同,就会带来反作用.

反作用的来源

  • 配置文件
  • 数据库
  • 获取用户的输入
  • ...

全部的外部交互都有可能代来反作用,反作用也使得方法通用性降低不适合扩展和可重用性;同时反作用会给程序中带来安全隐患给程序带来不肯定性,可是反作用不可能彻底禁止,尽量控制它们在可控范围内发生.

柯里化(Haskell Brooks Curry)

使用柯里化解决纯函数的反作用.什么是柯里化呢? 当函数有多个参数的时候,对函数进行改造调用一个函数只传递并返回一个新的函数(这部分参数之后永远不会发生变化),这个新的函数去接收剩余的参数,返回结果。

  • 使用柯里化解决上一个案例中硬编码的问题
//硬编码
function checkAge(age){
    let min = 18;
    return age >= min;
}

//解决硬编码的问题 普通的纯函数
function checkAge(min,age){
    return age >= min;
}

console.log(checkAge(18,20));//true

//解决基准值的问题 经过闭包的方式
function checkAge(min) {
    return function (age) {
        return age >= min;
    }
}
let checkAge = min => ((age) =>(age>=min));

let checkAge18 = checkAge(18);
let checkAge20 = checkAge(20);

console.log(checkAge18(20));
console.log(checkAge18(24));
console.log(checkAge20(20));
console.log(checkAge20(24));
复制代码
  • lodash 中的柯里化的方法

lodash 通用的柯里化方法

curry(func) 建立一个函数而且该函数接收一个或多个func的参数,若是func所须要的参数,若是func所须要的参数都被提供则

则执行func并返回执行的结果,不然继续返回该函数并等待接受剩余的参数

参数:须要柯里化的函数

返回值:柯里化后的函数

const _ = require('lodash');
function getSum(a, b, c) {
    return 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 match = curry(function (reg, str) {
    return str.match(reg);
});

const haveSpace = match(/\s+/g);
const haveNumber = match(/\d+/g);

const filter = curry(function(func,arry){
    return arry.filter(func);
});

console.log(haveSpace('hello world'));
console.log(haveNumber('123abc'));

console.log(filter(haveSpace,['jonm Connm','Jone_Done']));

const findSpace = filter(haveSpace);//新的函数 查找数组中具备空白数组的函数

console.log(findSpace(['jonm Connm','Jone_Done']));
复制代码

闭包的本质就是内部函数能够访问外部函数的成员,而柯里化解决的是函数多个参数将函数进行分解的最小粒度的问题。要注意闭包和柯里化的区别两个不是一个概念。

  • 柯里化的原理
//柯里化原理实现
        function curry(func) {
            return function curriedFn(...args) {
                //判断匿名接受的参数个数以及func的形参个数
                if (args.length < func.length) {
                    //只传递部分的参数则返回一个新的函数
                    return function () {
                        //再次调用curriedFn 合并参数
                        return curriedFn(...args.concat(Array.from(arguments)));
                    }
                }
                //参数相同的状况下直接调用func
                return func(...args);
            }
        }
        function getSum(a, b, c) {
            return 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));
复制代码

这一块是比较烧脑的,跟着调试工具来进行理解就很是容易理解了,以下图所示:当执行到curried(1,2)(3)的时候,能够看到在Closure的做用域中有两个一个是传入的func一个是分解的函数传递的值args[1,2]

Untitled 5.png

代码继续往下执行,会调用curriedFn()将上一次的参数和此次传入的(3)进行合并,这时候arg.length==func.length,就会调用本来的函数func将全部的参数传递给它.

Untitled 6.png

  • 柯里化可让咱们给一个函数传递较少的参数获得一个已经记住了某些固定的新函数
  • 这是一种对函数参数的缓存
  • 让函数变的更灵活,让函数的粒度更小
  • 能够把多元函数转换成一元函数,能够组合使用函数产生强大的功能。

函数组合

函数组合(compose):若是一个函数要通过多个函数处理才能获得最终值,这个时候能够把中间过程的函数合并成一个函数。函数就像是数据的管道,函数组合就是把这些管道链接起来,让数据穿过多个管道造成最终结果。函数组合默认是从右到左执行.

  • 纯函数和柯里化容易写出洋葱代码 h(g(f(x)))
  • 函数组合能够把细粒度的函数从新组合生成一个新的函数

以下例子,演示了函数组合

function compose(f, g) {
    return function (value) {
        return f(g(value));
    }
}

/* 演示函数组合的使用 */
function reverse(arr) {
    return arr.reverse();
}

function first(arr) {
    return arr[0];
}

const last = compose(first,reverse);
console.log(last([1,2,3,4,5]));
复制代码

Lodash 中的组合函数,经过flowRight方法对函数进行组合,函数的执行顺序从右到左

const _ = require('lodash');

const reverse = arr => arr.reverse();

const first = arr => arr[0];

const toUpper = s => s.toUpperCase();

const l = _.flowRight(toUpper, first, reverse);

console.log(l(['a', 'b', 'c', 'd', 'e']));
复制代码

下面咱们来看看flowRight 的方法是如何实现的,这里就要考到API掌握的程度了,数组的reducereverse 因为数组的执行顺序从左到右执行因此要讲数组进行反转调用reverse()方法,reduce方法是遍历数组将上一个数组元素的值传递给下一个数组元素。这样咱们就实现了组合函数,上一个函数的值传递给下一个函数。

//flowRight 的实现方法
function compose(...args) {
    console.log(args);
    return function (value) {
        return args.reverse().reduce(function (acc, fn) {
            return fn(acc);
        }, value);
    }
}
//获取数组最后一个元素 转换为大写 注意函数的运行顺序从右到左
const l = compose(toUpper, first, reverse);
复制代码

arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

第一个累计器累计回调的返回值; 它是上一次调用回调时返回的累积值

第二个参数数组中正在处理的元素。

将compose简写:经过ES6箭头函数简化代码

const compose = (...args) => (value) => args.reverse().reduce((acc, fn) =>
    fn(acc), value);//reduce 第二个参数是一个初始的值 reduce是将全部数组进行遍历好比累加第一个的结果会传入到第二个中
复制代码
  • 函数组合要知足结合律 既能够把g和h组合,还可吧f和g组合,结果都是同样的
let f = compose(f,g,h);
let a = compose(compose(f,g),h) == compose(f,compose(g,h))

//结合律
const f = _.flowRight(_.flowRight(_.toUpper,_.first),_.reverse);
===
const f = _.flowRight(_.toUpper,_.flowRight(_.first,_.reverse));

console.log(f(['a', 'b', 'c', 'd', 'e']));
复制代码

组合函数如何调试

组合函数如何调试呢?好比我想打印某个方法执行的结果,其实处理很是简单咱们只须要在想要打印某个方法的执行结果的方法后面添加一个方法tracetrace方法就是提供打印的方法,在该方法中能够拿到上一个方法的返回值这样就能够打印上个一个方法的结果了,以下代码所示:

/* 函数组合调试 */
//NEVER SAY DIE => never-say-die

const _ = require('lodash');
//_.split();

const split = _.curry((sep, str) => {
    return _.split(str, sep);
});

//toLower join
const join = _.curry((sep, arr) => {
    return _.join(arr, sep);
});

const trace = _.curry((tag,v)=>{
    console.log(tag,v);
    return v;
});

const map = _.curry((func,arr)=>{
    return _.map(arr,func);
})

const f = _.flowRight(join('-'),trace('map'), map(_.toLower),trace('split'),split(' '));

console.log(f('NEVER SAY DIE'));
复制代码

lodash/fp 模块

  • lodash 的fp模块提供了实用的对函数式编程友好的方法。
  • 提供了不可变auto-curried iteratee-first data-last 的方法 函数优先数据滞后。

解决了上述中要使用curry进行柯里化的问题,有一些自带的方法是先传递数据在传递回调函数的,而fp模块就是解决这种问题,将数据滞后。(PS:其实不一样的语言和框架都是为了解决问题的,请不要忘记程序员的本质就是为了解决问题)

以下代码中,通常常见的方法好比map()第一个参数都须要传递数据才能够执行,可是这样就没法作到柯里化的处理了,那就必须经过柯里化将该方法从新封装一层以下代码:这样是很是很差的设计,那么loadsh是否提供了这样的解决方案呢?答案是确定的咱们来看fp模块

const _ = require('lodash');

//_.split();

const split = _.curry((sep, str) => {
    return _.split(str, sep);
});

//toLower join

const join = _.curry((sep, arr) => {
    return _.join(arr, sep);
});

const log=function(v){
    console.log(v);
    return v;
}

const trace = _.curry((tag,v)=>{
    console.log(tag,v);
    return v;
});

const map = _.curry((func,arr)=>{
    return _.map(arr,func);
})

const f = _.flowRight(join('-'),trace('map'), map(_.toLower),trace('split'),split(' '));

console.log('??',f('NEVER SAY DIE'));
复制代码

以下代码,fp模块对map、join、split对了处理,以函数优先数据滞后

const fp = require('lodash/fp');
const f = fp.flowRight(fp.join('-'),fp.map(fp.toLower),fp.split(' '));

console.log(f('NEVER SAY DIE'));//never_say_die
复制代码

map方法的区别和fp模块

以下代码,在_.map中对某个数组执行将数组元素转换为Number类型,可是结果打印倒是:23 NaN 2 这是为何呢?parseInt(s: string, radix?: number) radix 进制因此会存在问题致使2被转换2进制了,而fp模块的map只会向parseInt传递一个参数

console.log(_.map(['23','8','10'],parseInt));//23 NaN 2
//parseInt('23',0,array)
//parseInt('8',1,array)
//parseInt('10',2,array)

//fp 模块就不会出现这种问题
//fp map 的函数的参数只有一个就是处理的参数
console.log(fp.map(parseInt,['23','8','10']));//23 8 10
复制代码

PointFree

能够把数据处理的过程定义成与数据无关的合成运算,不须要用到表明数据的那个参数,只要把简单的运算步骤合成到一块儿,在使用这种模式以前须要定义一些辅助的基本运算函数。

  • 不须要指明处理的数据
  • 只须要合成运算过程
  • 须要定义一些辅助的基本运算函数

PointFree 模式 不须要关心数据

const f = fp.flowRight(fp.join('-'),fp.map(fp.toLower),fp.split(' '));
复制代码

案例演示,其实PointFree模式就是函数的组合,函数组合不须要处理数据的,返回的新函数来处理数据

//Hello world => hello_world

const fp = require('lodash/fp');

const f = fp.flowRight(fp.replace(/\s+/g,'_'),fp.toLower);//函数组合不须要处理数据
//返回新的函数来处理数据
console.log(f('Hello world'));
复制代码

下面咱们在写一个案例来更深刻的理解PointFree模式

//world wild web => W,W,W
//先切割字符串变成数组,map将数组的每个元素转换为大写,map将数组获取数组的元素的首字母
const firstLetterToUpper = fp.flowRight(fp.join(', '),
fp.map(fp.flowRight(fp.first,fp.toUpper)),fp.split(' '));

console.log(firstLetterToUpper('world wild web'));
复制代码

Functor(函子)

函数式编程中如何控制反作用控制在可控的范围内、异常处理、异步操做等。这些问题引入了函子的概念

Fuctor函子

  • 容器:包含值和值的变形关系(这个变形关系就是函数)
  • 函子:是一个特殊的容器,经过一个普通的对象来实现,该对象具备map方法,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);
console.log(r);//Container { _value: 36 }
复制代码

总结:

  • 函数式编程的运算不直接操做值,而是由函子完成
  • 函子就是一个实现了map的契约对象
  • 能够把函子想象成一个盒子,这个盒子里面封装了一个值
  • 想要处理盒子中的值,须要盒子的map方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
  • 最终map方法返回一个包含新值的盒子(函子)

存在的问题,在输入null的时候存在异常,没法处理异常状况,那么如何解决这种的反作用呢?继续看下面

//演示null undefined的问题
Container.of(null).map(x=>x.toUpperCase());//TypeError: Cannot read property 'toUpperCase' of null
复制代码

MayBe 函子

MayBe函子的做用就是能够对外部的控制状况作处理

class MayBe {
    static of(value) {
        return new MayBe(value);
    }

    constructor(value) {
        this._value = value;
    }

    map(fn) {
        return this.isNoting() ? MayBe.of(null) : MayBe.of(fn(this._value));
    }

    isNoting() {
        return this._value === null || this._value === undefined;
    }
}

// let r = MayBe.of('hello world').map(x => x.toUpperCase());
// let r = MayBe.of(null).map(x => x.toUpperCase());//MayBe { _value: null }
let r = MayBe.of('hello world')
    .map(x => x.toUpperCase())
    .map(x => null)
    .map(x => x.split(' '));//MayBe { _value: null } 可是那个地方出现了问题呢? 是没法知道的

//maybe 函子的问题

console.log(r);
复制代码

MayBe 函子其实就是在容器的内部判断值是否为空,若是为空就返回一个值为空的函子。可是MayBe函子没法知道哪一个地方出现了问题,如法处理异常问题,这就继续引出了下一个概念。

Either 函子

Either 二者中的任何一个,相似if...else...的处理。异常会让函数变的不纯,Either函子能够用来作异常处理,这种函子在经常使用的业务开发中会常常用到务必掌握。

以下代码,定义两个函子,一个处理正确的结果,一个处理异常的结果,异常的处理直接返回this

class Left {
    constructor(value) {
        this._value = value;
    }

    static of(value) {
        return new Left(value);
    }

    map(fn) {
        return this;
    }
}

class Right {
    constructor(value) {
        this._value = value;
    }

    static of(value) {
        return new Right(value);
    }

    map(fn) {
        return Right.of(fn(this._value));
    }
}
复制代码

注意相同的输入在两个函子中是不一样的输出

let r1 = Right.of(12)
    .map(x => x + 2);

let l1 = Left.of(12).map(x => x + 2);

console.log(r1,l1);//Right { _value: 14 } Left { _value: 12 }
复制代码

下面来演示,异常的处理状况,以下代码在catch中调用Left函子返回错误的结果

function parseJson(str){
    try {
        return Right.of(JSON.parse(str))
    } catch (e) {
        //出现错误的时候 使用Left 由于相同的输入 获得相同的输出
        return Left.of({error:e.message});
    }
}

//异常状况的处理
let r = parseJson('{ "name":"zs" }');

console.log(r);//Left { _value: { error: 'Unexpected token n in JSON at position 1' } }
复制代码

正常的结果处理状况,经过.map对下一步的业务逻辑进一步处理

//正确状况下的处理
let r = parseJson('{ "name":"zs" }').map(x=>x.name.toUpperCase());//处理json将name属性转换为大写
console.log(r);//Right { _value: { name: 'ZS' } }
复制代码

IO函子

IO 函子中的_value是一个函数,这里把函数做为值来处理;IO函子能够把不纯的动做存储到_value中,延迟执行这个不纯的操做(惰性执行),包装当前的操做把不纯的操做交个调用者处理

//IO 函子
const fp = require('lodash/fp');

class IO {
    static of(value) {
        return new IO(function () {
            return value;
        });
    }
    constructor(fn) {
        this._value = fn;
    }
    map(fn){
        return new IO(fp.flowRight(fn,this._value));
    }
}

//调用
let io = IO.of(process).map(p=>p.execPath).map(p=>p.toUpperCase());
console.log(io);
//将组合的函数调用 先执行p.execPath 再执行:p=>p.toUpperCase() 注意map函数的执行顺序
console.log(io._value());///Users/prim/.nvm/versions/node/v12.14.0/bin/node 执行方法
///USERS/PRIM/.NVM/VERSIONS/NODE/V12.14.0/BIN/NODE
复制代码

Folktale

folktale 是一个标准的函数式编程库,异步任务的实现过于复杂,使用folktale中的Task来演示.只提供了一些函数式处理的操做:compose、curry等一些函子Task、Either、Maybe等

Task 函子处理异步任务

const { compose, curry } = require('folktale/core/lambda');
const { toUpper, first,split,find } = require('lodash/fp');
const { task } = require('folktale/concurrency/task');
const fs = require('fs');
let f = curry(2, (x, y) => {
    return x + y;
})

console.log(f(1, 2));//3
console.log(f(1)(2));//3

//compose 函数组合

let f1 = compose(toUpper, first);

console.log(f1(['one', 'two']));//ONE

function readFile(filename) {
    return task(resolver => {
        fs.readFile(filename, 'utf-8', (err, data) => {
            if (err) {
                resolver.reject(err);
            }
            resolver.resolve(data);
        })
    });
}

readFile('package.json')
    .map(split('\n'))
    .map(find(x=>x.includes('version')))
    .run()//?? run有什么用?执行了什么代码呢? 是将上述的结果返回给listen吗?
    .listen(
        {
            onRejected:err=>{
                console.log(err);
            },
            onResolved:data=>{
                console.log(data);
            }
        }
    );
复制代码

Pointed函子

Pointed 函子是实现了of静态方法的函子,of方法是为了不使用new来建立对象,更深层的含义是of方法用来把值放到上下文Context(把值放到容器中,使用map来处理值)

其实上述将的函子都是Pointed函子。

Monad函子

IO函子的问题,在业务逻辑遇到函子嵌套的状况IO(IO(x)); Monad就是解决函子嵌套问题的。

let readFile = function (filename) {
    return new IO(function () {
        return fs.readFileSync(filename, 'utf-8');
    });
}

let print = function (log) {
    return new IO(function(){
        console.log(log);
        return log;//log = IO(x)
    });
}

let cat = fp.flowRight(print,readFile);

let r = cat('package.json')._value()._value(); // IO(IO(x))
console.log(r);//IO { _value: [Function] }
复制代码
  • Monad 函子是能够变扁的Pointed函子
  • 一个函子若是具备join和of两个方法并遵照一些定律就是一个Monad
const fp = require('lodash/fp');
const fs = require('fs');
class IO {
    static of(value) {
        return new IO(function () {
            return value;
        });
    }
    constructor(fn) {
        this._value = fn;
    }
    map(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 (log) {
    return new IO(function(){
        console.log(log);
        return log;//log = IO(x)
    });
}
let r = readFile('package.json')//_value = fn1
    .map(x=>x.toUpperCase())//处理文件 _value=fn11
    .flatMap(print)//return IO(value) ==> _value = fp.flowRight(print,fn11,fn1); value = _value();
    .join(); // map(fn2) _value = fn2=new IO() ,fn1 join():_value: fp.flowRight(fn2, fn1) => new IO(fn3);---> join:fn3()
console.log(r);//IO { _value: [Function] }
复制代码

总结

  • 函数式编程不能提升程序的性能,由于大量使用闭包在某种程度上会下降性能
  • 函数式编程中的函数不是程序中的函数和方法,而是数学中的函数
  • 函数式一等公民(MDN的解释中只包含这三点)
    • 函数能够存储在变量中
    • 函数能够做为参数
    • 函数能够做为返回值
  • 反作用会让一个函数变的不纯,可是反作用是不可避免的,由于代码不免会依赖外部文件、数据库等,只能最大程度上控制反作用在可控的范围内
  • 柯里化函数curry也是高阶函数
  • 柯里化函数内部用到了闭包,对函数的参数作了缓存
  • 柯里化函数能够把多个参数的函数转换成只有一个参数的函数,经过组合产生功能更强大的函数
  • 柯里化让函数变的更灵活,让函数的粒度更小
  • 函数能够看作一个处理数据的管道,管道中输入参数 x,在管道中对数据处理后获得结果 y
  • 经过函数组合能够把多个一元函数组合成一个功能更强大的函数
  • 函数组合须要知足结合律,函数组合默认的执行顺序是从右到左
  • 函子是一个特殊的容器(对象),这个容器内部封装一个值,经过 map 传递一个函数对值进行处理
  • MayBe 函子的做用是处理外部的空值状况,防止空值的异常
  • IO 函子内部封装的值是一个函数,把不纯的操做封装到这个函数,不纯的操做交给调用者处理
  • Monad 函子内部封装的值是一个函数(这个函数返回函子),目的是经过 join 方法避免函子嵌套
相关文章
相关标签/搜索