了解JavaScript中的函数卷曲以及何时使用

117 阅读7分钟

在这篇文章中,我们将讨论JavaScript中的函数库,这是函数式编程的一个高级概念。

JavaScript是网络的核心技术之一。大多数网站都使用它,而且所有现代网络浏览器都支持它,不需要插件。在这个系列中,我们将讨论不同的技巧和窍门,这将有助于你的日常JavaScript开发。

什么是函数策反?

函数策反是一种使用JavaScript函数的高级技术。事实上,它并不局限于JavaScript,在其他编程语言中也有使用。

按照维基百科的说法。

咖喱是将一个需要多个参数的函数转换为一连串的函数的技术,每个函数只需要一个参数。

换句话说,Currying只是将一个需要多个参数的函数转换为一连串需要一个参数的嵌套函数。例如,对于一个需要三个参数的函数f ,你可以这样称呼它:f(arg1, arg2, arg3) 。当你使用函数currying时,你将能够像f(arg1)(arg2)(arg3) 那样调用它。

让我们假设你有一个需要三个参数的函数,如下面的片段所示。

function fooBar(arg1, arg2, arg3) {
   …
}

要调用上述函数,你将使用以下语法。

fooBar(1,2,3);

现在,让我们看看上述函数的咖喱版的简单实现。

function fooBarCurriedVersion(arg1) {
  return (arg2) => { 
    return (arg3) => { 
      return fooBar(arg1, arg2, arg3)
    }  
  }
}

而用这个方法,你可以用下面的语法调用fooBarCurriedVersion

fooBarCurriedVersion(arg1)(arg2)(arg3);

这样做的一个好处是,你可以分开传递每个参数给函数。例如,如果你在代码中的某个时刻只知道arg1 的值,你可以只用这个参数来调用curried函数,并将得到的函数传递给你代码的其他部分。

让我们试着理解它是如何执行的。

首先,fooBarCurriedVersion(arg1) 语句被执行,它返回需要一个参数的可调用函数。接下来,由fooBarCurriedVersion 函数返回的可调用函数被调用,参数为arg2 ,它再次返回需要单个参数的可调用函数。最后,用arg3 参数调用前一步返回的可调用函数。

值得注意的是,由于所有的函数都是闭包的,所以在函数调用之间,传递的参数值会被保留下来。也就是说,如果你一旦调用一个被诅咒的函数的参数被初始化,该函数实例就会有固定的参数,不管你以后创建什么其他实例。

你也可以像下面的片段那样调用curried函数。它的工作原理与fooBar(arg1)(arg2)(arg3) 相同。

let callableOne = fooBarCurriedVersion(arg1);
let callableTwo = callableOne(arg2);
let result = callableTwo(arg3);

正如你所看到的,当我们使用currying函数时,该函数接受一个参数并返回一个可调用的函数,该函数接受下一个参数并返回另一个可调用的函数,一直到所有参数都用完为止。

事实上,你也可以像下面的片段中所示那样调用它。

let callable = fooBarCurriedVersion(1);
let result = callable(2)(3);

所以,这就是JavaScript中函数夤缘的基本知识。在下一节中,我们将通过一个真实世界的例子来演示它是如何工作的。

一个真实世界的例子

现在,你知道了函数库是如何在JavaScript中工作的。在这一节中,我们将看到你如何在日常的JavaScript开发中使用它。

首先,让我们看一下下面这个函数,它在加入必要的费用和应用折扣后,计算出产品的最终价格。

function calFinalPrice(actualPrice, charges, discountRate) {
    var finalPrice;

    finalPrice = actualPrice + charges - (actualPrice * discountRate/100);
}

现在,你可以像下面的片段那样调用这个函数。

const actualPrice = 100;
const charges = 20;

// get discount rate from the configuration
const discountRate = getDiscountRateFromConfig();

calFinalPrice(actualPrice, charges, discountRate);

正如你所看到的,我们从配置中获取了折扣率,所以它每次都是固定的。因此,如果我们创建一个咖喱版的函数,就可以避免每次调用calFinalPrice ,在第三个参数中传递它,如下面的代码段所示。

首先,我们实现了calFinalPriceWithDiscount 函数,它是calFinalPrice 函数的诅咒版本。在calFinalPriceWithDiscount 函数的第一个参数中,我们传递了折扣率,这个折扣率将在以后用于计算产品的最终价格。

discVersionFunc 变量持有可调用的,它需要两个参数,实际价格和费用。由于discVersionFunc 版本将折扣率包裹在一个闭包中,你不需要在每次需要计算产品的最终价格时传递它。

最后,你可以通过传递价格和费用值来计算产品的最终价格,如上面的片段所示。

现在,假设你想在某个时间段内提供一个特殊的折扣,你仍然可以使用calFinalPriceWithDiscount ,如下面的片段所示。

const speicalDiscountRate = getSpecialDiscountRateFromConfig();
const specialDiscVersionFunc = calFinalPriceWithDiscount(specialDiscountRate);

alert(specialDiscVersionFunc(100)(25));
alert(specialDiscVersionFunc(200)(15));

正如你所看到的,函数库的主要好处是,当你需要重复调用一个具有相同参数的函数时,你可以重用和重构你的代码,这在一段时间内变得更容易维护。

什么时候使用 "函数库"?

函数策反是一种在某些情况下很有帮助的技术,但它并不是你想在所有的函数中默认使用的东西。当函数库可以帮助你实现这些目标之一时,可以考虑使用函数库。

  • 编写更简洁的代码
  • 删除昂贵的计算过程
  • mapforEach,创建一个单参数函数。

编写更干净的代码,减少重复

有时函数库可以简化你的代码。假设你有一个日志函数logToFile(filename, appname, text) ,将一些文本记录到一个文件中。为了方便在代码中添加日志语句,你可能想设置一次文件名和应用程序名,然后简单地写一些类似log(text) 。为了实现这一点,你可以创建一个咖喱版的日志函数。

//a curryed version of the logging function
const logCurried = filename => appname => text => logToFile(filename, appname, text)

//create logging functions for a specific file and app
let log = logCurried("somepath/filename-error.log")("My App")
let logWarning = logCurried("somepath/filename-warning.log")("My App")

//now we can use the functions like this:
log("an error occurred")
logWarning("just a warning")

删除昂贵的计算

咖喱函数的另一个用途是节省昂贵的计算,如文件I/O或数据库读取。例如,假设你有下面这个函数。

//get attributes from an item in the database
function getItemAttribute (id, attribute) {
    //read the item from the database
    let item = databaseRead(id)
    
    //return the requested attribute value
    return  item[attribute]
}

//get some attributes from a particular item
let color=getItemAttribute("item001", "color")
let shape=getItemAttribute("item001", "shape")
let size=getItemAttribute("item001", "size")

假设我们想从同一个函数中读取一行中的若干属性。这就意味着要多次读取同一个数据库记录,这既浪费又慢。我们可以用currying重写getItemAttribute 函数,使之更有效率。

//get attributes from an item in the database
function getItemAttribute (id) {
    //read the item from the database
    let item = databaseRead(id)
    
    //and return a function to get attributes from that item
    return attribute => item[attribute]
}

//get some attributes from a particular item
const item001 = getItemAttributes("item001")
let color=item001("color")
let shape=item001("shape")
let size=item001("size")

mapforEach创建一个单参数函数

使用mapforEach 方法,你可以对一个数组的每个元素应用一个函数。然而,这些方法希望函数有一个特定的签名--通常你只想使用一个单参数的方法。使用函数夤缘,你可以创建一个任何函数的版本,可以与mapforEach 一起使用。

你可以在我们的其他JavaScript教程中了解更多关于mapforEach

总结

今天,我们讨论了函数crying在JavaScript中是如何工作的。我们还通过几个真实的例子来了解函数卷取在JavaScript中的实际应用。