65.9K
CodeProject 正在变化。 阅读更多。
Home

重载 JavaScript 函数

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2013年11月28日

CPOL

11分钟阅读

viewsIcon

48887

downloadIcon

267

用 JavaScript 重载函数的超简单方法

引言

JavaScript 的一个优点是它是弱类型的,这带来了很大的灵活性,但不幸的是,这也意味着函数重载不可用。我们没有显式的参数类型声明,因此在声明函数时无法“类型化”参数。

但是我们真的需要函数重载吗?没有它我们也能完成工作,我们可以简单地让函数检查传入参数的类型和参数的长度,并据此决定逻辑。实际上,许多开发人员就是这样做的,但在我看来,这会降低代码的可读性/可维护性,并且还会给每个需要重载逻辑的函数添加自定义代码,这引入了一个可能引入错误的代码层。因此,让我们尝试看看如何在 JavaScript 中最好地实现这一点。

我在网上搜索过,有几种方法可以实现这一点。然而,它们不如我想要的那么灵活和快速实现。如果我想在我的代码中从 A 点 -> B 点(B 点具有函数重载功能),我希望以最少的编码来实现。我希望能够尽快将它添加到我现有的代码中,而且我通常喜欢在这种事情上采用约定优于配置的方法。

在本文中,我将要求您在 JavaScript 类上进行一次函数调用,通过该调用,您将自动拥有函数重载。仍然需要遵循一些约定,但它们简单明了,几乎不需要或不需要任何配置即可使其工作。

我们需要什么?

首先,我们需要一种在不重载的情况下正常编写代码的方式,但它应该遵循一些约定,以便轻松添加重载。其次,我们需要一种简单的方式来“告知”我们定义的参数的类型信息。

对于第一步,我选择了一种简单的方法来编写不同的函数,这些函数将被重载到一个函数中。在这里,我想给我的函数编号。假设您有 3 个不同的名为 add 的函数需要重载,那么您编写原型函数的方式是为每个函数调用添加一个后缀,这个后缀是一个简单的计数器。一个例子最能说明这一点。我想有 3 种不同风格的函数,都叫做 add,那么我需要做的是创建三个名为 add1add2add3 的函数。稍后,我的代码将查找这些“编号函数”并将它们组合成一个没有数字的重载函数,所以:add1add2add3 ... addX -> add(重载以服务所有这些函数)。

对于第二步,我需要一种简单、快速实现且易于记忆的方法来告知每个参数的类型。这里约定优于配置发挥得很好,我选择使用旧的匈牙利命名法来命名我的参数。所以我的参数将以一个或多个描述类型的字母开头,然后在第一个大写字母处停止,那里就是参数名称的开始。几个例子:nNumericParamstrNameoSomeObject 等等……在前面的例子中,这些参数的类型是“n”、“str”和“o”。我们将通过一个非常简单的对象将这些映射到实际类型,我们将把这个对象传递给为我们创建重载的函数。

所以让我们回顾一下并给出一个小而完整的示例,说明我们需要如何编写一个类。

// a useless class really, but a good example 
function AdditionClass() {
  this.IsAdditionClass = true;
}
AdditionClass.prototype = {
  add1: function(nValue1, nValue2) {
    if (console)
      console.log('calling add1 with params:', arguments);
    return nValue1 + nValue2;
  },
  add2: function(nValue, strValue) {
    if (console)
      console.log('calling add2 with params:', arguments);
    return nValue + strValue;
  },
  add3: function(nValue1, nValue2, nValue3) {
    if (console)
      console.log('calling add3 with params:', arguments);
    return nValue1 + nValue2 + nValue3;
  }
}

在这个类中,我们编写了 add1add2add3 这些函数,它们将被重载为一个名为 add 的函数(请注意,您不应该在您的类中创建或使用函数名 add,否则函数重载器将失败)。我们还使用匈牙利命名法命名了我们的参数,这将允许我们“告知”其类型。

Using the Code

要添加函数重载,我们只需要调用一个函数:overloadPrototype

此函数接受两个参数,第一个参数是类的构造函数,第二个参数是一个对象,它将匈牙利命名法前缀映射到您可以从 typeof 获取的实际类型。您可能需要在自己的代码中进行自定义类型匹配逻辑,因此为了方便,实际匹配发生在名为 checkTypesMatch 的函数中,该函数会传入参数本身以及它所属参数的匈牙利前缀。我想在这里指出,参数检查并非总是使用,如果不同要重载的函数具有唯一的参数长度,那么重载代码将仅根据参数长度做出决定,这将在本文后面详细解释。

在我们之前的例子中,添加重载的调用如下所示:

overloadFunction(AdditionClass, { n : "number", str : "string", o : "object" });

幕后

那么函数 overloadFunction 到底做了什么呢?首先,它遍历传入构造函数的原型函数,一旦它找到一个最后一个字符是“1”的函数,它就会将这个函数名传递给一个名为 processFunctions 的函数,该函数将尝试连接重载。

processFunctions 函数首先获取传入函数名的基本名称。在这个例子中,传入函数名“add1”意味着基本名称是 add,并且新的重载函数将使用这个名称创建。

其次,processFunctions 将循环并尝试查找所有带计数器的函数,在此示例中为 add1add2add3。当找不到更多函数或已计数最多 10 个函数(10 个重载)时,它将停止。(请注意,在维护代码时,不能在带编号的函数中留下空隙)。我假设您不应该有超过 10 个重载,因为这可能会使重载功能变慢,我们将在本文后面讨论性能。如果您有超过 10 个重载并且确实需要它们,您可能需要增加循环的上限。

第三,processFunctions 将根据参数长度对这个函数列表进行排序,在此过程中,它将检查我们是否有重复的参数长度。这里我们有两种情况,重复长度或不重复,这由变量 duplicateLen 指示。

让我们首先考虑简单的情况,即没有重复长度时。这种情况更容易编写重载函数,并且重载逻辑也很快,其成本基本上只是函数调用的另一层间接。现在 processFunctions 会将构造函数、计数函数数组和基本名称传递给另一个名为 createLengthOverload 的函数。

函数 createLengthOverload 会创建一个带有基本名称(这里是“add”)的新函数,这个函数非常简单,它会查找传入参数的长度,并根据此调用相应的原始函数。(这里是 add1add2add3 之一)。

在我们的第二种情况下,当函数具有相同长度时,显然我们需要根据类型信息来决定调用哪个函数,这就是事情变得有点难看的地方。 微笑 | :)

在这种情况下,首先我们需要创建一个新的数据结构(在代码中称为 data),它将长度进行整合,这意味着我们将具有相同长度的函数进行分组。长度 X 用作键,值是一个函数信息数组。

现在我们需要遍历这个新的数据结构 (data),检查哪些长度有多个函数。在我们上面的例子中,add1add2 的参数长度都是 2,add3 的参数长度是 3。所以 add3 是安全的,它没有其他函数在它的参数长度上竞争,然而 add1add2 确实竞争,所以决定调用哪个函数将基于类型匹配决策。对于每组共享相同长度的函数,我们现在为它们创建一个新的数据结构,称为 annotationArray,这个结构将“描述”每个函数的参数类型(这里是 add1add2 这两个函数)。annotationArray 将包含 add1 的:['number', 'number'] 和 add2 的:['number', 'string']。此时,我们进行一些健全性检查,我们需要确保这些不同的函数至少有一个不同的参数类型,否则我们无法决定调用哪个函数,每个不同的函数至少一个参数应该不同(在我们的例子中,add1 的第二个参数是 number,add2 的第二个参数是 string)。接下来(在设置优化标志的情况下)会删除相同参数的类型信息,在我们的例子中,我们删除 add1add2 的第一个参数的类型信息,因为它们都是数字,在决策中不起任何作用,我们只保留第二个参数的信息。现在我们准备创建我们的重载函数,我们将数据结构以及基本名称和构造函数传递给函数 createTypeOverload

函数 createTypeOverload 将创建一个带有基本名称(这里是“add”)的新函数,类似于 createLengthOverload,但逻辑将有所不同。新创建的 add 函数将能够访问我们之前创建和完善的 (data) 数据结构。根据它和传递给它的参数长度,它将决定调用哪个函数(add1add2add3)。如果传递了 3 个参数,它将直接调用 add3;如果传递了两个参数,它将遍历候选函数,在这种情况下是 add1add2,并将尝试根据数据类型消除不匹配的函数,最终只有一个函数应该赢得匹配。

标志 

optimizeTypeInfo: 这应该设置为 true,基本上这将移除重载的匹配信息,如果它们在所有相同长度的候选者中所有参数都具有相同的类型。这将加快匹配过程,但这S意味着如果所有候选者都具有相同的参数(例如数字),并且您传入一个字符串,它将被匹配到第一个候选者,即使它不是数字。 一个例子最能说明这一点,add1add2 的第一个参数都具有数字类型。如果此标志设置为 true,则第一个参数永远不会匹配,因此即使您向第一个参数传入一个 string,它也将被接受并且不会用于匹配过程。如果此标志设置为 false,则所有参数类型都必须精确匹配。如果您向第一个参数传入一个 string,它将不会匹配 add1add2,并且会抛出异常。 

matchNull:另一个限制是传递 null 作为参数值。由于我们无法从 null 推断类型,因此我们必须决定是匹配任何内容还是不匹配任何内容。如果您选择不匹配任何内容,这实际上意味着您根本无法将 null 传递给任何重载函数,因为它将不匹配。如果您将其设置为匹配 null (true),这意味着 null 值将匹配任何内容(就像一个通配符)。我们可以在这里创建一个简单的约定,传入 null 但仍然描述其类型,可能使用一个可以在 checkTypesMatch 函数中轻松测试的对象。但这留给读者决定是否需要。 

性能  

显然事情不会神奇地发生,总要付出代价,问题是这个代价有多大,你是否愿意付出。在其他语言中,编译器会处理重载,所以它们是免费的;在 JavaScript 中,我们必须大致在运行时完成编译器所做的工作。如果您的代码是关键代码,那么在没有函数重载的情况下进行优化当然是首选,但是如果您的代码不是关键代码,那么让我们看看我们的代价有多大。在所有要重载的函数中,参数长度都是唯一的情况下,这种情况非常快,几乎可以说没有代价。如果参数长度小于 10,我使用一个数组,将参数长度作为索引来调用函数,所以这里的代价只是调用函数本身的 apply 所带来的一层间接。如果参数长度大于 10,那么我使用一个哈希表来获取函数调用,这会稍微昂贵一些。当存在共同长度时,我们必须遍历不同的候选函数并逐个消除它们,因此代价将根据共享相同参数长度的函数数量而有所不同。如果我有时间,我会进行一些压力测试并发布一些常见情况的数字。

关于附加代码的注意事项

附加代码是为在 NodeJS 中运行而编写的,因此使用 require 方法包含文件。对于客户端,请使用标准脚本标签包含文件。

联系方式

我尝试了不同长度和/或类型的不同情况,以确保代码没有任何问题。如果发现任何问题或使用时遇到困难,请给我发消息到我的 CodeProject 账户,我将尽快修复。

谢谢。

© . All rights reserved.