本文深入解析JavaScript中自动柯里化的实现原理,通过对比不同编码方案,展示如何利用闭包与递归构建高效的部分应用函数。文章详细剖析了基于fn.length判断参数数量的核心逻辑,并附带基准测试数据,论证该方法在减少闭包层级带来的性能优势,帮助开发者掌握函数式编程中的关键技巧。
引言
本文通过代码演示与实例解析,深入探讨JS中自动柯里化的精妙实现,涵盖其基础用法及核心知识,供读者学习参考。
什么是柯里化?
何谓柯里化?
计算机科学领域内,柯里化(Currying)技术旨在将接收多参数的函数转换为仅接收单一参数(即原函数首参)的函数,并返回接收剩余参数并产出结果的新函数。该术语由逻辑学家 Haskell Curry 命名,但发明者实为 Moses Schnfinkel 与 Gottlob Frege。
理论过于晦涩?不妨先观代码示例:
柯里化应用
柯里化应用场景
若需实现列表元素的特定处理,例如令列表内各元素加一,直观思路如下:
const list = [0, 1, 2, 3];
list.map(elem => elem + 1);
此法看似简单?若需改为加2呢?
const list = [0, 1, 2, 3];
list.map(elem => elem + 1);
list.map(elem => elem + 2);
如此处理效率似显不足,是否可封装处理函数?
然而,map 的回调函数仅接受当前元素 elem 单一参数,看似难以直接封装...
或许你会期望:若能获取一个部分配置好的函数,例如:
// plus返回部分配置好的函数
const plus1 = plus(1);
const plus2 = plus(2);
plus1(5); // => 6
plus2(7); // => 9
将此类函数传入 map:
const list = [0, 1, 2, 3];
list.map(plus1); // => [1, 2, 3, 4]
list.map(plus2); // => [2, 3, 4, 5]
效果显著!如此,无论增加数值多少,仅需 list.map(plus(x)) 即可,完美达成封装目标,大幅提升代码可读性!
然而核心问题在于:此类 plus 函数应如何构建?
此时,柯里化技术便可大显身手:
柯里化函数
柯里化函数解析
// 原始的加法函数
function origPlus(a, b) {
return a + b;
}
// 柯里化后的plus函数
function plus(a) {
return function(b) {
return a + b;
}
}
// ES6写法
const plus = a => b => a + b;
观察可知,柯里化的 plus 函数首先接收参数 a,随后返回接收参数 b 的函数。得益于闭包机制,返回函数可访问父函数参数 a。例如:const plus2 = plus(2) 等效于 function plus2(b) { return 2 + b; },从而实现了部分配置。
通俗而言,柯里化即是对多参数函数进行部分配置的过程,每一步均返回接收单个参数的部分配置函数。极端情况下,可能需要多次配置,例如多次相加:
multiPlus(1)(2)(3); // => 6
此种写法虽显怪异,但若深入 JS 函数式编程领域,这将成为常态。
JS中自动柯里化的精巧实现
JS中自动柯里化的精妙实现
柯里化(Currying)为函数式编程关键环节,诸多函数式语言(如 Haskell)默认自动执行柯里化。然而 JS 并不具备此特性,故需自行实现自动柯里化函数。
首先展示代码:
// ES5
function curry(fn) {
function _c(restNum, argsList) {
return restNum === 0 ?
fn.apply(null, argsList) :
function(x) {
return _c(restNum - 1, argsList.concat(x));
};
}
return _c(fn.length, []);
}
// ES6
const curry = fn => {
const _c = (restNum, argsList) => restNum === 0 ?
fn(...argsList) : x => _c(restNum - 1, [...argsList, x]);
return _c(fn.length, []);
}
/***************** 使用 *********************/
var plus = curry(function(a, b) {
return a + b;
});
// ES6
const plus = curry((a, b) => a + b);
plus(2)(4); // => 6
至此,自动柯里化功能得以实现!
若能理解上述过程,恭喜你!你已具备大佬潜质,不妨点赞并开启你的函数式编程之旅(滑稽)。
若尚感困惑,无需担忧,现为你梳理思路。
需求分析
需求分析
我们需要构建一个 curry 函数,接收待柯里化的函数作为参数,返回接收单一参数的函数。接收到的参数存入列表,当参数数量充足时,执行原函数并返回结果。
实现方式
实现策略
经简单思考可知,柯里化部分配置函数的步骤数等同于 fn 的参数个数,即拥有两个参数的 plus 函数需分两步配置。可通过 fn.length 获取函数参数个数。
核心思路为:每次传入参数,均将其加入参数列表 argsList。若无更多参数需传入,则调用 fn.apply(null, argsList) 执行原函数。为此,需构建内部判断函数 _c(restNum, argsList),接收剩余参数个数 restNum 与已获参数列表 argsList;_c 的功能在于判断是否仍有未传入参数,当 restNum 为零时,通过 fn.apply(null, argsList) 执行原函数并返回结果。若仍有参数需传递,即 restNum 不为零时,需返回单参数函数
function(x) {
return _c(restNum - 1, argsList.concat(x));
}
以继续接收参数。此处形成尾递归,函数接收参数后,剩余需参数量 restNum 减一,并将新参数 x 加入 argsList 后传入 _c 进行递归调用。结果表现为:参数不足时,返回负责接收新参数的单参数函数;参数充足时,调用原函数并返回。
现再审视:
function curry(fn) {
function _c(restNum, argsList) {
return restNum === 0 ?
fn.apply(null, argsList) :
function(x) {
return _c(restNum - 1, argsList.concat(x));
};
}
return _c(fn.length, []); // 递归开始
}
是否逐渐清晰?
ES6 写法因运用数组解构及箭头函数等语法糖,外观更为精简,但核心思想一致。
// ES6
const curry = fn => {
const _c = (restNum, argsList) => restNum === 0 ?
fn(...argsList) : x => _c(restNum - 1, [...argsList, x]);
return _c(fn.length, []);
}
与其他方法的对比
与其他方法对比
另有一常用方法:
function curry(fn) {
const len = fn.length;
return function judge(...args1) {
return args1.length >= len ?
fn(...args1):
function(...args2) {
return judge(...[...args1, ...args2]);
}
}
}
// 使用箭头函数
const curry = fn => {
const len = fn.length;
const judge = (...args1) => args1.length >= len ?
fn(...args1) : (...args2) => judge(...[...args1, ...args2]);
return judge;
}
与本文前述方法对比,发现该方法存在两点问题:
依赖 ES6 解构(函数参数中的 ...args1 与 ...args2);
性能略逊一筹。
性能问题
性能问题探讨
进行如下测试:
console.time("curry");
const plus = curry((a, b, c, d, e) => a + b + c + d + e);
plus(1)(2)(3)(4)(5);
console.timeEnd("curry");
测试环境(Manjaro Linux,Intel Xeon E5 2665,32GB DDR3 四通道1333Mhz,Node.js 9.2.0):
本文所述方法耗时约 0.325ms
其他方法耗时约 0.345ms
微小差异推测源于闭包机制。因闭包访问性能开销较大,且该方法形成两个闭包:fn 和 len,而前述方法仅形成 fn 一个闭包,故造成此微小差距。