进阶学习2:函数式编程FP——闭包纯函数Lodash柯里化
Posted JIZQAQ
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进阶学习2:函数式编程FP——闭包纯函数Lodash柯里化相关的知识,希望对你有一定的参考价值。
目录
三、闭包
1.闭包的概念
闭包 (Closure):函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员
前面的样例(又粘过来了),还有前面的once函数其实都已经使用到了闭包的概念。
function makeFn () {
let msg = 'Hello function'//外部函数内的成员
return function () {
console.log(msg)//这个内部函数的作用域中调用了外部函数作用域的成员msg
}
}
const fn = makeFn()
fn()
闭包的本质:函数在执行的时候会放到一个执行栈上当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员
2.闭包案例
1.求number的power次方
//DEMO8
//闭包
function makePower(power) {
return function (number){
return Math.pow(number, power)
}
}
let power2 = makePower(2)
let power3 = makePower(3)
console.log(power2(4))
console.log(power2(5))
console.log(power3(3))
结果:
通过debug模式,打断点,可以大概了解闭包是怎么工作的。
2.通过级别计算员工总工资
求员工工资,每个员工工资由基本工资+绩效工资组成。相同级别的员工,基本工资是相同的,我们需要做的是让员工直接按照级别+绩效工资来计算出自己的总工资。
//DEMO9
//闭包 工资计算
function makeSalary(base) {
return function (performance){
return base + performance
}
}
let level1 = makeSalary(12000)
let level2 = makeSalary(15000)
console.log(level1(3000))
console.log(level2(2000))
结果:
同样通过打断点,来观察闭包是何时生成的。
四、纯函数Pure Functions
1.纯函数的概念
纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
lodash 是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法
2.纯函数和不纯函数样例
数组的 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)
// => []
函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的),我们可以把一个函数的执行结果交给另一个函数去处理。
3.Lodash
Lodash 通过降低 array、number、objects、string 等等的使用难度从而让 javascript 变得更简单。 Lodash 的模块化方法 非常适用于:
- 遍历 array、object 和 string
- 对值进行操作和检测
- 创建符合功能的函数
英文官网
中文官网
英文官网右上角有文档和函数式编程的指导文档。
lodash安装
npm install lodash
lodash使用
//DEMO10
//lodash 使用(这些都是纯函数)
//first / last / toUpper / reverse / each / includes / find / findIndex
const _ = require('lodash')
const array = ['jack', 'tom', 'lucy', 'kate']
//获取第一个元素
console.log(_.first(array))
//获取最后一个元素
console.log(_.last(array))
//全部转换为大写
console.log(_.toUpper(array))
//顺序全部反转。这里的reverse和 array.reverse()区别是,lodash的是纯函数,使用后array不改变,而array.reverse()使用后,array本身被反转
console.log(_.reverse(array))
//each遍历
_.each(array, (item, index) =>{
console.log(item, index)
})
//下面两个例子老师课堂上没有演示,根据官方文档做了一下尝试
//_.find(collection, [predicate=_.identity], [fromIndex=0])#
//遍历 collection(集合)元素,返回 predicate(断言函数)第一个返回真值的第一个元素。predicate(断言函数)调用3个参数: (value, index|key, collection)。
console.log(_.find(array, function(item) { return item == 'peter'; }))//不存在的话返回undefined
console.log(_.find(array, function(item) { return item == 'tom'; }))
//_.findIndex(array, [predicate=_.identity], [fromIndex=0])#
//该方法类似_.find,区别是该方法返回第一个通过 predicate 判断为真值的元素的索引值(index),而不是元素本身。
console.log(_.findIndex(array, function(item) { return item == 'peter'; }))//不存在的话返回-1
console.log(_.findIndex(array, function(item) { return item == 'tom'; }))
返回的结果:
4.纯函数的好处
1.可缓存
因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
下面是记忆函数的一个例子
用到了lodash的memoize函数,先从官网了解一下memoize函数
//DEMO10
//纯函数的好处:可缓存
//记忆函数
const _ = require('lodash')
function getArea(r){
console.log(r)
return r * r * Math.PI
}
let getAreaWithMemory = _.memoize(getArea)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
运行结果如下,可以看到其实只有第一次运行了getArea函数,后面都是直从缓存获取的。
接下来亲自模拟一下memoize函数的实现
apply:方法能劫持另外一个对象的方法,继承另外一个对象的属性.
Function.apply(obj,args)方法能接收两个参数 obj:这个对象将代替Function类里this对象 args:这个是数组,它将作为参数传给Function(args-->arguments)
//DEMO11
//纯函数的好处:可缓存
//记忆函数模拟
function getArea(r){
console.log(r)
return r * r * Math.PI
}
function memoize(fn){
let cache = {}
return function () {
console.log("arguments:"+JSON.stringify(arguments))
let key = JSON.stringify(arguments)
cache[key] = cache[key] || fn.apply(fn, arguments)
return cache[key]
}
}
let getAreaWithMemory = memoize(getArea)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
打印结果如下:
2.可测试
纯函数让测试更方便
3.并行处理
在多线程环境下并行操作共享的内存数据很可能会出现意外情况
纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数 (ES6 之后Web Worker可以多线程)
5.副作用
纯函数:对于相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
// 不纯的
let mini = 18
function checkAge (age) {
return age >= mini
}
// 纯的(有硬编码,后续可以通过柯里化解决)
function checkAge (age) {
let mini = 18
return age >= mini
}
副作用让一个函数变的不纯(如上例),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部 的状态就无法保证输出相同,就会带来副作用。
副作用来源:
- 配置文件
- 数据库
- 获取用户的输入
- ……
所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作 用会给程序中带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控 范围内发生。
6.柯里化(Haskell Brooks Curry)
1.柯里化概念
柯里化:当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接收剩余的参数,返回结果
使用柯里化解决上面样例遇到的问题
//DEMO12
//柯里化演示
//普通的纯函数
function checkAge (age, mini) {
return age >= mini
}
console.log(checkAge(17, 18))
console.log(checkAge(24, 18))
console.log(checkAge(24, 22))
//现在18这个基准值经常重复,于是回忆以前闭包的概念
//函数的柯里化
function checkAge2 (mini) {
return function(age){
return age >= mini
}
}
const mini18 = checkAge2(18);
const mini22 = checkAge2(22);
console.log(mini18(17))
console.log(mini18(24))
console.log(mini22(24))
//函数的柯里化,使用箭头函数再实现一次(ES6)
//const checkAge3 = (mini) => (age) => {return age >= mini} ;
//最简化的写法
const checkAge3 = mini => age => age >= mini ;
const mini18_2 = checkAge3(18);
const mini22_2 = checkAge3(22);
console.log(mini18_2(17))
console.log(mini18_2(24))
console.log(mini22_2(24))
输出的结果:
2.Lodash中柯里化方法
_.curry(func)
功能:创建一个函数,该函数接收一个或多个 func 的参数,如果 func 所需要的参数都被提 供则执行 func 并返回执行的结果。否则继续返回该函数并等待接收剩余的参数。
参数:需要柯里化的函数
返回值:柯里化后的函数
//DEMO13
//lodash中的柯里化
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))//curry后,我们可以传递原本函数需要的部分参数,然后curry会返回一个函数接收剩余的参数
console.log(curried(1)(2)(3))
3.柯里化案例
//DEMO14
//柯里化安利
''.match(/\\s+/g)//匹配空白字符 +:多个 g:全局匹配
''.match(/\\d+/g)//匹配提取所有的数字
//如果判断数组中的话,前面的办法就没办法重用了,所以还是需要函数
//function match(reg, str){
// return str.match(reg)
//}
//但是每次输入正则表达式也很麻烦,所以还是需要柯里化
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(haveSpace('helloworld'))
console.log(haveNumber('abc123'))
console.log(haveNumber('abc'))
const filter = _.curry(function(func, array){
return array.filter(func)
})
console.log(filter(haveSpace,['John Connor','John_Connor']))
const findSpace = filter(haveSpace)
console.log(findSpace(['John Connor','John_Connor']))
//改造成箭头函数
const filter2 = _.curry((func, array)=> array.filter(func))
console.log(filter2(haveSpace,['John Connor','John_Connor']))
const findSpace2 = filter2(haveSpace)
console.log(findSpace2(['John Connor','John_Connor']))
输出结果:
4.柯里化原理模拟
//DEMO15
//柯里化安利
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))//curry后,我们可以传递原本函数需要的部分参数,然后curry会返回一个函数接收剩余的参数
console.log(curried(1)(2)(3))
function curry (func) {
//...args剩余的参数
return function curriedFn(...args) {
//判断实参和形参的个数
//实际传入参数少于需要的,则返回一个函数
//args里面保存着之前传进来的参数
if(args.length < func.length){
return function() {
//每次拼接上arguments中的
return curriedFn(...args.concat(Array.from(arguments)))
}
}
//传入的参数就是需要的参数的数量
return func(...args)
}
}
5.柯里化总结
- 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
- 这是一种对函数参数的'缓存'
- 让函数变的更灵活,让函数的粒度更小
- 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能
参考资料:
1.购买的拉勾网《大前端训练营》课程
2.Lodash中文文档
https://www.lodashjs.com/docs/
3.W3School JavaScript 方法文档
以上是关于进阶学习2:函数式编程FP——闭包纯函数Lodash柯里化的主要内容,如果未能解决你的问题,请参考以下文章
进阶学习3:函数式编程FP——函数的组合组合函数模拟Lodash fp模块PointFree
进阶学习1:函数式编程FP——概念头等函数高阶函数常用高阶函数模拟
进阶学习4:函数式编程FP——函子FunctorMayBe函子Either函子IO函子FolktalePointer函子Monad