什么是函数科里化?
简单来说,就是能将多次传入参数的函数转换为单次传入参数函数的过程
举个栗子
function multiFn(a, b, c) {
return a * b * c;
}
期望如下的输出,能得到上面这个函数一样的结果。
var newFun = currying(multiFn);
newFun(2)(3)(4);
newFun(2,3,4);
newFun(2)(3,4);
newFun(2,3)(4);
是不是跟bind函数的调用有点像。这个函数看着还是挺神奇的~~
显然,如果我们通过多次return函数的形式不能实现,因为这种方式是比较死板的。
函数科里化的两个最重要的特点就是:
- 可以多次延迟调用
- 传参比较灵活
核心步骤分析
从调用方式可以看出,函数什么时候执行,是跟原函数multiFn有密切联系的。实际上他们的关系是这样子的:
- 各种花里胡哨传入参数的总长度小于multiFn的长度时,代表参数还未到位,需要使用闭包来继续收集函数参数
- 当各种花里胡哨传入参数的总长度等于multiFn的长度时,代表参数到位了,可以直接执行函数了
则重点是需要明确(当前调用时的参数curArgs长度)+(之前传入的参数长度storeArgs)与 (未被科里化前的函数multiFn形参长度)的关系
接下来开始我们的代码实现部分。newFun(2,3)(4)的调用方式来说,首次过程分析如下:
- 先记录mutilFn形参个数,存储之前传入的参数,以便于之后的拼接及上述两者的参数长度比较。
function currying(func, storeArgs) {
var arity = func.length; // 记录目标函数mutilFn形参个数
var args = storeArgs || []; // 记录之前传入的参数集合
}
2.通过调用的方式明确是要返回一个函数,并且这个函数会被递归调用的。在此之前,先明确一个知识点。
[].slice.call(arguments)是将类数组arguments转换为数组,apply和call传入的第一个参数为null时,实际上指向的是window
return function(){
// 获取调用时传入的参数,将参数转化为数组。curArgs = [2,3]
var curArgs = [].slice.call(arguments);
// 将上次的参数与当前参数进行组合,并修正传参顺序。第一次时上一次存储的参数args为空,则拼接完curArgs为[2,3]
Array.prototype.unshift.apply(curArgs, args);
// 发现参数不够,返回闭包函数继续收集参数,并且需传回当前收集的参数curArgs.为了给调用的函数继续延迟调用,则需要返回函数而不是直接调用函数。
if(curArgs.length < arity) {
return currying.call(null, func, curArgs);
}
// 4、参数不够,不会被执行
return func.apply(null, curArgs);
}
3.分析第二次参数传入的过程
return function () {
// 获取调用时传入的参数,将参数转化为数组。curArgs=[4]
var curArgs = [].slice.call(arguments);
// 将上次的参数与当前参数进行组合,并修正传参顺序。上一次参数为[2,3],则拼接完curArgs=[2,3,4]
Array.prototype.unshift.apply(curArgs, args);
// 3、参数足够,跳过
if(curArgs.length < arity) {
return currying.call(null, func, curArgs);
}
// 4、参数够了,则直接执行被转化的函数
return func.apply(null, curArgs);
}
4.至此,按照ES5方式实现的代码完毕
function currying(func, storeArgs) {
var arity = func.length;
var args = storeArgs || [];
return function () {
var curArgs = [].slice.call(arguments);
Array.prototype.unshift.apply(curArgs, args);
if(curArgs.length < arity) {
return currying.call(null, func, curArgs);
}
return func.apply(null, curArgs);
}
}
根据如上的分析过程,可以采用es6实现参数默认值和参数转换为数组的这个步骤。可以得出ES6的实现如下:
function currying(func, storeArgs = []) {
var arity = func.length;
return function (...curArgs) {
curArgs.unshift(...storeArgs); // 将当前调用的参数融合进去既有的参数
if (curArgs.length < arity) { // 参数未到位,继续收集参数,并需将上次收集的参数继续传入
return currying.call(null, func, curArgs);
}
return func.apply(null, curArgs); // 参数到位,直接执行函数
}
}
function multiFn(a, b, c) {
return a *b *c;
}
var newFunc = currying(multiFn);
newFunc(1, 2)(3);
newFunc(1)(2, 3);
newFunc(1)(2)(3);
newFunc(1, 2, 3);
依据如上的思路,手写模拟实现bind函数。
bind函数的使用是延迟执行,第一个参数为调用对象,第二个参数为第一次传入的数值。整体思路为取出调用对象,取出所有应当被传入的数值参数,然后用apply来执行。
Function.prototype.bind = function () {
var fn = this; // 实际上就是bark函数
var args = Array.prototype.slice.call(arguments); // 将函数参数转化为数组。args有两个元素,第一个元素为调用对象,第二个参数为首次调用传入的数值
var context = args.shift(); // context为弹出调用对象,此时args只剩一个形参,为数值
return function () {
return fn.apply(context, args.concat(Array.prototype.slice.call(arguments)));
}
}
// eg :
function bark(animal, behavior) {
var str = `${animal} can ${behavior}`;
return str;
}
var cat = {};
var fn = bark.bind(cat, 'Tom');
var result = fn('eat');
console.log(result);
小结
- 各种花里胡哨传入参数的总长度小于multiFn的长度时,代表参数还未到位,需要使用闭包递归调用来继续收集函数参数。
- 当各种花里胡哨传入参数的总长度等于multiFn的长度时,代表参数到位了,可以直接执行函数了。
参考链接:高阶函数应用 —— 柯里化与反柯里化
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。