function add ( a, b, c ) {
return a + b + c;
}
add ( 1 , 2 , 3 ) ;
const curriedAdd = curry ( add) ;
curriedAdd ( 1 ) ( 2 ) ( 3 ) ;
curriedAdd ( 1 , 2 ) ( 3 ) ;
严格意义上的柯里化通常要求“每一层函数只接收一个参数”。但 JavaScript 工具库里的 curry 往往会做增强,允许一次传入多个参数,所以 curriedAdd(1, 2)(3) 这种混合调用也很常见。
function log ( date, importance, message ) {
console . log ( ` [ ${ date. toISOString ( ) } ] [ ${ importance} ] ${ message} ` ) ;
}
使用柯里化后,可以提前固定一部分参数,生成更适合当前业务场景的函数:
const curriedLog = curry ( log) ;
const logNow = curriedLog ( new Date ( ) ) ;
const logInfoNow = logNow ( "INFO" ) ;
logInfoNow ( "用户登录成功" ) ;
logInfoNow ( "数据加载完毕" ) ;
这里的时间和日志级别被保存在闭包中,后续调用只需要传入真正变化的 message。
当参数还没收集够时,柯里化函数不会立即执行原函数,而是返回一个新函数继续等待参数。这让它很适合用于事件绑定、异步回调、配置预置等场景。
柯里化经常和 map、filter、reduce 这类高阶函数结合,让代码更接近“声明式表达”。
const prop = curry ( ( key, obj ) => obj[ key] ) ;
const users = [
{ id : 1 , name : "Alice" , age : 24 } ,
{ id : 2 , name : "Bob" , age : 28 } ,
{ id : 3 , name : "Charlie"
下面从简单版本开始,一步步实现一个更接近 Lodash.curry 使用体验的版本。
如果只处理两个参数的函数,柯里化就是一个闭包嵌套:
function curryTwoArgs ( fn ) {
return function ( a ) {
return function ( b ) {
return fn ( a, b) ;
} ;
} ;
}
function add ( a, b ) {
return a + b;
}
const curriedAdd = curryTwoArgs ( add
这个版本只能处理两个参数。如果原函数有 3 个、4 个甚至更多参数,就不能继续手写固定层数了。
JavaScript 函数对象有一个 length 属性,用来表示函数期望接收的形参数量。
function sum ( a, b, c ) { }
console . log ( sum. length ) ;
function curryV2 ( fn ) {
return function curried ( ... args) {
if ( args. length >= fn. length ) {
return fn. apply ( this , args) ;
}
return function ( ... nextArgs) {
return curried. apply ( this , args. concat ( nextArgs
function sum3 ( a, b, c ) {
return a + b + c;
}
const curriedSum = curryV2 ( sum3) ;
console . log ( curriedSum ( 1 , 2 , 3 ) ) ;
console . log ( curriedSum ( 1 ) ( 2 ) ( 3 ) ) ;
不过要注意:fn.length 不统计剩余参数,也会被默认参数影响。
function withDefault ( a, b = 2 , c ) { }
console . log ( withDefault. length ) ;
因此,完全依赖 fn.length 时,带默认值的函数可能会比你想象中更早执行。后面的 V4 会提供一个手动指定参数个数的方案。
有时我们不想严格从左到右传参,而是希望先固定第一和第三个参数,把第二个参数留到后面:
const _ = curry. placeholder ;
const curriedFn = curry ( fn) ;
curriedFn ( 1 , _, 3 ) ( 2 ) ;
这就需要在收集参数时保留“坑位”。下一次调用时,新参数会优先填补之前的占位符。
function curryV3 ( fn ) {
return function curried ( ... args) {
const _ = curryV3. placeholder ;
const validArgs = args. filter ( ( arg ) => arg !== _) ;
if ( validArgs. length >= fn. length ) {
return fn. apply ( this , args) ;
}
const _ = curryV3. placeholder ;
function greet ( greeting, name, punctuation ) {
return ` ${ greeting} , ${ name} ${ punctuation} ` ;
}
const curriedGreet = curryV3 ( greet) ;
console . log ( curriedGreet ( "Hello" , "John" ,
但它的判断条件有漏洞:只检查“有效参数数量”是否足够,不检查“前 fn.length 个位置是否都已经填好”。
curriedGreet ( _, _, _, "Hello" , "John" , "!" ) ;
这个调用里有效参数数量已经够了,但前三个位置仍然是占位符。V3 会提前执行原函数,导致错误结果。在这个例子中,因为模板字符串不能直接拼接 Symbol,所以会抛出 TypeError。
只有前 arity 个参数都不是占位符时,才执行原函数。
合并参数时不能用 shift() 把未填的占位符变成 undefined。
当函数有默认参数时,允许手动指定需要收集的参数个数。
function curryV4 ( fn, arity = fn. length ) {
return function curried ( ... args) {
const _ = curryV4. placeholder ;
const isComplete =
args. length >= arity &&
args. slice ( 0 , arity) . every ( ( arg ) => arg !== _) ;
if ( isComplete
这里的 arity 表示“需要收集多少个参数后再执行”。默认值是 fn.length,但你也可以手动指定。
const _ = curryV4. placeholder ;
function greet ( greeting, name, punctuation ) {
return ` ${ greeting} , ${ name} ${ punctuation} ` ;
}
const curriedGreet = curryV4 ( greet) ;
console . log ( curriedGreet ( "Hello" , "John" ,
console . log (
curriedGreet ( _, _, _, "Hello" , "John" , "!" ) ( _, _, _, "Hello" , "John" , "!" ) (
"Hello" ,
"John" ,
"!" ,
) ,
) ;
function joinWithDefault ( a, b = "B" , c = "C" ) {
return ` ${ a} - ${ b} - ${ c} ` ;
}
console . log ( joinWithDefault. length ) ;
如果使用默认的 fn.length,传入第一个参数后就会执行:
const curriedJoinFast = curryV4 ( joinWithDefault) ;
console . log ( curriedJoinFast ( "A" ) ) ;
如果希望它像三参数函数一样继续收集参数,可以手动指定 arity:
const curriedJoin = curryV4 ( joinWithDefault, 3 ) ;
console . log ( curriedJoin ( "A" ) ( "X" ) ( "Y" ) ) ;
console . log ( curriedJoin ( "A" , _, "Y" ) ( "X" ) ) ;
console . log ( curriedJoin ( "A" ) ( undefined )
这里传入 undefined 会触发 JavaScript 默认参数机制,所以 b 仍然取默认值 "B"。
function check ( reg, errMsg, value ) {
if ( ! reg. test ( value) ) {
console . warn ( errMsg) ;
return false ;
}
return true ;
}
check ( / ^ 1[ 3- 9 ] \d {9} $ / , "手机号格式不正确" , "13800138000" ) ;
check ( / ^ [ \w -] + ( \. [ \w -] + ) * @[ \w -] + ( \. [ \w -]
const curriedCheck = curryV4 ( check) ;
const checkPhone = curriedCheck ( / ^ 1[ 3- 9 ] \d {9} $ / , "手机号格式不正确" ) ;
const checkEmail = curriedCheck (
/ ^ [ \w -] + ( \. [ \w -] + ) * @[ -
业务层只需要关心用户输入,正则和错误提示都已经被闭包保存起来。
const prop = curryV4 ( ( key, obj ) => obj[ key] ) ;
const users = [
{ id : 1 , name : "Alice" , age : 24 } ,
{ id : 2 , name : "Bob" , age : 28 } ,
{ id : 3 , name : "Charlie"
users.map(prop("name")) 的意图很直接:从每个用户对象中取出 name 属性。
function request ( baseUrl, method, path ) {
return fetch ( ` ${ baseUrl} ${ path} ` , { method } ) . then ( ( res ) => res. json ( ) ) ;
}
const curriedRequest = curryV4 ( request) ;
const myApi = curriedRequest (
这个例子里,基础域名和请求方法都可以提前固定,业务代码只需要传入接口路径。
下面是一组可以直接用 Node.js 运行的测试,覆盖普通调用、混合调用、占位符、极端占位符、默认参数等情况。
const assert = require ( "node:assert/strict" ) ;
function curryV4 ( fn, arity = fn. length ) {
return function curried ( ... args) {
const _ = curryV4. placeholder ;
const isComplete =
args. length >= arity &&
args. slice ( 0 , arity) . every ( ( arg ) arg _
柯里化的核心是:把一次性接收多个参数的函数,改造成可以分阶段接收参数的函数。
用闭包保存已经收集到的参数。
参数不够时继续返回函数,参数够了再执行原函数。
支持占位符时,要同时关注“参数数量”和“参数位置”。
如果原函数没有默认参数,直接使用 curryV4(fn) 通常就够了。如果原函数存在默认参数,并且你希望它继续收集后面的参数,可以使用 curryV4(fn, arity) 手动指定参数个数。
,
age
:
22
}
,
] ;
const names = users. map ( prop ( "name" ) ) ;
const ages = users. map ( prop ( "age" ) ) ;
)
;
curriedAdd ( 1 ) ( 2 ) ;
)
)
;
} ;
} ;
}
console . log ( curriedSum ( 1 , 2 ) ( 3 ) ) ;
return function ( ... nextArgs) {
const mergedArgs = args
. map ( ( arg ) => ( arg === _ ? nextArgs. shift ( ) : arg) )
. concat ( nextArgs) ;
return curried. apply ( this , mergedArgs) ;
} ;
} ;
}
curryV3. placeholder = Symbol ( "placeholder" ) ;
"!"
)
)
;
console . log ( curriedGreet ( "Hello" ) ( "John" ) ( "!" ) ) ;
console . log ( curriedGreet ( "Hello" , _, "!" ) ( "John" ) ) ;
)
{
return fn. apply ( this , args) ;
}
return function ( ... nextArgs) {
let nextIndex = 0 ;
const mergedArgs = args. map ( ( arg ) => {
if ( arg !== _) {
return arg;
}
if ( nextIndex < nextArgs. length ) {
return nextArgs[ nextIndex++ ] ;
}
return _;
} ) ;
return curried. apply (
this ,
mergedArgs. concat ( nextArgs. slice ( nextIndex) ) ,
) ;
} ;
} ;
}
curryV4. placeholder = Symbol ( "placeholder" ) ;
"!"
)
)
;
console . log ( curriedGreet ( "Hello" ) ( "John" ) ( "!" ) ) ;
console . log ( curriedGreet ( "Hello" , _, "!" ) ( "John" ) ) ;
console . log ( curriedGreet ( _, "John" , _) ( _, "!" ) ( "Hello" ) ) ;
(
"Y"
)
)
;
+
)
+
$
/
,
"邮箱格式不正确"
,
"test@gmail.com"
)
;
\w
]
+
(
\.
[ \w -]
+
)
+
$
/
,
"邮箱格式不正确" ,
) ;
checkPhone ( "13800138000" ) ;
checkEmail ( "test@gmail" ) ;
,
age
:
22
}
,
] ;
const names = users. map ( prop ( "name" ) ) ;
const ages = users. map ( prop ( "age" ) ) ;
console . log ( names) ;
console . log ( ages) ;
"https://api.myproject.com"
)
;
const myApiGet = myApi ( "GET" ) ;
const myApiPost = myApi ( "POST" ) ;
myApiGet ( "/users/list" ) . then ( ( data ) => console . log ( data) ) ;
myApiPost ( "/users/create" ) . then ( ( data ) => console . log ( "创建成功" , data) ) ;
=>
!==
)
;
if ( isComplete) {
return fn. apply ( this , args) ;
}
return function ( ... nextArgs) {
let nextIndex = 0 ;
const mergedArgs = args. map ( ( arg ) => {
if ( arg !== _) {
return arg;
}
if ( nextIndex < nextArgs. length ) {
return nextArgs[ nextIndex++ ] ;
}
return _;
} ) ;
return curried. apply (
this ,
mergedArgs. concat ( nextArgs. slice ( nextIndex) ) ,
) ;
} ;
} ;
}
curryV4. placeholder = Symbol ( "placeholder" ) ;
const _ = curryV4. placeholder ;
function sum3 ( a, b, c ) {
return a + b + c;
}
const curriedSum = curryV4 ( sum3) ;
assert. equal ( curriedSum ( 1 , 2 , 3 ) , 6 ) ;
assert. equal ( curriedSum ( 1 ) ( 2 ) ( 3 ) , 6 ) ;
assert. equal ( curriedSum ( 1 , 2 ) ( 3 ) , 6 ) ;
assert. equal ( curriedSum ( 1 ) ( 2 , 3 ) , 6 ) ;
function greet ( greeting, name, punctuation ) {
return ` ${ greeting} , ${ name} ${ punctuation} ` ;
}
const curriedGreet = curryV4 ( greet) ;
assert. equal ( curriedGreet ( "Hello" , "John" , "!" ) , "Hello, John!" ) ;
assert. equal ( curriedGreet ( "Hello" ) ( "John" ) ( "!" ) , "Hello, John!" ) ;
assert. equal ( curriedGreet ( "Hello" , _, "!" ) ( "John" ) , "Hello, John!" ) ;
assert. equal ( curriedGreet ( _, "John" , _) ( _, "!" ) ( "Hello" ) , "Hello, John!" ) ;
const waitMoreArgs = curriedGreet ( _, _, _) ( "Hello" ) ;
assert. equal ( typeof waitMoreArgs, "function" ) ;
assert. equal ( waitMoreArgs ( "John" ) ( "!" ) , "Hello, John!" ) ;
assert. equal (
curriedGreet ( _, _, _, "Hello" , "John" , "!" ) ( _, _, _, "Hello" , "John" , "!" ) (
"Hello" ,
"John" ,
"!" ,
) ,
"Hello, John!" ,
) ;
function list ( a, b, c ) {
return [ a, b, c] ;
}
const curriedList = curryV4 ( list) ;
assert. deepEqual ( curriedList ( _, 2 ) ( 1 , 3 ) , [ 1 , 2 , 3 ] ) ;
assert. deepEqual ( curriedList ( _, _, 3 ) ( 1 ) ( 2 ) , [ 1 , 2 , 3 ] ) ;
function joinWithDefault ( a, b = "B" , c = "C" ) {
return ` ${ a} - ${ b} - ${ c} ` ;
}
assert. equal ( joinWithDefault. length , 1 ) ;
const curriedJoinFast = curryV4 ( joinWithDefault) ;
assert. equal ( curriedJoinFast ( "A" ) , "A-B-C" ) ;
const curriedJoin = curryV4 ( joinWithDefault, 3 ) ;
assert. equal ( curriedJoin ( "A" ) ( "X" ) ( "Y" ) , "A-X-Y" ) ;
assert. equal ( curriedJoin ( "A" , _, "Y" ) ( "X" ) , "A-X-Y" ) ;
assert. equal ( curriedJoin ( "A" ) ( undefined ) ( "Y" ) , "A-B-Y" ) ;
assert. equal ( curriedJoin ( _, _, "Y" ) ( "A" ) ( undefined ) , "A-B-Y" ) ;
console . log ( "All curryV4 tests passed." ) ;
JavaScript 学习笔记:柯里化与手写 Curry 函数