JavaScript函数柯里化与反柯里化原理解析
本文深入解析JavaScript中的函数柯里化与反柯里化技术。通过具体场景演示柯里化如何实现参数复用与延迟计算,并介绍通用实现方案。同时探讨反柯里化如何基于鸭子类型思想扩大方法适用范围,解析Function.prototype.uncurrying的实现机制及代码细节。
函数柯里化基础概念
提到函数柯里化(Currying),很多人可能会感到困惑,这似乎是一个生硬的中式翻译。为了厘清概念,我们需要深入探讨其本质。
根据维基百科的定义,柯里化是一种将接收多个参数的函数转换为接收单一参数(原函数的第一个参数)并返回一个新函数的技术,该新函数接受剩余参数并返回最终结果。这一概念由数学家Haskell Brooks Curry提出,并以他的名字命名。
概念往往都是干涩且难懂的,让我们用人话来解释就是:如果我们不确定这个函数有多少个参数,我们可以先给它传入一个参数,然后通过JS闭包(如若不懂JS闭包,请先学习闭包知识点再来学习本篇博文https://www.jb51.net/article/171398.htm)来进行返回一个函数,内部函数接收除开第一个参数外的其余参数进行操作并输出,这个就是函数的柯里化;
为了更直观地理解,我们来看一个具体的应用场景。
假设我们需要计算程序员每天的加班时长。
考虑到程序员加班较多的现实情况,我们首先想到的实现方式可能是直接累加时间。
var overtime=0;
function time(x){
return overtime+=x;
}
time(1); //1
time(2); //3
time(3); //6
上述代码虽然可行,但每次调用都需要重新计算当日时间,操作繁琐。如果调用频率极高且数据量巨大,可能会带来性能隐患。是否存在一种既能简化操作又能保证性能的解决方案?答案是肯定的。
function time(x){
return function(y){
return x+y;
}
}
var times=time(0);
times(3);
然而,前文代码仍存在局限性:实际开发中参数往往是不确定的。虽然基本实现了柯里化的形式,但无法灵活处理参数数量变化的情况。从上述代码可以看出,柯里化的本质是:函数调用时仅接收一个参数,通过闭包返回内部函数以处理剩余参数,并利用闭包特性保存首个参数的状态。
我们对代码进行进一步优化:
柯里化的通用实现与优势
// 首先定义一个变量接收函数
var overtime = (function() {
//定义一个数组用来接收参数
var args = [];
//这里运用闭包,调用外部函数返回一个内部函数
return function() {
//arguments是浏览器内置对象,专门用来接收参数
//如果参数的长度为0即没有参数的时候
if(arguments.length === 0) {
//定义变量用来累加
var time = 0;
//循环累加,用i和args的长度进行比较
for (var i = 0, l = args.length; i < l; i++) {
//进行累加操作 等价于time=time+args[i]
time += args[i];
}
// 返回累加的结果
return time;
//如果arguments对象参数长度不为零,即有参数的时候
}else {
//定义的空数组添加arguments参数作为数组项,第一个参数古args作为改变this指向,第二个参数arguments把剩余参数作为数组形式添加至空数组中
[].push.apply(args, arguments);
}
}
})();
overtime(3.5); // 第一天
overtime(4.5); // 第二天
overtime(2.1); // 第三天
//...
console.log( overtime() ); // 10.1
改造后的代码已具备基本功能,但这并非柯里化的完整实现。接下来,我们将介绍一种通用的实现模式。
以下是通用的柯里化实现方案:
//定义方法currying,先传入一个参数
var currying=function(fn){
//定义空数组装arguments对象的剩余参数
var args=[];
//利用闭包返回一个函数处理剩余参数
return function (){
//如果arguments的参数长度为0,即没有剩余参数
if(arguments.length===0){
//执行上面方法
//这里的this指向下面的s,类似于s(),代表参数长度为0的时候直接调用函数
return fn.apply(this,args)
}
console.log(arguments)
//如果arguments的参数长度不为0,即还有剩余参数
//在数组的原型对象上添加数组,apply用来更改this的指向为args
//将[].slice.call(arguments)的数组添加到原型数组上
//这里的[].slice.call(arguments)===Array.prototype.slice.call(arguments)实质上就是将arguments对象转成数组并具有slice功能
Array.prototype.push.apply(args,[].slice.call(arguments))
//args.push([].slice.call(arguments))
console.log(args)
//这里返回的arguments.callee是返回的闭包函数,callee是arguments对象里面的一个属性,用于返回正被执行的function对象
return arguments.callee
}
}
//这里调用currying方法并传入add函数,结果会返回闭包内部函数
var s=currying(add);
//调用闭包内部函数,当有参数的时候会将参数逐步添加到args数组中,待没有参数传入的时候直接调用
//调用的时候支持链式操作
s(1)(2)(3)();
//也可以一次性传入多个参数
s(1,2,3);
console.log(s());
JavaScript函数柯里化具有以下显著优势:
第一,支持延迟计算。调用柯里化函数时若未传参,参数会被存入数组;只有当不再传入新参数时,才会执行最终计算。
第二,实现参数复用。在多次调用同一函数且大部分参数相同的情况下,柯里化是极佳的选择。
凡事皆有对立统一,既然有柯里化,自然也存在反柯里化。
接下来探讨反柯里化(uncurrying)。
从字面理解,反柯里化与柯里化相反。其核心作用是扩大方法的适用范围:调用某个方法时,无需关心对象自身是否具备该方法,只要该方法适用于该对象即可使用。这体现了动态语言中的“鸭子类型”思想。
在学习反柯里化之前,我们先理解动态语言中的鸭子类型概念,这将有助于深入理解。
根据维基百科的解释:
鸭子类型是动态类型编程的一种风格。
在这种风格中,对象的有效语义不由其继承的类或实现的接口决定,而是由其当前拥有的方法和属性集合决定。
这一概念源于James Whitcomb Riley提出的“鸭子测试”,其表述如下:
如果一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么它就可以被称作鸭子。
通俗来说,就像你是你父母的子女,无论是否优秀,血缘关系不变;同理,只要你的行为像鸭子(如会呱呱叫、行走姿态像鸭子),无论你是否是真正的鸭子,你都可以被视为鸭子。
JavaScript中广泛引用了鸭子类型思想。例如,变量赋值时无需考虑类型,这种灵活性使得JavaScript成为典型的动态类型语言。
我们来看看反柯里化中如何应用鸭子类型:
反柯里化原理与实现
//函数原型对象上添加uncurring方法
Function.prototype.uncurring = function() {
//改变this的指向
//这里的this指向是Array.prototype.push
var self = this;
//这里的闭包用来返回内部函数的执行
return function() {
//创建一个变量,在数组的原型对象上添加shift上面删除第一个参数
//改变数组this的指向为arguments
var obj = Array.prototype.shift.call(arguments);
//最后返回执行并给方法改变指向为obj也就是arguments
// 并传入arguments作为参数
return self.apply(obj, arguments);
};
};
//数组原型对象上添加uncurrying方法
var push = Array.prototype.push.uncurring();
//测试一下
//匿名函数自执行
(function() {
//这里的push就是一个函数方法了
//相当于传入参数arguments和4两个参数,但是在上面shift方法中删除第一个参数,这里的arguments参数被截取了,所以最后实际上只传入了4
push(arguments, 4);
console.log(arguments); //[1, 2, 3, 4]
//匿名函数自调用并带入参数1,2,3
})(1, 2, 3)
此时读者可能会疑惑:arguments是一个接收参数的对象,本身没有push方法,为何能调用push?
原因在于代码`var push = Array.prototype.push.uncurring();`在数组原型的push方法上添加了uncurring方法。执行匿名函数`push(arguments, 4)`时,实质是调用Function原型上添加的uncurring方法并返回闭包内部函数。由于Array原型上的shift方法会截取arguments的第一个参数,实际调用变为`push(4)`,因此最终结果为[1,2,3,4]。
在《JavaScript设计模式与开发实践》一书中,JS函数的反柯里化案例如下:
//定义一个对象
var obj = {
"length":1,
"0":1
}
//在Function原型对象定义方法uncurrying
Function.prototype.uncurrying = function() {
//this指向Array.prototype.push
var self = this;
//闭包返回一个内部函数
return function() {
// 这里可以拆开理解
//首先执行apply return
//Function.prototype.call(Array.prototype.push[obj,2])
//然后Array.prototype.push.call(obj,2)
//call改变指向 obj.push(2)
//所以最后结果就是 {0: 1, 1: 2, length: 2}
return Function.prototype.call.apply(self, arguments);
}
}
//在
var push = Array.prototype.push.uncurrying()
push(obj, 2)
console.log(obj);
//{0: 1, 1: 2, length: 2}
如果上述方式难以理解,我们来看一个更易懂的版本:
Function.prototype.unCurrying = function () {
var self = this;
return function () {
//[].slice.call(arguments,1)===Array.prototype.push.slice.call(arguments,1)===arguments.slice(1)
return self.apply(arguments[0], [].slice.call(arguments, 1));
};
};
var push = Array.prototype.push.uncurrying()
console.log(push);
push(obj,2) //{0: 1, 1: 2, length: 2}
console.log(obj);
具体分析如下:
首先,在Function原型对象上添加uncurrying方法,使所有Function均可借用。
其次,该方法返回一个闭包内部函数。
闭包函数返回的结果是调用方法,其中self指向Array.prototype.push。apply方法的第一个参数用于更改指向,对应`push(obj,2)`即更改指向为`obj.push(2)`。
apply方法的第二个参数涉及call方法,用于将指向更改为arguments。由于arguments具备slice方法,等价于`arguments.slice(1)`。
以上即为本文的全部内容,希望对各位的学习有所帮助,也欢迎大家继续支持本站。
什么时候该用JS构造器:与工厂模式、字面量的选择对比
2026-11-05 16:18:22
推荐几款轻量级JS照片墙开源库及对比
2026-11-05 15:56:19
JavaScript递归写法有哪些常见陷阱及避免方法
2026-11-05 14:53:38
JS数组浅拷贝与深拷贝怎么选?场景与方法总结
2026-11-05 14:41:02
p5.js 中 setup() 和 draw() 函数的执行机制详解
2026-11-05 12:32:38
