重载 JavaScript 函数





5.00/5 (4投票s)
用 JavaScript 重载函数的超简单方法
引言
JavaScript 的一个优点是它是弱类型的,这带来了很大的灵活性,但不幸的是,这也意味着函数重载不可用。我们没有显式的参数类型声明,因此在声明函数时无法“类型化”参数。
但是我们真的需要函数重载吗?没有它我们也能完成工作,我们可以简单地让函数检查传入参数的类型和参数的长度,并据此决定逻辑。实际上,许多开发人员就是这样做的,但在我看来,这会降低代码的可读性/可维护性,并且还会给每个需要重载逻辑的函数添加自定义代码,这引入了一个可能引入错误的代码层。因此,让我们尝试看看如何在 JavaScript 中最好地实现这一点。
我在网上搜索过,有几种方法可以实现这一点。然而,它们不如我想要的那么灵活和快速实现。如果我想在我的代码中从 A 点 -> B 点(B 点具有函数重载功能),我希望以最少的编码来实现。我希望能够尽快将它添加到我现有的代码中,而且我通常喜欢在这种事情上采用约定优于配置的方法。
在本文中,我将要求您在 JavaScript 类上进行一次函数调用,通过该调用,您将自动拥有函数重载。仍然需要遵循一些约定,但它们简单明了,几乎不需要或不需要任何配置即可使其工作。
我们需要什么?
首先,我们需要一种在不重载的情况下正常编写代码的方式,但它应该遵循一些约定,以便轻松添加重载。其次,我们需要一种简单的方式来“告知”我们定义的参数的类型信息。
对于第一步,我选择了一种简单的方法来编写不同的函数,这些函数将被重载到一个函数中。在这里,我想给我的函数编号。假设您有 3 个不同的名为 add
的函数需要重载,那么您编写原型函数的方式是为每个函数调用添加一个后缀,这个后缀是一个简单的计数器。一个例子最能说明这一点。我想有 3 种不同风格的函数,都叫做 add
,那么我需要做的是创建三个名为 add1
、add2
和 add3
的函数。稍后,我的代码将查找这些“编号函数”并将它们组合成一个没有数字的重载函数,所以:add1
、add2
、add3
... addX
-> add
(重载以服务所有这些函数)。
对于第二步,我需要一种简单、快速实现且易于记忆的方法来告知每个参数的类型。这里约定优于配置发挥得很好,我选择使用旧的匈牙利命名法来命名我的参数。所以我的参数将以一个或多个描述类型的字母开头,然后在第一个大写字母处停止,那里就是参数名称的开始。几个例子:nNumericParam
、strName
、oSomeObject
等等……在前面的例子中,这些参数的类型是“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;
}
}
在这个类中,我们编写了 add1
、add2
和 add3
这些函数,它们将被重载为一个名为 add
的函数(请注意,您不应该在您的类中创建或使用函数名 add
,否则函数重载器将失败)。我们还使用匈牙利命名法命名了我们的参数,这将允许我们“告知”其类型。
Using the Code
要添加函数重载,我们只需要调用一个函数:overloadPrototype
。
此函数接受两个参数,第一个参数是类的构造函数,第二个参数是一个对象,它将匈牙利命名法前缀映射到您可以从 typeof
获取的实际类型。您可能需要在自己的代码中进行自定义类型匹配逻辑,因此为了方便,实际匹配发生在名为 checkTypesMatch
的函数中,该函数会传入参数本身以及它所属参数的匈牙利前缀。我想在这里指出,参数检查并非总是使用,如果不同要重载的函数具有唯一的参数长度,那么重载代码将仅根据参数长度做出决定,这将在本文后面详细解释。
在我们之前的例子中,添加重载的调用如下所示:
overloadFunction(AdditionClass, { n : "number", str : "string", o : "object" });
幕后
那么函数 overloadFunction
到底做了什么呢?首先,它遍历传入构造函数的原型函数,一旦它找到一个最后一个字符是“1
”的函数,它就会将这个函数名传递给一个名为 processFunctions
的函数,该函数将尝试连接重载。
processFunctions
函数首先获取传入函数名的基本名称。在这个例子中,传入函数名“add1
”意味着基本名称是 add
,并且新的重载函数将使用这个名称创建。
其次,processFunctions
将循环并尝试查找所有带计数器的函数,在此示例中为 add1
、add2
和 add3
。当找不到更多函数或已计数最多 10 个函数(10 个重载)时,它将停止。(请注意,在维护代码时,不能在带编号的函数中留下空隙)。我假设您不应该有超过 10 个重载,因为这可能会使重载功能变慢,我们将在本文后面讨论性能。如果您有超过 10 个重载并且确实需要它们,您可能需要增加循环的上限。
第三,processFunctions
将根据参数长度对这个函数列表进行排序,在此过程中,它将检查我们是否有重复的参数长度。这里我们有两种情况,重复长度或不重复,这由变量 duplicateLen
指示。
让我们首先考虑简单的情况,即没有重复长度时。这种情况更容易编写重载函数,并且重载逻辑也很快,其成本基本上只是函数调用的另一层间接。现在 processFunctions
会将构造函数、计数函数数组和基本名称传递给另一个名为 createLengthOverload
的函数。
函数 createLengthOverload
会创建一个带有基本名称(这里是“add
”)的新函数,这个函数非常简单,它会查找传入参数的长度,并根据此调用相应的原始函数。(这里是 add1
、add2
和 add3
之一)。
在我们的第二种情况下,当函数具有相同长度时,显然我们需要根据类型信息来决定调用哪个函数,这就是事情变得有点难看的地方。
在这种情况下,首先我们需要创建一个新的数据结构(在代码中称为 data
),它将长度进行整合,这意味着我们将具有相同长度的函数进行分组。长度 X 用作键,值是一个函数信息数组。
现在我们需要遍历这个新的数据结构 (data
),检查哪些长度有多个函数。在我们上面的例子中,add1
和 add2
的参数长度都是 2,add3
的参数长度是 3。所以 add3
是安全的,它没有其他函数在它的参数长度上竞争,然而 add1
和 add2
确实竞争,所以决定调用哪个函数将基于类型匹配决策。对于每组共享相同长度的函数,我们现在为它们创建一个新的数据结构,称为 annotationArray
,这个结构将“描述”每个函数的参数类型(这里是 add1
和 add2
这两个函数)。annotationArray
将包含 add1
的:['number', 'number'] 和 add2
的:['number', 'string']。此时,我们进行一些健全性检查,我们需要确保这些不同的函数至少有一个不同的参数类型,否则我们无法决定调用哪个函数,每个不同的函数至少一个参数应该不同(在我们的例子中,add1
的第二个参数是 number,add2
的第二个参数是 string)。接下来(在设置优化标志的情况下)会删除相同参数的类型信息,在我们的例子中,我们删除 add1
和 add2
的第一个参数的类型信息,因为它们都是数字,在决策中不起任何作用,我们只保留第二个参数的信息。现在我们准备创建我们的重载函数,我们将数据结构以及基本名称和构造函数传递给函数 createTypeOverload
。
函数 createTypeOverload
将创建一个带有基本名称(这里是“add
”)的新函数,类似于 createLengthOverload
,但逻辑将有所不同。新创建的 add
函数将能够访问我们之前创建和完善的 (data
) 数据结构。根据它和传递给它的参数长度,它将决定调用哪个函数(add1
、add2
或 add3
)。如果传递了 3 个参数,它将直接调用 add3
;如果传递了两个参数,它将遍历候选函数,在这种情况下是 add1
和 add2
,并将尝试根据数据类型消除不匹配的函数,最终只有一个函数应该赢得匹配。
标志
optimizeTypeInfo
: 这应该设置为 true
,基本上这将移除重载的匹配信息,如果它们在所有相同长度的候选者中所有参数都具有相同的类型。这将加快匹配过程,但这S意味着如果所有候选者都具有相同的参数(例如数字),并且您传入一个字符串,它将被匹配到第一个候选者,即使它不是数字。 一个例子最能说明这一点,add1
和 add2
的第一个参数都具有数字类型。如果此标志设置为 true
,则第一个参数永远不会匹配,因此即使您向第一个参数传入一个 string
,它也将被接受并且不会用于匹配过程。如果此标志设置为 false
,则所有参数类型都必须精确匹配。如果您向第一个参数传入一个 string
,它将不会匹配 add1
或 add2
,并且会抛出异常。
matchNull
:另一个限制是传递 null
作为参数值。由于我们无法从 null
推断类型,因此我们必须决定是匹配任何内容还是不匹配任何内容。如果您选择不匹配任何内容,这实际上意味着您根本无法将 null
传递给任何重载函数,因为它将不匹配。如果您将其设置为匹配 null
(true
),这意味着 null
值将匹配任何内容(就像一个通配符)。我们可以在这里创建一个简单的约定,传入 null
但仍然描述其类型,可能使用一个可以在 checkTypesMatch
函数中轻松测试的对象。但这留给读者决定是否需要。
性能
显然事情不会神奇地发生,总要付出代价,问题是这个代价有多大,你是否愿意付出。在其他语言中,编译器会处理重载,所以它们是免费的;在 JavaScript 中,我们必须大致在运行时完成编译器所做的工作。如果您的代码是关键代码,那么在没有函数重载的情况下进行优化当然是首选,但是如果您的代码不是关键代码,那么让我们看看我们的代价有多大。在所有要重载的函数中,参数长度都是唯一的情况下,这种情况非常快,几乎可以说没有代价。如果参数长度小于 10,我使用一个数组,将参数长度作为索引来调用函数,所以这里的代价只是调用函数本身的 apply
所带来的一层间接。如果参数长度大于 10,那么我使用一个哈希表来获取函数调用,这会稍微昂贵一些。当存在共同长度时,我们必须遍历不同的候选函数并逐个消除它们,因此代价将根据共享相同参数长度的函数数量而有所不同。如果我有时间,我会进行一些压力测试并发布一些常见情况的数字。
关于附加代码的注意事项
附加代码是为在 NodeJS 中运行而编写的,因此使用 require
方法包含文件。对于客户端,请使用标准脚本标签包含文件。
联系方式
我尝试了不同长度和/或类型的不同情况,以确保代码没有任何问题。如果发现任何问题或使用时遇到困难,请给我发消息到我的 CodeProject 账户,我将尽快修复。
谢谢。