JavaScript偏函数与柯里化详解:从bind到高级实现
本文深入解析JavaScript中的偏函数与柯里化技术。通过bind方法演示如何固定参数生成偏函数,介绍手动实现partial函数的方法。进一步阐述柯里化概念,将多参数函数转换为链式调用,并提供高级curry实现代码,展示如何在保持原有调用方式的同时生成便捷偏函数,适用于日志处理等场景。
偏函数与柯里化概述
本文通过具体实例解析JavaScript中的偏函数与柯里化概念,旨在帮助读者深入理解并掌握相关技巧。
此前我们主要关注this的绑定,现在我们将深入探讨更高级的参数绑定技术。虽然使用频率较低,但在特定场景下非常实用。
使用bind实现偏函数
bind方法的完整语法结构如下所示。
let bound = func.bind(context, arg1, arg2, ...);
该方法不仅可以绑定上下文this,还能预先设定函数的初始参数。例如,假设我们有一个乘法函数mul(a,b):
function mul(a, b) {
return a * b;
}
基于该函数,我们可以利用绑定创建一个名为double的新函数:
let double = mul.bind(null, 2);
alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10
执行mul.bind(null, 2)会创建新函数double。调用mul时,第一个参数(上下文)固定为null,第二个参数固定为2。若需固定多个参数,只需依次列出即可。
这种技术被称为偏函数应用(Partial Application),即创建一个新函数,将现有的一些参数值固定下来。
注意,虽然此处并未实际使用this,但由于bind方法要求提供上下文参数,因此必须传入null作为占位。
在以下代码中,函数triple实现了乘以3的功能:
let triple = mul.bind(null, 3);
alert( triple(3) ); // = mul(3, 3) = 9
alert( triple(4) ); // = mul(3, 4) = 12
alert( triple(5) ); // = mul(3, 5) = 15
我们通常为什么要使用偏函数?
偏函数的主要优势在于:通过创建一个名称直观的独立函数(如double、triple),调用时无需每次传入第一个参数,因为该参数已通过bind固定。
偏函数的另一种应用场景是:当存在一个通用函数时,为了方便使用,可以创建一个更常用的特定变体。
例如,假设有一个函数send(from, to, text),利用偏函数可以创建一个从当前用户发送消息的变体:sendTo(to, text)。
无上下文的偏函数实现
接下来讨论不使用上下文的偏函数实现。
如果只想固定部分参数,而不绑定this呢?
内置的bind不允许这样做,因为它强制要求绑定上下文并跳转到后续参数。幸运的是,我们可以仅绑定参数,实现一个名为partial的函数。
具体实现如下:
function partial(func, ...argsBound) {
return function(...args) { // (*)
return func.call(this, ...argsBound, ...args);
}
}
// Usage:
let user = {
firstName: "John",
say(time, phrase) {
alert(`[${time}] ${this.firstName}: ${phrase}!`);
}
};
// add a partial method that says something now by fixing the first argument
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());
user.sayNow("Hello");
// Something like:
// [10:00] Hello, John!
调用partial(func[, arg1, arg2...])的结果是调用func的包装器(见(*)行代码):
- this保持一致(因为
user.sayNow是通过user调用的)。 - 随后传入参数...garsBound——partial使用该参数("10:00")进行调用。
- 接着提供参数...gars——传递给包装器的参数("Hello")。
那么,使用spread运算符是否就能轻松实现呢?
实际上,loadash库也提供了—.partial的实现方式。
柯里化概念与基础实现
接下来介绍柯里化(Currying)。
有时人们会混淆偏函数与另一个名为“柯里化”的函数功能。柯里化是另一种有趣的函数处理技术,这里必须涉及。
柯里化(Currying):将调用函数f(a,b,c)转换为f(a)(b)(c)方式的调用。
让我们实现一个柯里化函数,执行一个两元参数函数,即将f(a,b)转换为f(a)(b):
function curry(func) {
return function(a) {
return function(b) {
return func(a, b);
};
};
}
// usage
function sum(a, b) {
return a + b;
}
let carriedSum = curry(sum);
alert( carriedSum(1)(2) ); // 3
上述过程是通过一系列包装器实现的。
curry(func)的结果是function(a)的一个包装器。- 当调用
sum(1)时,参数被保存在词法环境中,然后返回新的包装器function(b)。 - 随后
sum(1)(2)提供2并最终调用function(b),然后将调用传递给原始多参数函数sum。
有一些高级柯里化实现,如lodash库中_.curry可实现更复杂功能。其返回一个包装器,允许函数提供全部参数被正常调用或返回偏函数。
function curry(f) {
return function(..args) {
// if args.length == f.length (as many arguments as f has),
// then pass the call to f
// otherwise return a partial function that fixes args as first arguments
};
}
柯里化的应用场景
柯里化的应用场景有哪些?
高级柯里化允许函数正常调用,也可以容易以偏函数方式调用。为了理解其优势,我们需要一个实际示例说明。
例如,我们有一个日志函数log(date,importance,message),用于格式化输出信息。实际项目中这些函数也有许多其他有用的特性,如:通过网络发送或过滤:
function log(date, importance, message) {
alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
}
让我们使用柯里化!将log函数柯里化:
log = _.curry(log);
柯里化后仍然可以正常调用:log(new Date(), "DEBUG", "some debug");
我们也可以使用柯里化方式调用:log(new Date())("DEBUG")("some debug"); // log(a)(b)(c)
这里定义一个便捷函数,记录当天日志:
// todayLog will be the partial of log with fixed first argument
let todayLog = log(new Date());
// use it
todayLog("INFO", "message"); // [HH:mm] INFO message
现在再定义一个便捷函数:记录当天debug信息:
let todayDebug = todayLog("DEBUG");
todayDebug("message"); // [HH:mm] DEBUG message
总结上述过程:
1. 柯里化后没有失去任何东西,log仍然可以正常调用。 2. 我们能生成在多个场景使用的便捷偏函数。
高级柯里化实现原理
接下来展示高级柯里化实现。
如果你感兴趣,这里提供了上面提到的高级柯里化实现:
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}
function sum(a, b, c) {
return a + b + c;
}
let curriedSum = curry(sum);
// still callable normally
alert( curriedSum(1, 2, 3) ); // 6
// get the partial with curried(1) and call it with 2 other arguments
alert( curriedSum(1)(2,3) ); // 6
这里实现看上去有点复杂,但确实很容易理解。curry(func)的结果是包装器curried,如下所示:
// func is the function to transform
function curried(...args) {
if (args.length >= func.length) { // (1)
return func.apply(this, args);
} else {
return function pass(...args2) { // (2)
return curried.apply(this, args.concat(args2));
}
}
};
当我们运行时,有两个分支:
1. 如果传递args数与原函数已经定义的参数个数一样或更长,那么直接调用。 2. 获得偏函数:否则,不调用func函数,返回另一个包装器pass,提供连接之前的参数一起做为新参数重新应用curried。然后再次执行一个新调用,返回一个新偏函数(如果参数不够)或最终结果。
举例,让我们看sum(a, b, c)会怎样,三个参数,所以sum.length=3.
如果调用curried(1)(2)(3):
1. 第一次调用curried(1),在词法环境中记住1,返回包装器pass。
2. 使用(2)调用包装器pass:其带着前面的参数(1),连接他们然后调用curried(1,2),因为参数数量仍然小于3,返回pass。
3. 再次使用(3)被调用包装器pass,带着之前的参数(1,2),然后增加3,并调用curried(1,2,3)——最终有三个参数,传递给原始函数。
如果仍然不清楚,可以按顺序在脑子里或纸上跟踪调用过程。
柯里化的限制与总结
柯里化仅针对函数参数长度固定的情况。
柯里化需要函数有已知的参数数量固定。
柯里化比定义多一点。
根据柯里化定义,转换sum(a,b,c)至sum(a)(b)(c)。
但在Javascript中大多数实现是超越定义,也可以让函数使用多个参数变量执行。
最后进行总结。
当把已知函数的一些参数固定,结果函数被称为偏函数,通过使用bind获得偏函数,也有其他方式实现。
当我们不想一次一次重复相同的参数时,偏函数是很便捷的。如我们有send(from,to)函数,如果from总是相同的,可以使用偏函数简化调用。
柯里化是转换函数调用从f(a,b,c)至f(a)(b)(c)。Javascript通常既实现正常调用,也实现参数数量不足时的偏函数方式调用。
当我们想容易的偏函数时,柯里化非常好。如我们已经看到的日志示例:通用的函数是log(date,importance,message),柯里化之后获得偏函数为,一个参数如log(date),或两个参数log(date,importance)。
更多关于JavaScript相关内容可查看本站专题:《JavaScript常用函数技巧汇总》、《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》及《JavaScript数学运算用法总结》
希望本文所述对大家JavaScript程序设计有所帮助。
ES6模块化中js文件怎么用import和export交互
2026-11-05 17:00:48
JS读取本地文件时报错或无响应的常见原因
2026-11-05 16:32:31
let、const与var的区别:JavaScript变量声明最佳实践
2026-11-05 14:16:38
JS中直接定义静态二维数组的字面量写法
2026-11-05 14:16:08
JS 怎么查找字符串中所有匹配项的位置
2026-11-05 14:15:53
相关标签:
