本文深入解析JavaScript中的函数柯里化技术,从apply和call的基础用法入手,逐步演示如何构建基础、通用及灵活的柯里化函数。通过代码示例展示参数分批处理、自动执行控制及多步执行的具体实现,帮助开发者理解闭包在柯里化中的应用及其实际价值。
一、简单了解apply和call
- call 和 apply 旨在改变函数执行时的上下文(context),即调整函数体内 this 的指向。
- call 与 apply 功能一致,区别仅在于参数接收方式不同;call 可视作 apply 的语法糖。
- 格式:
apply(context,[arguments]),call(context,param1,param2,...)。
二、什么是函数柯里化?
柯里化(Currying)技术将接收多参数的函数转换为接收单一参数(原函数首个参数)并返回接收剩余参数及结果的新函数。
此处举例,存在一个add()函数,用于处理传入参数(param1,params2,…)的相加求和。
// 在这里第一个具有两个参数`x`、`y`的`add(x , y)`函数
function add(x , y){
return x + y;
}
// 调用`add()`函数,并给定两个参数`4`和`6`
add(4,6);
// 模拟计算机操作,第一步 传入第一个参数 4
function add(4 , y){
return 4 + y;
}
// 模拟计算机操作,第二步 传入第一个参数 6
function add(4 , 6){
return 4 + 6;
}
若对add()函数进行柯里化,其形态如何?此处进行简单实现:
// 柯里化过的add()函数,可以接受部分参数
function add(x ,y){
if (typeof y === 'undefined') {
return function (newy){
return x + newy;
}
}
// 完整应用
return x + y;
}
// 测试调用
console.log(typeof add(4)); // [Function]
console.log(add(4)(6)); // 10
// 可以创建保存函数
let saveAdd = add(4);
console.log(saveAdd(6)); // 10
由上述简易柯里化的add()函数可见,函数可接受部分参数并返回新函数以继续处理剩余参数。
三、写一个公共的柯里化函数
此处创建公共柯里化函数,避免每次编写函数时内部实现复杂的柯里化逻辑。
// 定义一个createCurry的函数
function createCurry(fn){
var slice = Array.prototype.slice,
stored_args = slice.call(arguments,1);
return function () {
let new_args = slice.call(arguments),
args = stored_args.concat(new_args);
return fn.apply(null,args);
}
}
在上述公共柯里化函数中:
arguments并非真实数组,而是具备length属性的对象,故从Array.prototype借用slice方法将arguments转为真实数组,以便操作。- 首次调用
createCurry函数时,变量stored_args保存除首个参数外的其余参数,因首个参数为待柯里化的函数。 - 执行
createCurry函数返回的函数时,变量new_args获取参数并转为数组。 - 内部返回函数通过闭包访问
stored_args存储的值与new_args值,合并为新数组并赋值给args。 - 最后调用
fn.apply(null,args)方法,执行被柯里化的函数。
现测试公共柯里化函数
// 普通函数add()
function add(x , y){
return x + y;
}
// 柯里化得到一个新的函数
var newAdd = createCurry(add,4);
console.log(newAdd(6)); // 10
//另一种简便方式
console.log(createCurry(add,4)(6));// 10
此处不限于两参数柯里化,亦支持多参数:
// 多个参数的普通函数
function add(a,b,c,d){
return a + b + c + d;
}
// 柯里化函数得到新函数,多个参数可以随意分割
console.log(createCurry(add,4,5)(5,6)); // 20
// 两步柯里化
let add_one = createCurry(add,5);
console.log(add_one(5,5,5));// 20
let add_two = createCurry(add_one,4,6);
console.log(add_two(6)); // 21
由上述示例可见局限:无论两参或多参,仅能分两步执行,如公式:
- fn(x,y) ==> fn(x)(y);
- fn(x,y,z,w) ==> fn(x)(y,z,w) || fn(x,y)(z,w)||…
若需更灵活:
- fn(x,y) ==> fn(x)(y);
- fn(x,y,z) ==> fn(x,y)(z) || fn(x)(y)(z);
- fn(x,y,z,w) ==> fn(x,y)(z)(w) || fn(x)(y)(z)(w) || …;
如何实现?
四、创建一个灵活的柯里化函数
经上述练习,发现所建柯里化函数存在局限,期望函数可分多步执行:
// 创建一个可以多步执行的柯里化函数,当参数满足数量时就去执行它:
// 函数公式:fn(x,y,z,w) ==> fn(x)(y)(z)(w);
let createCurry = (fn,...params)=> {
let args = parsms || [];
let fnLen = fn.length; // 指定柯里化函数的参数长度
return (...res)=> {
// 通过作用域链获取上一次的所有参数
let allArgs = args.slice(0);
// 深度拷贝闭包共用的args参数,避免后续操作影响(引用类型)
allArgs.push(...res);
if(allArgs.length < fnLen){
// 当参数数量小于原函数的参数长度时,递归调用createCurry函数
return createCurry.call(this,fn,...allArgs);
}else{
// 当参数数量满足时,触发函数执行
return fn.apply(this,allArgs);
}
}
}
// 多个参数的普通函数
function add(a,b,c,d){
return a + b + c + d;
}
// 测试柯里化函数
let curryAdd = createCurry(add,1);
console.log(curryAdd(2)(3)(4)); // 10
上述已实现灵活柯里化函数,但发现新问题:
- 若首次传入全部参数,未返回结果而是函数(function)。
- 需再次调用返回函数方可返回结果:
curryAdd(add,1,2,3,4)(); - 有人建议全参时调用原
add()函数,此法可行;但为满足参数数量,此处仍处理该情况。
只需在返回函数前增加判断:
let createCurry = (fn,...params)=> {
let args = parsms || [];
let fnLen = fn.length; // 指定柯里化函数的参数长度
if(length === _args.length){
// 加入判断,如果第一次参数数量以经足够时就直接调用函数获取结果
return fn.apply(this,args);
}
return (...res)=> {
let allArgs = args.slice(0);
allArgs.push(...res);
if(allArgs.length < fnLen){
return createCurry.call(this,fn,...allArgs);
}else{
return fn.apply(this,allArgs);
}
}
}
上述可视为完成灵活柯里化函数,但尚不灵活,因无法控制执行时机,参数足即自动执行。期望实现可控执行时机:
五、写一个可控制的执行时间的柯里化函数
直接说明函数公式:
- fn(a,b,c) ==> fn(a)(b)(c )();
- fn(a,b,c) ==> fn(a);fn(b);fn(c );fn();
- 参数足够时不执行,需再次调用函数才执行并返回结果。在上述示例中添加小条件即可实现。
// 当参数满足,再次执行时调用函数
let createCurry = (fn,...params)=> {
let args = parsms || [];
let fnLen = fn.length; // 指定柯里化函数的参数长度
//当然这里的判断需要注释掉,不然当它第一次参数数量足够时就直接执行结果了
//if(length === _args.length){
// 加入判断,如果第一次参数数量以经足够时就直接调用函数获取结果
//return fn.apply(this,args);
//}
return (...res)=> {
let allArgs = args.slice(0);
allArgs.push(...res);
// 在这里判断输入的参数是否大于0,如果大于0在判断参数数量是否足够,
// 这里不能用 && ,如果用&& 也是参数数量足够时就执行结果了。
if(res.length > 0 || allArgs.length < fnLen){
return createCurry.call(this,fn,...allArgs);
}else{
return fn.apply(this,allArgs);
}
}
}
// 多个参数的普通函数
function add(a,b,c,d){
return a + b + c + d;
}
// 测试可控制的柯里化函数
let curryAdd = createCurry(add,1);
console.log(curryAdd(2)(3)(4)); // function
console.log(curryAdd(2)(3)(4)()); // 10
console.log(curryAdd(2)(3)()); // 当参数不足够时返回 NaN
总结
本文至此结束,希望能带来帮助,也欢迎关注本站更多内容!