函数式编程与Js异步编程、手写Promise(02)

Part1 · JavaScript【深度剖析】

函数式编程与Js异步编程、手写Promise

文章说明:本专栏内容为本人参加【拉钩大前端高新训练营】的学习笔记以及思考总结,学徒之心,仅为分享。如若有误,请在评论区支出,如果您觉得专栏内容还不错,请点赞、关注、评论。

共同进步!

四、纯函数

1.纯函数概念

在这里插入图片描述

  • 纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用

    • 纯函数就类似数学中的函数(用来描述输入和输出之间的关系),y=f(x)
  • lodash是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法

  • 数组slice和splice分别是:纯函数和不纯的函数

    • slice返回数组中的指定部分,不会改变原数组
    • splice对数组进行操作返回该数组,会改变原数组
let numbers = [1, 2, 3, 4, 5];
// 纯函数
numbers.slice(0, 3)
// =>[1,2,3]
numbers.slice(0, 3)
// =>[1,2,3]
numbers.slice(0, 3)
// =>[1,2,3]

// 不纯的函数
numbers.splice(0, 3)
// => [1,2,3]
numbers.splice(0, 3)
// => [4,5]
numbers.splice(0, 3)
// => []
  • 函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)
  • 我们可以把一个函数的执行结果交给另一个函数去处理

2.纯函数的好处

  • 可缓存

    • 因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
    const _ = require('lodash')
    
    function getArea(r) {
        return Math.PI * r * r
    }
    
    let getAreaWithMemory = _.memoize(getArea)
    console.log(getAreaWithMemory(4))
    
  • 自己模拟一个memoize函数

function getArea (r) {
  console.log(r)
  return Math.PI * r * r
}

function memoize(f) {
    // 使用对象将结果缓存起来,键为传入函数f的参数,值为传入函数f的返回值
    let cache = {}
    // 返回一个function,在其中判断是否需要再次执行获取返回值
    return function () {
        // arg_str为传入函数f的参数,使用JSON.stringify将其转化为字符串
        let arg_str = JSON.stringify(arguments)
        // 对cache[arg_str]进行赋值,有两种情况:
        // 1.cache中存在arg_str键,则直接取值对其赋值
        // 2.cache中无arg_str键,需要调用传入的f函数
        // 使用f.apply调用f函数,第一个参数指向f本身,第二个参数为传入函数f的参数,随后取其返回值
        cache[arg_str] = cache[arg_str] || f.apply(f, arguments)
        return cache[arg_str]
    }
}

// 4
// 50.26548245743669
// 50.26548245743669
// 50.26548245743669
  • 可测试
    • 纯函数让测试更方便
  • 并行处理
    • 在多线程环境下并行操作共享的内存数据很可能会出现意外情况
    • 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数(Web Worker)

3.副作用

  • 纯函数:对于相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
// 不纯的
let mini = 18  // 依赖外部状态导致函数不纯

function checkAge(age) {
    return age >= mini
}

//纯函数(硬编码问题,后续通过柯里化解决)
function checkAge2(age) {
    let mini = 18
    return age >= mini
}

副作用让一个函数变得不纯(如上例),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用

副作用来源:

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

所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降,不适合扩展和可重用性,同时副作用会给程序中带来安全隐患,给程序带来不确定性,但副作用不可能完全禁止,尽可能控制他们在可控范围内发生。

五、柯里化(Haskell Brooks Curry)

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

//普通纯函数
function checkAge(min,age) {
    return age >= mini
}

checkAge(18,24)
checkAge(18,20)
checkAge(20,30)

// 柯里化
function checkAge(min) {
    return function (age) {
        return age >= min
    }
}

// ES6写法
let checkAge = min => (age => age >= min)

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

checkAge18(24)
checkAge18(20)
  • 柯里化
    • 当一个函数有多个参数的时候,先传递一部分参数调用它(这部分参数以后永远不变)
    • 然后返回一个新的函数接收剩余的参数,返回结果

1.lodash中的柯里化函数

  • _.curry(func)
    • 功能:创建一个函数,该函数接收一个或多个func参数,如果func所需要的参数都被提供则执行func并返回执行结果。否则继续返回该函数并等待接收剩余的参数。
    • 参数:需要柯里化的函数
    • 返回值:柯里化后的函数
const _ = require('lodash');

// 要柯里化的函数
function getSum(a, b, c) {
    return a + b + c
}

// 柯里化后的函数
let curried = _.curry(getSum)
// 测试
curried(1, 2, 3)
curried(1, 2)(3)
curried(1)(2, 3)
  • 案例
const _ = require('lodash')
const match = _.curry(function (reg, str) {
    return str.match(reg)
})

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

console.log(haveSpace('Hello World'))
console.log(haveNumber('$25'))

const filter = _.curry(function (func, array) {
    return array.filter(func)
})

console.log(filter(haveSpace, ['John Connor', 'John_Donne']))

const findSpace = filter(haveSpace)
console.log(findSpace(['John Connor', 'John Donne']))
  • 模拟_.curry()的实现
function curry(func) {
    return function curriedFn(...args) {
        // 判断实参和形参的个数,func.length代表func的参数的长度
        if (args.length < func.length) {
            return function () {
                // 将已传递的参数与未传递的参数拼接,concat用于数组拼接
                return curriedFn(...args.concat(Array.from(arguments)))
            }
        }
        return func(...args)
    }
}

2.总结

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

六、函数组合

  • 纯函数和柯里化很容易写出洋葱代码(一圈一圈又一圈h(g(f(x))))

    • 获取数组的最后一个元素再转换成大写字母,.toUpper(.first(.reverse(array)))
      在这里插入图片描述
  • 函数组合可以让我们把细粒度的函数重新组合生成一个新的函数

1.管道

下面这张图表示程序中使用函数处理数据的过程,给fn函数输入参数a,返回结果b。可以想象a数据通过一个管道得到了b数据。
在这里插入图片描述

当fn函数比较复杂的时候,我们可以把函数fn拆分成多个小函数,此时多了中间运算过程产生的m和n。

下面这张图中可以想象成把fn这个管道拆分成了3个管道f1,f2,f3,数据a通过管道f3得到结果m,m再通过管道f2得到结果n,n通过管道f1得到最终结果b。
在这里插入图片描述

fn = compose(f1,f2,f3)
b = fn(a)

2.函数组合

  • 函数组合(compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
    • 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
    • 函数组合默认是从右到左执行
// 组合函数  数组先翻转再去第一个元素
function compose(f, g) {
    return function (x) {
        return f(g(x))
    }
}

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

function reverse(arr) {
    return arr.reverse()
}

// 从右到左执行
let last = compose(first, reverse)
console.log(last([1, 2, 3, 4]))
  • lodash中的组合函数
  • lodash中组合函数flow()或者flowRight(),他们都可以组合多个函数
  • flow()是从左到右运行
  • flowRight()是从右到左运行,使用的更多一点
const _ = require('lodash')

const toUpper = s => s.toUpperCase()
const reverse = arr => arr.reverse()
const first = arr => arr[0]

const f = _.flowRight(toUpper, first, reverse)
console.log(f['one', 'two', 'three'])
  • 模拟实现lodash的flowRight方法
// 多函数组合
function compose(...fns) {
    // 传入不定数量的函数,使用...fns代替,剩余参数
    // 返回一个函数,其需要接收一个参数value
    return function (value) {
        // 返回调用,从...fns中最后一个函数开始调用,所以先需要将参数fns进行翻转
        // 每个函数需要对value进行处理并返回处理后的值
        // reduce对数组中的每一个元素去执行我们提供的函数,并将其汇总成单个的结果
        // reduce参数为一个函数,该函数需要两个参数,acc:累计的结果,fn如何处理每次结果并返回新的结果
        return fns.reverse().reduce(function (acc, fn) {
            // fn数组中的每一个函数,acc上一次返回的结果
            return fn(acc)
            // acc初始的值为第一次调用时传的value
        }, value)
    }
}

// ES6
const compose = (...fns) => value => fns.reverse().reduce((acc, fn) => fn(acc), value)
  • 函数的组合要满足结合律(associativity)
    • 我们既可以把g和h结合,也可以把f和g结合,结果都是一样的,即结合顺序不影响结果值
// 结合律(associativity)
let f = compose(f, g, h)
let associative = compose(compose(f, g), h) == compose(f, compose(g, h))
// true
  • 所以代码还可以像下面这样
const _ = require('lodash')
// const f = _.flowRight(_.toUpper, _.first, _.reverse)
// const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse)
const f = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse))
console.log(f(['one', 'two', 'three']))
// => THREE

3.调试

  • 如何调试组合函数
const f = _.flowRight(_.toUpper, _.first, _.reverse)
console.log(f(['one', 'two', 'three']))
const _ = require('lodash')
    const trace = _.curry((tag, v) => {
    console.log(tag, v)
    return v
})
const split = _.curry((sep, str) => _.split(str, sep))
const join = _.curry((sep, array) => _.join(array, sep))
const map = _.curry((fn, array) => _.map(array, fn))
const f = _.flowRight(join('-'), trace('map 之后'), map(_.toLower),
trace('split 之后'), split(' '))
console.log(f('NEVER SAY DIE'))
  • lodash/fp
    • lodash的fp模块提供了使用的对函数式编程友好的方法
    • 提供了不可变auto-curried-first data-last的方法
// lodash 模块
const _ = require('lodash')
_.map(['a', 'b', 'c'], _.toUpper)
// => ['A', 'B', 'C']
_.map(['a', 'b', 'c'])
// => ['a', 'b', 'c']
_.split('Hello World', ' ')
// lodash/fp 模块
const fp = require('lodash/fp')
fp.map(fp.toUpper, ['a', 'b', 'c'])
fp.map(fp.toUpper)(['a', 'b', 'c'])
fp.split(' ', 'Hello World')
fp.split(' ')('Hello World')
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

4.Point Free

Point Free:我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参
数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。

  • 不需要指明处理的数据
  • 只需要合成运算过程
  • 需要定义一些辅助的基本运算函数
const f = fp.flowRight(fp.join('-'), fp.map(_.toLower), 1 fp.split(' '))
  • 案例演示
// 非 Point Free 模式
// Hello World => hello_world
function f (word) {
	return word.toLowerCase().replace(/\s+/g, '_');
}
// Point Free
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
console.log(f('Hello World'))
  • 使用Point Free的模式,把单词中的首字母提取并转换成大写
const fp = require('lodash/fp')
const firstLetterToUpper = fp.flowRight(join('. '),
fp.map(fp.flowRight(fp.first, fp.toUpper)), split(' '))
console.log(firstLetterToUpper('world wild web'))
// => W. W. W

今日分享截止到这里,明天继续更新后续部分:Functor(函子)

记录:2020/10/28

下一篇:函子

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

5coder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值