小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金
柯里化
维基百科上说道:柯里化,英语:Currying(果然是满满的英译中的既视感),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
这里我们先提供一个checkAge
函数,判断用户输入输入的年龄是否大于基准值
例子
function checkAge (age) {
let min = 18
return age >= min
}
复制代码
我们这样在函数内部直接定义min的值,这样就是硬编码(硬编码是将数据直接嵌入到程序或其他可执行对象的源代码中的软件开发实践,与从外部获取数据或在运行时生成数据不同)
这个时候就有万恶的产品经理跳出来了:昂?我们想要的是更加灵活的,靠用户自己做判断的,怎么能你们做判断呢?
带着万恶产品经理的述求,我们的升级版1.0,带着它六亲不认的步伐走来了
升级1.0:
function checkAge (min,age) {
return age >= min
}
复制代码
这样我们就把硬编码的函数改造成了纯函数,让函数更加灵活了,不过这样子也有一些比较麻烦的事情,比如下面的例子
checkAge(18, 22)
checkAge(18, 23)
checkAge(22, 22)
checkAge(22, 10)
复制代码
我们需要多次使用,同一个基准值进行比较的时候,就需要多次输入参数,让我这个强迫症觉得很不舒服。那么问题来了,该怎么改造呢?
听到了我这个强迫症的呼唤,强势升级版2.0带着他的代码走来了!
升级2.0
function checkAge(min) {
return function(age) {
return age >= min
}
}
复制代码
那刚刚那几个例子,我们就可以用如下的方法解决了
// 用户基准值为 18
let checkAge18 = checkAge(18)
checkAge18(22)
checkAge18(23)
// 用户基准值为 22
let checkAge22 = checkAge(22)
checkAge18(22)
checkAge18(10)
复制代码
其实这个就是函数的柯里化。
我感觉吧,这么多能不能优化一点啊?
带着我这个拮据要求的新一代代码,携带者箭头函数,它es6
,被称为前端魔法的划时代奠基人的es6
,面试问的最多的es6
,他来了!
使用es6的箭头函数重写checkAge
let checkAge = min => (age => age >= min )
复制代码
这样进行修改之后,我们一行就解决了这个checkAge
方法,相比之前的checkAge
2.0版本,我们做到了质的飞跃,我愿称es6
为最强
想起来一个面试问题(关于箭头函数的this指向和普通函数的this指向)
Q: 箭头函数的this指向是在定义的时候确定还是在引用的时候确定的呢?那普通函数的this指向呢?
A: 箭头函数的this指向,和箭头函数定义所在上下文的this相同。对于普通函数,this在函数调用时才确定;而对于箭头函数,this在箭头函数定义时就已经确定了,并且不能再被修改
什么是函数的柯里化?
当我们一个函数需要传入多个参数的时候,我们可以对这个函数进行改造,我们可以调用一个函数,只传递部分的参数,并且让这个函数返回一个新的函数,这个新的函数接收剩余的参数,并且返回剩下的结果,
一句话总结:在函数式编程中,函数柯里化是指将接受多个参数的函数转为接受不定个数参数的高阶函数。 从功能上说,它实现了函数功能的细化。
Lodash中柯里化的应用
通过上一个例子,我们理解了柯里化和柯里化的使用,但是这个checkAge
函数并不通用,那该怎么通用呢?
听到了我们的呼唤,那个被称为【前端救护者】的Lodash
挺身而出!不过我们这次用的就是curry
这个函数,Lodash
妥妥工具库;
curry
参数和功能介绍
-
功能:
创建一个函数,该函数接收
func
的参数,要么调用func
返回的结果,如果func
所需参数已经提供,则直接返回func
所执行的结果。或返回一个函数,接受余下的func
参数的函数,可以使用func.length
强制需要累积的参数个数。 -
参数:
func
(Function) : 用来柯里化(curry)的函数。[arity=func.length]
(number) : 需要提供给func
的参数数量。
-
返回:
(Function) : 返回新的柯里化(curry)函数。
curry
使用
柯里化最终可以把多元函数,转换成一元函数
const _ = require("lodash")
function sum(a, b, c) {
return a + b + c;
};
const curried = _.curry(sum);
// 好像这样似乎也没必要用这个方法是不是
curried(1, 2, 3); // => 6
// 看看这个
curried(1)(2)(3); // => 6
// 再看看这个
curried(1, 2)(3); // => 6
复制代码
curry
进行柯里化的时候,会按照我们传入的参数进行返回,如果需要入参为3个时候,我们传入了两个,curry
会返回一个需要剩余入参的函数。纤细看下面的分析!
到这,会不会发现这个柯里化的方法很好用,可是再好用轮子还是别人造的,总用别人的轮子也不太好,还是我们自己上!
柯里化原理模拟
经过上一节我们已经理解到,curry
需要的参数为fun
和需要提供给 func
的参数数量,
curry
的几种使用形式
function getSum (a,b,c) {
return a + b + c
}
const curried = _.curry(getSum)
复制代码
我们先看看 curried
这个函数都可以怎么调用
-
传入全部参数
curried(1,2,3) 复制代码
如果我们传入的参数和
getSum
需要的参数一致的话,我们就会立即调用getSum
,并返回这个执行结果 -
传入部分参数
curried(1,2)(3) curried(1)(2)(3) curried(1)(2,3) 复制代码
如果我们传入部分参数,此时
curried
返回返回一个新的函数,并且等待getSum
剩余的其他参数
经过上述的分析,我们返回这个函数,在调用的时候需要接受参数,而且接收参数的个数是不固定的,我们需要获取到全部的参数,且我们需要进行一下判断,形参的个数是否相同,那么带着以上的需求,我们正式进行模拟实现。
代码模拟
- 我们需要可以接收一个函数,并且返回一个函数
function curry(fun) {
return function () {
...
}
}
复制代码
- 我们需要返回的函数可以接收新的参数,我们可以通过
es6
的剩余参数进行获取
function curry(fun) {
return function (...args) {
...
}
}
复制代码
- 我们要进行实参的个数和形参的个数是否相同
- 实参的个数:
args
是个数组,我们可以用args.length
获取我们实际调用本次函数的时候的参数数量。 - 形参的格式:就是我们传入的
fun
这个函数的形式参数的个数,我们要获取fun
的形参个数,我们可以通过fun.length
进行获取 - 如果 实参 < 形参 那么就返回一个新的函数,接收剩余的参数值
- 如果 实参 >= 形参 那么就直接调用fun,返回执行结果
实参 >= 形参
```js
function curry(fun) {
return function (...args) {
// 判断实参和形参的个数
// 实参 < 形参 那么就返回一个新的函数,接收剩余的参数值
if(args.length < fun.length){
return function() {
...
}
}
// 实参 >= 形参 那么就直接调用fun,返回执行结果
return fun(...args)
}
}
```
复制代码
实参 < 形参
实参 < 形参的话,我们每次调用函数的时候,我们就需要把之前的情况保留起来,并且每次都传递给下一个重新生成函数
```js
function curry(fun) {
return function curriedFn(...args) {
// 判断实参和形参的个数
// 实参 < 形参 那么就返回一个新的函数,接收剩余的参数值
if(args.length < fun.length){
return function() {
// arguments 是伪数组,所以需要使用 Array.from
return curriedFn(...args.concat(Array.from(arguments)))
}
}
// 实参 >= 形参 那么就直接调用fun,返回执行结果
return fun(...args)
}
}
```
复制代码
到这我们的curry
就实现完毕了。
总结
- 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
- 这是一种对函数参数的缓存
- 让函数变得更加灵活,让函数的粒度更小
- 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能
我们用checkAge
演示了柯里化的使用,但是它并不通用,所以我们引入了 lodash
中的柯里化方法,并且按照其中的curry
,我们也模拟实现了一下 curry
函数。