JavaScript偏函数与柯里化详解:从bind到高级实现

作者:青柠时间:2026-10-23 05:54:31浏览:392

本文深入解析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程序设计有所帮助。

作者最新文章