js函数式编程基础:高阶函数柯理化函数合成loadash

拖泥羊      2022-02-10     230

关键词:

一、函数式编程

什么是函数式编程(FP)?

  1. FP是一种编程范式,也是一种编程风格,和面向对象是并列的关系。
  2. FP用于描述数据或函数之间的映射;根据输入通过某种运算获得相应的输出,即映射关系,例如:y=sin(x)。
  3. FP需要有输入和输出,相同的输入有相同的输出(这种称之为纯函数)。所以我们可以利用这个特点重用该函数,达到代码重用的目的。

函数式编程的常见应用场景

  • ES6中的map、filter、some等高阶函数。
  • React的高阶组件使用了高阶函数来实现,高阶函数就是函数式编程的一个特性。Redux也使用了函数式编程的思想。
  • Vue3也开始拥抱函数式编程,Vue2中也有一些高阶函数。
  • Webpack打包过程中利用tree shaking过滤无用代码。
  • 有很多库可以帮助我们进行函数式开发:lodash、underscore、ramda。

函数式编程的好处

  • 综合高阶函数、纯函数、柯理化、函数组合等好处

二、高阶函数

什么是高阶函数?高阶函数有以下几种特性:

  • 函数作为变量(函数是一等公民特性)
  • 函数作为参数,例如:map、filter、some等(高阶函数特性)
  • 函数作为返回值,例如:闭包、节流、防抖函数等 (高阶函数特性)

使用高阶函数的好处?

  • 高阶函数可以帮助我们重用、抽象一些过程代码,让代码复用率更高

常用的高阶函数?

  • forEach
  • map
  • filter
  • every
  • some
  • find/findIndex
  • reduce
  • sort

例如:map、some、函数节流与防抖等

const map = (array, fn) => { 
    let results = [] 
    for (const value of array) { 
        results.push(fn(value)) 
    }
    return results 
}

// test
let arr = [1, 2, 3, 4]
arr = map(arr, v => v * v)
console.log(arr)
// some 判断数组中是否有一个元素满足我们指定的条件,满足是true,都不满足为false
const some = (array, fn) => { 
    let result = false 
    for (const value of array) {
        result = fn(value) 
        // 如果有一个元素不满足就直接跳出循环
        if (result) { 
            break 
        }
    }
    return result
}

// test
let arr = [1, 3, 4, 9]
let arr1 = [1, 3, 5, 9]
let r = some(arr, v => v % 2 === 0)
console.log(r) // true
r = some(arr1, v => v % 2 === 0)
console.log(r) // false
// once
// 函数节流,让函数只执行一次
function once(fn) {
    let done = false
    return function() {
        // 判断值有没有被执行,如果是false表示没有执行,如果是true表示已经执行过了,不必再执行
        if(!done) {
            done = true
            // 调用fn,当前this直接传递过来,第二个参数是把fn的参数传递给return的函数
            return fn.apply(this, arguments)
        }
    }
}

// test
let pay = once(function (money) {
    console.log(`支付:${money} RMB`)
})

pay(5) //支付:5 RMB
pay(5)
pay(5)
pay(5)
pay(5)

三、纯函数

什么是纯函数?

  • 相同的输入有相同的输出而且没有副作用,常见的纯函数slice
  • 纯函数必须要有输入输出
let numbers = [1, 2, 3, 4, 5] 
// 纯函数 
// 对于相同的函数,输出是一样的

// slice方法,截取的时候返回截取的函数,不影响原数组
numbers.slice(0, 3) // => [1, 2, 3] 
numbers.slice(0, 3) // => [1, 2, 3] 
numbers.slice(0, 3) // => [1, 2, 3] 

// 不纯的函数 
// 对于相同的输入,输出是不一样的

// splice方法,返回原数组,改变原数组
numbers.splice(0, 3) // => [1, 2, 3] 
numbers.splice(0, 3) // => [4, 5] 
numbers.splice(0, 3) // => []

纯函数有什么好处:

  • 可缓存(根据相同的传入参数,例如Loadash中的memoize()方法)
  • 可测试
  • 并行处理
    多线程环境下并行操作共享的内存数据很可能会出现意外情况。纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数。(虽然JS是单线程,但是ES6以后有一个Web Worker,可以开启一个新线程)

怎么理解函数的副作用?
如果一个函数依赖于外部的状态就无法保证相同的输出,就会带来副作用,如下面的例子:

// 不纯的函数,因为它依赖于外部的变量
let mini = 18 
function checkAge (age) { 
    return age >= mini 
}

副作用来源有哪些?

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

四、函数的柯理化

什么是函数的柯理化?

  • 柯里化(Currying)是把多个参数的函数变换成单一参数的函数
  • 柯理化可以对函数参数进行缓存
//柯理化示例
const _ = require('lodash')

function getSum (a, b, c) {
  return a + b + c
}

const curried = _.curry(getSum)

console.log(curried(1, 2, 3))  // 6
console.log(curried(1)(2, 3))  // 6
console.log(curried(1, 2)(3))  // 6

柯理化的模拟实现

// 写法一:
function curry(fn) {
    function _c(restNum, argsList) {
        return restNum === 0 ?
        // fn.apply(null, argsList) 
        fn.apply(null, argsList) :
        function(x) {
            return _c(restNum - 1, argsList.concat(x));
        };
    }
    return _c(fn.length, []);
}

// 使用
var plus = curry(function(a, b) {
    return a + b;
});

// 3
console.log(plus(1)(2));
  • 入参出参:调用传递一个纯函数的参数,完成之后返回一个柯里化函数
  • 入参情况分析:
    如果curried调用传递的参数和getSum函数参数个数相同,那么立即执行并返回调用结果
    如果curried调用传递的参数是getSum函数的部分参数,那么需要返回一个新的函数,并且等待接收getSum的其他参数
  • 重点关注:
    获取调用的参数
    判断个数是否相同
// 写法二:模拟柯里化函数
function curry (func) {
  // 取名字是为了下面实参小于形参的时候用的
  return function curriedFn(...args) {
    // 判断实参和形参的个数
    if(args.length < func.length) {
      return function() {
        // 等待传递的剩余参数,如果剩余函数的参数加上之前的参数等于形参,那么就返回func
        // 第一部分参数在args里面,第二部分参数在arguments里面,要将两个合并并且展开传递(使用...)
        // concat函数要合并两个数组,arguments为伪数组,所以用Array.from进行转换
        return curriedFn(...args.concat(Array.from(arguments)))
      }
    }
    // 如果实参大于等于形参的个数
    // args是剩余参数,是个数组形式,而返回的时候要展开(使用...)
    return func(...args)
  }
}


// test
const curriedTest = curry(getSum)

console.log(curriedTest(1, 2, 3))  // 6
console.log(curriedTest(1)(2, 3))  // 6
console.log(curriedTest(1, 2)(3))  // 6

五、函数的合成

什么是函数的合成?
如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这叫做"函数的合成"(compose)。

为什么要合成函数?
纯函数和柯里化很容易写出洋葱代码 h(g(f(x))),函数的合成可以简化一系列操作

函数的合成默认是从右到左执行

// 函数组合演示
function compose(f, g) {
  return function (value) {
    return f(g(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])) // 4

函数的合成实现原理

//入参不固定,参数都是函数,出参是一个函数,这个函数要有一个初始的参数值
function compose (...args) {
  // 返回的函数,有一个传入的初始参数即value
  return function (value) {
    // ...args是执行的函数的数组,从右向左执行那么数组要进行reverse翻转
    // reduce: 对数组中的每一个元素,去执行我们提供的一个函数,并将其汇总成一个单个结果
    // reduce的第一个参数是一个回调函数,第二个参数是acc的初始值,这里acc的初始值就是value

    // reduce第一个参数的回调函数需要两个参数,第一个参数是汇总的一个结果,第二个参数是如果处理汇总的结果的函数并返回一个新的值
    // fn指的是数组中的每一个元素(即函数),来处理参数acc,完成之后下一个数组元素处理的是上一个数组的结果acc
    return args.reverse().reduce(function (acc, fn) {
      return fn(acc)
    }, value)
  }
}


//test
const fTest = compose(toUpper, first, reverse)
console.log(fTest(['one', 'two', 'three'])) // THREE


// ES6的写法(函数都变成箭头函数)
const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)

六、常用的函数式编程库Loadash,柯理化/函数合成更简单(重要)

学习了函数式编程的概念,最重要的是我们要会使用Loadash库进行一些简单的应用。

参考资料:Loadash官网

Loadash的安装

> npm i --save lodash

Loadash常用的方法

  • curry(),创建一个函数,该函数接收一个或多个 func的参数,如果 func 所需要的参数都被提供则执行 func 并返回执行的结果。否则继续返回该函数并等待接收剩余的参数。
  • flow() 组合多个函数,是从左到右运行
  • flowRight() 组合多个函数。是从右到左运行,使用的更多一些 (注意:执行完的前一个函数结果需要是后一个函数的入参,数据类型也要主要匹配;)
  • memoize() 可以缓存函数执行,相同的参数使用缓存
// Loadash中柯理化curry()示例

const _ = require('lodash')

// 参数是一个的为一元函数,两个的是二元函数
// 柯里化可以把一个多元函数转化成一元函数
function getSum (a, b, c) {
  return a + b + c
}

// 定义一个柯里化函数
const curried = _.curry(getSum)

// 如果输入了全部的参数,则立即返回结果
console.log(curried(1, 2, 3)) // 6

//如果传入了部分的参数,此时它会返回当前函数,并且等待接收getSum中的剩余参数
console.log(curried(1)(2, 3)) // 6
console.log(curried(1, 2)(3)) // 6
// Loadash中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(['one', 'two', 'three'])) // THREE
// Loadash中memoize()示例:
const _ = require('lodash')

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

let getAreaWithMemory = _.memoize(getArea)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
// 4
// 50.26548245743669
// 50.26548245743669
// 50.26548245743669

// 看到输出的4只执行了一次,因为其结果被缓存下来了
// 自定义实现memoize()缓存函数
function memoize (f) {
  let cache = {}
  return function () {
    // arguments是一个伪数组,所以要进行字符串的转化
    let key = JSON.stringify(arguments)
    // 如果缓存中有值就把值赋值,没有值就调用f函数并且把参数传递给它
    cache[key] = cache[key] || f.apply(f,arguments)
    return cache[key]
  }
}

let getAreaWithMemory1 = memoize(getArea)
console.log(getAreaWithMemory1(4))
console.log(getAreaWithMemory1(4))
console.log(getAreaWithMemory1(4))
// 4
// 50.26548245743669
// 50.26548245743669
// 50.26548245743669

七、Loadash其它常用方法:

参考:https://www.lodashjs.com/docs/lodash.join

// 转换成小写:
_.toLower("NERVER"]) 
// "nerver"

// 分割字符串,最多展示limit个
_.split("nerver-say-die", "-", [limit]) 
// ["nerver","say","die"]

// 组合数组为字符串
_.join(["nerver","say","die"], "-") 
// "nerver-say-die"

// 类似于ES6的map,参数位置不同
_.map(array, item=>console.log(item)) 

八、关于flowRight的使用和调试(函数组合)

// NEVER SAY DIE --> nerver-say-die

const _ = require('lodash')
 
const split = _.curry((sep, str) => _.split(str, sep))
const join = _.curry((sep, array) => _.join(array, sep))

// 我们需要对中间值进行打印,并且知道其位置,用柯里化输出一下
const log = _.curry((tag, v) => {
  console.log(tag, v)
  return v
})

// 从右往左在每个函数后面加一个log,并且传入tag的值,就可以知道每次结果输出的是什么
const f = _.flowRight(join('-'), log('after toLower:'), _.toLower, log('after split:'), split(' '))
// 从右到左
//第一个log:after split: [ 'NEVER', 'SAY', 'DIE' ] 正确
//第二个log: after toLower: never,say,die  转化成小写字母的时候,同时转成了字符串,这里出了问题
console.log(f('NEVER SAY DIE')) //n-e-v-e-r-,-s-a-y-,-d-i-e


// 修改方式,利用数组的map方法,遍历数组的每个元素让其变成小写 
// 这里的map需要两个参数,第一个是数组,第二个是回调函数,需要柯里化
const map = _.curry((fn, array) => _.map(array, fn))

// split(' ')(str),入参为一个string,返回一个array
// map(_.toLower)(array),入参为array,返回一个array
// join('-'),入参为array,返回一个string
const f1 = _.flowRight(join('-'), map(_.toLower), split(' '))
console.log(f1('NEVER SAY DIE')) // never-say-die

九、Loadsh中的FP模块

1、FP模块中的函数调整了参数位置,遵循数据之后、函数优先的特点;即参数中通常将函数放到前面。
2、FP模块中的函数都被柯理化了,方便函数组合

// lodash 模块 
const _ = require('lodash')
// 数据置先,函数置后
_.map(['a', 'b', 'c'], _.toUpper) 
// => ['A', 'B', 'C'] 
_.map(['a', 'b', 'c']) 
// => ['a', 'b', 'c'] 

// 数据置先,规则置后
_.split('Hello World', ' ') 

//BUT
// 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

特别鸣谢:拉勾教育前端高薪训练营

柯理化函数编程思想(代码片段)

柯理化函数编程思想:  函数柯里化(functioncurrying)又称部分求值。一个currying的函数首先会接受一些参数,接受了这些参数后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包里被保... 查看详情

11.高阶函数(匿名/*递归/函数式)对象编程基础

高阶函数匿名函数匿名函数存在的情况:内置函数函数式编程递归函数式编程面向对象的程序设计类:实例:OOP类的名称空间/对象的名称空间高阶函数匿名函数lambdax:x+y#returnx+y定义标志/参数(形式类似函数传参)/跟表达式(返... 查看详情

函数式编程基础---高阶函数和偏函数

一、高阶函数  所谓高阶函数是指可一把函数作为参数,或者是可以将函数作为返回值的函数(我们见得闭包函数就是高阶函数)。functionfoo(x){returnfunction(){returnx;}}  对于程序的编写,高阶函数比普通函数要灵活的多,除了... 查看详情

柯理化

在JS中柯里化就是把一个需要传入多个参数的函数变成多个嵌套的只要传入一个参数的函数在普通函数中的柯理化:varadd=function(x,y){ returnx+y;}柯里化:varaddCurring=function(x){ returnfunction(y){ returnx+y;}} addCurring(1)(2);//3&nbs... 查看详情

scala函数(柯理化)(代码片段)

scala函数柯理化,上代码,一目了然objectFunctiondefmain(args:Array[String])valname="zhangsan"valid="001"valfirst=showInfo(name)valsecond=first(id)second("playbaseketball")second("swiming")showInfo1(name)(id)("play 查看详情

函数式编程的类型转换

1、基础类型转换为高阶类型(monad),以便使用函数式编程的特性:map、reduce,pipeline、业务组织、异步编程等;2、高阶类型转化为基础类型:以便使用基础类型的态射(计算)功能。3、高阶类型的内部转换(泛型类型转换)--... 查看详情

scala学习(函数式编程面向对象编程)(代码片段)

文章目录函数式编程基础函数编程函数定义函数参数函数至简原则高阶函数编程面向对象编程基础面向对象编程高阶面向对象编程函数式编程基础函数编程函数定义packagelearn03objectdemo01defmain(args:Array[String]):Unit=//无参、无返回... 查看详情

scala学习(函数式编程面向对象编程)(代码片段)

文章目录函数式编程基础函数编程函数定义函数参数函数至简原则高阶函数编程面向对象编程基础面向对象编程高阶面向对象编程函数式编程基础函数编程函数定义packagelearn03objectdemo01defmain(args:Array[String]):Unit=//无参、无返回... 查看详情

高阶函数编程

 高阶函数编程:  高阶函数编程是在原有的编程基础上加以优化,使得代码在编译或阅读的过程中更加的方便。  不过个人觉得,高阶函数编程的使用过程中,首先需要一个人对函数使用和运算方面有很强或很熟练的思... 查看详情

kotlin函数式编程①(函数式编程简介|高阶函数|函数类别|transform变换函数|过滤函数|合并函数|map变换函数|flatmap变换函数)(代码片段)

文章目录一、函数式编程简介1、编程范式2、高阶函数3、函数式编程4、前端开发技术二、函数类别三、变换函数四、map变换函数1、map函数原型分析2、map函数设计理念3、代码示例五、flatMap变换函数1、flatMap函数原型分析2、代码... 查看详情

js高阶函数filterreducemap(代码片段)

...程/声明式编程//编程范式:面向对象编程(第一公民:对象)/函数式编程(第一公民:函数)//filter/map/reduce//filter中的回调函数有一个要求:必须返回一个boolean值//true:当返回true时,函数内部会自动将这次回调的n加入到新的数组中//false:当... 查看详情

函数式编程和高阶函数

函数式编程函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设... 查看详情

函数式编程

python下的函数式编程  函数式编程允许有变量;支持高阶函数(函数可以作为变量传入);支持闭包(可以返回函数);有限度地支持匿名函数高阶函数1.定义:能够接收函数作为参数的函数就是高阶函数,下面举个例子... 查看详情

python基础--函数式编程(代码片段)

#面向过程#函数式编程:函数式=编程语言定义的函数+数学意义上的函数#面向对象#高阶函数:1.函数接受的参数是一个函数名2.返回值中包含函数#deffoo(n):#n=bar#print(n)##defbar(name):#print(‘mynameis%s‘%name)###foo(bar)#把函数当做参数传给另... 查看详情

函数式编程

高阶函数我们在编写大段代码的时候会将其拆分成函数,这就将复杂任务转化为多个简单任务,便于程序的编写而高阶函数,简化而言就是在函数的基础上套用函数,提高代码的利用率使用map函数将其改为名字输入方式>>>... 查看详情

函数式编程语言

  函数式编程语言(functionalprogramlanguage)是种编程方式,它将电脑运算视为函数的计算。函数编程语言最重要的基础是λ演算(lambdacalculus),而且λ演算的函数可以接受函数当作输入(参数)和输出(返回值)... 查看详情

javascript系列:函数式编程(开篇)

前言:上一篇介绍了函数回调,高阶函数以及函数柯里化等高级函数应用,同时,因为正在学习JavaScript·函数式编程,想整理一下函数式编程中,对于我们日常比较有用的部分。 为什么函数式编程很重要?   学习... 查看详情

高阶函数和装饰器

函数式:一种编程范式纯函数式编程:没有变量,支持高阶函数编程 Python不是纯函数式编程语言,支持高阶函数编程变量可以指向函数,函数名就是指向函数的一个变量,与普通变量没有区别 高阶函数:能接收函数做参... 查看详情