JavaScript函数柯里化与反柯里化原理解析

作者:闭包旅人

时间:2026-10-24

浏览:335

本文深入解析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)`。

以上即为本文的全部内容,希望对各位的学习有所帮助,也欢迎大家继续支持本站。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章