🤔 什么是柯里化?
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。 —— 百度百科
emmm…读起来有点绕口,没明白是什么意思?别急,我们来举个简单的🌰。
现在我们需要实现一个add函数, 函数执行结果返回三个参数之和。一般情况下,我们会这样写:
function add (x, y, z) {
return x + y + z;
}
add(1, 2, 3);
现在我们有一个新的需求,需要将add函数的调用方式转换成add(1)(2)(3);
这时,我们稍加思索,也能很容易的想到写出下面这种:
function add (x) {
return function (y) {
return function (x) {
return x + y + z;
}
}
}
// es6 简化写法
// const add = x => y => z => (x+y+z);
add(1)(2)(3);
实际上,将add函数进行转换的这个过程,我们就称之为函数柯里化。
⚠️需要注意的是,对于JavaScript语言的函数柯里化概念与 数学和计算机科学中的柯里化概念并无完全一致。通过文章开头百度百科对柯里化的说明,我们能够知道,在计算机科学中,柯里化函数只能接受单一参数,但是在JavaScript语言实际使用柯里化函数时,我们也可以传递一个或多个参数。
这也就是说柯里化之后的add函数,我们也可以这样调用:
add(1, 2)(3);
add(1)(2, 3);
...
显而易见,我们上面重写的add函数并不能满足这种形式的调用;
同时,假如现在我们需要计算add(1)(2)(3)(4)这样的功能,它也是束手无策。
由此可见,我们上面重写的add函数虽然也实现了函数柯里化,但是这并不是我们想要的效果。
🤔那我们怎么才能实现我们想要的柯里化呢?
红宝书上面给了我们一种函数柯里化实现的方法:
function curry(fn){
var args = Array.prototype.slice.call(arguments, 1);
return function (){
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return fn.apply(null, finalArgs);
}
}
function add(num1, num2){
return num1+num2;
}
var cur = curry(add,1);
console.log(cur(2,3));
在这种实现方式中,实现柯里化的函数curry的第一个参数为需要柯里化的函数,后面的参数可以传入,也可以传一部分,也可以不传。在调用柯里化之后的函数的时候再传入剩余的参数。
这种实现方式只能把参数列表通过两次传递(进行柯里化的时候和调用柯里化函数的时候),显然。这也并不是我们想要的效果。
在实现这个函数柯里化的方法之前,我们需要明确一下我们的需求:
1、每次传递的参数个数不固定;
2、调用的此时次数也不固定;
那么每次调用之后返回的就不能是一个值,而必须是一个函数,就如红宝书给出方法一样。那么现在面临的问题就是,由于调用的次数不确定,如果判断何时结束以及如何处理?
这里就需要用到一个细节知识:
对象(包括数组、对象、函数等)参与算数运算或者逻辑运算时,就会无参调用其toString或者valueOf方法得到一个原始值,然后参与运算。
基于上面的思路,我们可以实现如下:
function add () {
let args = [...arguments];
function _add () {
args = args.concat([...arguments]);
return _add;
}
_add.toString = function () {
return args.reduce((total, cur) => {
return total + cur;
})
}
return _add;
}
console.log(+add(1)(2)(3,4)(5));
// 这里需要使用‘+’运算符进行隐式类型转换,才会执行toString方法,达到我们想要的目的;
到这里,我们的一个累加功能的柯里化函数就实现了。
就我个人理解看来,柯里化是一种编程思想,也可以称之为一种编程技巧。它运用闭包的机制,把一些参数存储起来,提供给下级的上下文使用。
看到这里,大家肯定就会问了,费这一顿劲把它写成柯里化函数到底有什么好处呢?
先上结论,再一一细说:
1、参数复用;
2、提前确认;
3、延迟运行;
参数复用:
假如,我们需要写一个将一个数组每一个元素放大n倍的函数,一般情况下,我们会这样写:
function amplify = (arr, num) {
return arr.map(item => item * num);
}
而我们在调用的时候:
const arr1 = [1,2,3];
const arr2 = [3,6,9];
// 把 arr1放大3倍;
amplify(arr1, 3);
amplify(arr2, 3);
// 如果我们有很大数组都需要放大3倍,那我们每次调用的时候都需要第二个参数传一个3
柯里化之后:
function amplify (num) {
return function (arr) {
return arr.map(item => item * num);
}
}
// 使用es6语法更加简单
// const amplify => num => arr => arr.map(item => item *num);
// 如果需要将数组扩大3倍;
const amplify3 = amplify(3);
// 如果需要扩大5倍;
const amplify5 = amplify(5);
// 调用;
amplify3(arr1);
amplify3(arr2);
这样,我们就不用每次在调用的时候都需要将扩大的倍数传进去了,达到了参数复用的效果。
提前确认
比如我们在定义一个函数的时候依赖某个全局参数或者环境变量等进行一些判断,从而执行不同的操作时:
const fn = function () {
if (a) {
// 如果 a 存在的时候;
// do something...
} else {
// 如果 a 不存在时,进行另外的处理;
}
}
这样写虽然可以满足我们的需求,但是每次调用fn时,都会对a进行判断。
const fn = (function () {
if (a) {
return function () {
// do something 存在a时进行的处理
}
} else {
return function () {
// do something 不存在a时进行的出来
}
}
})();
// 一种更加清晰的写法;
const fn = function (a) {
if (a) {
return function () {
// do something 存在a时进行的处理
}
} else {
return function () {
// do something 不存在a时进行的出来
}
}
}
const func1 = fn(a);
// 然后再调用func1
通过这种写法,代码就能在自然执行时提前确认应该返回哪一个函数,不会在每次调用的时候再进行判断。
延迟执行
事实上,我们上面的描述和例子中已经体现出来它延迟执行的特性.
通过闭包的方式将传递的参数保存在返回的函数的上下文中,但是返回的函数并不是立即执行,而是等待调用。
JavaScript中常用的bind方法就是通过柯里化机制实现的:
Function.prototype.bind = function (context) {
const _this = this;
const args = Array.prototype.slice .call(arguments, 1);
return function () {
return _this.apply(context, args);
}
}
bind方法返回一个具有固定的this指向的新的函数。
通用柯里化工具函数实现:
function curring (fn) {
return function curried (...args) {
if(args.length >= fn.length) {
// 如果传入的参数个数比函数实际的参数个数多,直接执行并返回结果;
return fn.apply(this, args)
} else {
// 如果参数个数还不够,则继续收集参数;
return function(...rest) {
return curried.apply(this, [...args, ...rest]);
}
}
}
}
我们常用的工具库lodash也实现了curry函数,并且支持placeholders,通过占位符来改变参数传入的顺序。
var abc = function(a, b, c) {
return [a, b, c];
};
var curried = _.curry(abc);
// Curried with placeholders.
curried(1)(_, 3)(2);
// => [1, 2, 3]
组合函数
组合函数也是函数式编程中的一个重要概念,将执行过程的每一步放到一个功能专一的函数中,在需要使用的地方直接调用函数。函数式编程侧重于关注函数的计算结果,而不用关注函数的实现。
同样的,我们通过一个🌰来介绍组合函数以及它的作用:
现在,我们需要实现这样一个函数:给定一个数字数组,可能包含0、undefinedy以及null。然后将给定的数组进行去零、筛除大于10的元素,在将数组每项乘以3,然后输出结果数组。
我们先按照正常的写法来实现一下:
// 对一个数组 去零 筛选 再乘3
// 去零函数
const filterBoolean = arr => arr.filter(Boolean);
// 筛除掉大于10的元素, 这里我们可以就可以小用一下上面讲过的函数柯里化思想
const filterSmaller = num => arr => arr.filter(item => item <= num);
const filterSmaller10 = filterBigger(10);
// 将数组的每项乘以3
const multiply = num => arr => arr.map(item => item * num);
const multiply3 = multiply(3);
const arr = [null, 2, 0, 8, 12, 7, 3];
// 最后计算结果
const resArr = multiply3( filterSmaller10( filterBoolean(arr) ) )
可以看出,我们虽然实现了功能,但是在最后计算的时候,对多个函数进行了嵌套,但是可读性比较差,代码并不优雅。
正是基于这种情况,我们可以构建一个组合函数,函数可以接受任意函数为参数,每个参数函数只能接受一个参数,最后返回一个铺排后的函数,像下面这样:
const resFn = compose(a, b, c)
resFn(arr) // c(b(a(arr)));
基于这种思想我们可以实现以下的代码:
function compose (...funcs) {
return functiion(origin) {
rest.reduce((total, item) => {
return item(total);
}, origin)
}
}
// 简化写法
// const compose = (...rest) => startNum => rest.reduce((total, item) => item(total), startNum);
// 调用;
const modifyArr = compose(filterBoolean, filterSmaller10, multiply3);
const resArr = modifyArr(arr);
然后再对上面的代码稍加优化:
function compose (...funcs) {
return (x) => {
let len = funcs.length;
// 如果没有处理函数,直接返回原数据
if(len === 0) return x;
// 如果只有一个处理函数,直接执行并返回结果
if(len === 1) return funcs[0](x);
reutrn funcs.reduce((total, func) => {
return func(total);
}, x)
}
}
const resArr = compose(filterBoolean, filterSmaller10, multiply3)(arr)
组合函数的思想在前端框和库中有着广泛的使用,比如在redux中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wo6g444o-1668999769669)(https://flowus.cn/preview/79508fd8-93f7-4d8f-a4b8-2e1cee57e3d7)]
🚩到这里,本篇的内容就讲完了,函数柯里化和组合函数的思想在函数式编程中都非常重要,灵活适当的使用在我们日常的代码开发可以使我们的代码简洁优雅,增加代码的可读性。