JavaScript 函数的命名参数,另一种方法





5.00/5 (12投票s)
JavaScript 中没有命名函数参数?自动创建它们
引言名称有什么意义?我们所说的玫瑰
换个名字闻起来也会很香
威廉·莎士比亚,《罗密欧与朱丽叶》
1 为什么需要命名参数?
本文旨在回答一个关于 JavaScript 的常见问题:是否存在命名函数参数?简短的回答是:不存在。那么,该怎么办?
在本文的开头,我想向读者保证:这不是关于传递具有表示参数的属性的对象的众所周知技术,不像 { oneArgument:3, anotherArgument: 3 }
。这完全是另一回事。看顶部的图片。它说明了这一想法。
命名函数参数是 许多编程语言的特性。
首先,为什么人们会询问命名参数?嗯,因为它们非常方便。
- 您不必记住参数的顺序。
- 调用语句的代码更加可读;它使传递给方法的每个值的角色一目了然。
- 一些或所有命名参数可以从调用语句中省略,然后使用默认值。
- 命名参数可以与位置参数混合。
当然,人们必须记住参数的名称,但这并不是一个严重的缺点,因为参数名称比顺序更能起到助记作用。此外,代码的可读性比最初的编码便利性更重要。但真正强大的好处将是支持开发过程。理想情况下,系统应检测所有拼写错误或滥用参数的情况。在基于编译器的技术中,编译器可以显示所有问题,但在基于解释器的系统中,运行时系统应该这样做,这仍然比什么都没有要好得多。而这正是现有的 JavaScript 解决方案失败的地方。我们能做得更好吗?让我们看看,但首先让我们讨论一些每个人都可以在网上找到的好主意。
2 一些好主意
首先,我们需要讨论我声称本文不讨论的同一件事。这是“对象传递技巧”。这个想法足够简单,几乎可以立即重新发明,并且仍然相当合理。与其传递多个参数,不如传递一个对象,其某些属性代表参数。在调用点,这可以是所有参数的使用,也可以是其中的一部分。
而不是拥有
function myFunction(first, medium, last) {
alert("first: " + first + "; medium: "
+ medium + "; last: " + last);
} // myFunction
可以有一个单参数函数,并用一个参数调用它
var myFunction = function (object) {
alert("first: " + object.first + "; medium: "
+ object.medium + "; last: " + object.last);
} // myFunction
// which can be called as
myFunction({medium: 2, last: 3, first: 1});
这种方法的缺点非常明显。首先,可能很难找出“参数名称”,它们成为属性名称。它们可能深藏在函数实现中。当然,属性名称的拼写错误可能几乎不被注意。但这种方法具有良好的灵活性。首先,调用中属性的顺序无关紧要。对象类型始终不匹配,但 JavaScript 按名称访问它们,这就是全部。当然,参数(属性)可以在调用中省略。它们将 undefined
对象传递给函数。
尽管这种风格存在所有问题,但它确实有很大意义:它没有任何显着的开销,并且在许多重要方法中使用。一个重要的例子是嵌入式 Object
对象的 Object.defineProperty
方法:https://mdn.org.cn/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty。
另一个有趣的想法只是描述个人的编码风格:声明一些中间命名变量,初始化它们,然后用于调用中
var width = 70, height = 115;
someFunction(width, height);
或者,更有趣的是
var width, height;
someFunction(width = 50, height = 114);
请在此处参阅 Stackoverflow 会员 dav_i 和 Ray Perea 的回答:http://stackoverflow.com/questions/11796093/named-parameters-in-javascript。
好吧,运行时系统没有检查任何东西;名称可能拼写错误;参数顺序不能改变。上下文被冗余变量污染,这总是很糟糕的。这只是个人纪律问题。这里唯一解决的问题是代码的可读性和更好的可维护性,特别是如果您在长时间后回到相同的代码。但可读性本身就足够重要了。让我们只记住这种调用形式,它的语法风格;它似乎很有帮助。
现在,让我们转到更鼓舞人心、更不寻常的想法。
首先,最常引用的解决方案是 jsFiddle 以以下方式呈现的解决方案:http://jsfiddle.net/9U328。
这个想法是调用 parameterfy
的函数用于包装原始函数。它表示为字符串,该字符串被解析以提取参数的名称。然后返回包装器函数,该函数仅接受一个参数,代表具有匹配参数名称的属性的对象。包装器函数使用此对象来创建用于传递给原始函数的参数数组。它有效。
但是,此实现的质量非常糟糕。首先,当参数数量为零时,它会失败。这只是一个易于修复的错误。最糟糕的是:从这些解决方案中,“this”参数完全丢失了,这是不可原谅的错误。没有这个隐式传递的参数(也可以通过 .call
或 .apply
方法显式传递),就没有体面的编程。
这个解决方案在许多地方被引用,而展示这个解决方案的人不引用来源,这一点也不是很好,但显而易见的是,这么多人不可能使用相同的确切源代码和相同的确切错误得出了完全相同的解决方案。事实是:我们面对的是一个抄袭盛行的社区,同事们,祝贺你们!我不想引用我发现的所有那些令人讨厌的出版物,只引用了最方便阅读的一篇。总之,这段代码的原始且未知的作者给了我们一个有趣的想法。
我发现的另一个实现要好得多。它是“bob.js JavaScript 框架”的一部分
http://www.bobjs.com,
http://bobjs.codeplex.com.
此实现没有我提到的问题;最重要的是,它正确地传递了“this”。此外,它保留了原始函数,而原始函数是调用的目标函数。
总之,所有这些解决方案都有一个问题:如果您拼写错误某些参数名称,则无法轻易检测到,这在某种程度上会使目标失效。
3 一些更好的主意
我们能做得更好吗?我认为可以。
我的主要目标是消除命名参数可能的拼写错误。
Ray Perea 提供的朴素技术给了我一个提示。我的想法是:我们需要将原始函数包装在某个非函数对象中,而不是另一个函数中。后来,我意识到它仍然应该是一个函数,但带有属性,这对于 JavaScript 来说也不是一个非常常见的构造。无论如何,函数是 JavaScript 中的一等公民,因此它们可以像任何其他对象一样具有属性。我需要这些属性来表示原始目标函数的命名参数。调用代码应将值赋给这些属性,这可以在调用语句中完成。
这样,我就可以密封包装器函数对象,因此不会隐式添加任何属性。如果调用代码以一些拼写错误的参数编写,则尝试将值赋给不存在的属性将被检测到。不幸的是,ECMAScript 标准 不要求 JavaScript 引擎遵守密封,除非代码以严格模式编写。但没有任何东西可以阻止我们将所有代码都以严格模式编写,这在许多其他方面也是一个好主意。
现在,我准备展示它的外观,但首先我想展示如何使用我用来说明概念的代码示例。
4 在 JavaScript Playground 上测试代码示例
JavaScript Playground 是我开发的一个用于 JavaScript 执行的微型平台,用于演示示例代码。这是我最初的 JavaScript Calculator 项目的一个衍生产品。它的工作原理是:我向计算器添加了 API,这是一个 JavaScript 文件,可以包含在一个带有脚本的小 HTML 文件中,该脚本代表代码示例。API 从 body 的 onload
脚本调用,并找到不同的页面,即 JavaScript Playground。使用 Web 存储 sessionStorage
,API 将 3 个参数传递给 JavaScript Playground:1) 示例脚本的文本,2) 防止 JavaScript Playground 在页面加载时立即执行的可选标志,3) 定义使用严格模式的另一个可选标志;true 表示严格模式,false 表示非严格模式,undefined 表示用户可以随时开启或关闭严格模式。请参阅本文提供的可下载代码中的“JavaScript.Playground/playgroundAPI.js”以及开关的说明(7. Dynamic Strict Mode Switching and Web Storage)。
本文的代码示例使用了默认的自动执行标志和严格模式标志,因为我想演示命名参数机制如何防止参数名称拼写错误。
加载并可选地执行示例后,用户可以修改示例代码并执行任意次数。如果此人设法使脚本挂起,则需要等待脚本执行超时很长时间。 :-)
5 解决方案:用法
例如,让我们考虑一个“普通”函数,它使用,比如说,3 个参数
var f = function(first, medium, last) {
alert("first: " + first + "; medium: " + medium + "; last: " + last
+ "; THIS: " + ((typeof this == typeof f) ? typeof this : this));
return first + medium + last;
};
// this is how we call it
f(1, 2, 3);
这就是我们通常所做的,也是不需要解释的。唯一可能需要解释的项目是隐式“this”参数的某种“复杂”处理。这真的很简单。在后续的代码示例中,我们将遇到“this”参数是函数的情况;我只是想避免输出整个函数体,否则该函数体将被连接到字符串的其余部分;我只想添加一个单词“function”。
在后续示例中,我将用我 JavaScript Playground API 中的函数 writeLine
替换 alert
。我这样做只是为了方便展示代码示例;这与本文的主题无关。您可以编写任何其他示例函数的实现;本文的主题是如何将参数传递给它。
现在,有了我提供的功能,该函数可以包装到此命名参数函数中
var f = namedCaller(function(first, medium, last) {
writeLine("first: " + first + "; medium: " + medium + "; last: " + last
+ "; THIS: " + ((typeof this == typeof f) ? typeof this : this));
return first + medium + last;
});
它可以根据赋值运算符传递参数来调用。自然,它们可以按任何顺序出现;这不会影响函数的操作
f(f.medium = 2, f.last = 3, f.first = 1);
一些或所有参数可能缺失;缺失的参数将作为 undefined 传递
f(f.last = 30, f.first = 10);
如果函数和调用在脚本的顶层定义,则最后一个调用的结果将是
first: 10; medium: undefined; last: 30; THIS: undefined
“this”的值取决于严格模式。如果模式是严格的,“this”是 undefined,但对于非严格模式,它将等于 window 对象(当然,对于嵌入到浏览器中的 JavaScript),因此结果将是
first: 1; medium: 2; last: 3; THIS: [object Window]
这是我实现的命名参数的一个最重要的特定功能:您可以省略一个参数,但不能拼写错误参数的名称。我将在下一节中解释如何实现这一点,但效果是:将抛出 TypeError 异常:“function... is not extensible”。
这种效果基于对象密封的效果;我将在下一节中解释它。目前,重要的是要知道,不幸的是,在非严格模式下不一定会遵守密封。但无论如何,强烈建议在严格模式下进行所有开发;即使开发人员想从产品中删除严格性,也可以在项目的最后阶段完成,那时所有与可能拼写错误相关的错误都已,希望,消除。
我所知道的最准确的关于严格模式的描述和使用教程可以在这里找到:https://mdn.org.cn/en-US/docs/Web/JavaScript/Reference/Strict_mode。
作为 namedCaller
函数参数传递的原始函数被保留为该函数对象的一个属性,名称为“targetFunction”。为什么?因为这样就保留了使用普通位置参数传递的替代方案。可以这样调用
f.targetFunction(100, 200, 300);
结果如下
first: 100; medium: 200; last: 300; THIS: function
有趣的是,由于 namedCaller
返回的对象 f
仍然被使用,因此仍然可以使用赋值语法,但是,很明显,参数名称和位置之间的对应关系将不会保持,因此调用
f.targetFunction(f.medium = 200, f.last = 300, f.first = 100);
将导致
first: 200; medium: 300; last: 100; THIS: function
请注意,在这种情况下,传递给目标函数的参数“this”也是一个函数。这个函数当然是 namedCaller
。
总之,参数“this”可以显式传递给目标函数或包装器函数。我们将在下一节 第 7 节 中进一步详细讨论。
6 解决方案:它是如何工作的?
这是完整的实现(“NamedCaller.js”)
"use strict";
function namedCaller(targetFunction, targetFunctionPropertyName) {
if (!targetFunctionPropertyName) targetFunctionPropertyName = "targetFunction";
var wrapper = (function createWrapperFunction() {
var prepareArguments = function (self) {
var argumentValues = [];
for (var index = 0; index < argumentNames.length; ++index)
argumentValues[index] = self[argumentNames[index]];
return argumentValues;
} //prepareArguments
var cleanUp = function (self) {
for (var index = 0; index < argumentNames.length; ++index)
self[argumentNames[index]] = undefined;
} //cleanUp
return function () {
var argumentValues = prepareArguments(wrapper);
cleanUp(wrapper);
return targetFunction.apply(this, argumentValues);
} //wrapper
})(); //createWrapperFunction
var argumentNames = (function parseArguments(self) {
var argumentNames = targetFunction.toString().
match(/function[^(]*\(([^)]*)\)/)[1].split(/,\s*/);
if (!argumentNames || !argumentNames[0]) // case of "function () {...}"
argumentNames = [];
for (var index = 0; index < argumentNames.length; ++index)
self[argumentNames[index]] = undefined;
return argumentNames;
})(wrapper); //parseArguments
Object.defineProperty(wrapper, targetFunctionPropertyName,
{ enumerable: true, value: targetFunction });
Object.seal(wrapper);
return wrapper;
}; //namedCaller
我使用了获取函数对象 targetFunction
并解析其字符串表示以提取参数名称的常用方法。当然,parametrify
实现中遗留的错误并未被忽视。这部分实现在 parseArguments
函数中。参数名称集 argumentNames
用于上面解析创建的 wrapper
函数的实现中。这不会造成任何问题,因为该名称集仅在调用函数时使用。
parseArguments
的重要部分是将属性添加到 wrapper
对象,每个参数名称一个属性。请注意,某个属性的名称可能与函数名称相同,但参数名称之间的名称冲突是不可能的,它将被检测为 SyntaxError
,并带有“duplicate formal argument”消息。这样,所有属性名称将保持唯一,并且不会进行属性的重新定义。
还定义了另一个属性 targetFunction
。重要的是,这是唯一可能发生属性名称冲突的地方。为防止任何可能的冲突,函数 namedCalled
的第二个形式参数 targetFunctionPropertyName
被添加。如果有一个形式参数名为“targetFunction”怎么办?为防止名称冲突而不重命名它,可以将 targetFunction
属性赋予替代名称。
例如,一个函数
var f = namedCaller(function(targetFunction, first, medium, last) {
writeLine(
"first: " + first + "; medium: " + medium +
"; last: " + last + "; targetFunction: " + targetFunction +
"; THIS: " + this);
return first + medium + last;
}, "originalFunction");
可以用这个命名参数 targetFunction
调用
f(f.medium = 2, f.last = 3, f.first = 1, f.targetFunction="TF");
并且仍然可以直接调用目标函数,使用位置参数传递
f.originalFunction.call(11, 12, 13, "TF");
另请参阅演示代码示例“demo7-resolving-name-clash.html”。
请注意,targetFunction
属性是使用 Object.defineProperty
创建的。这样做是为了防止覆盖此属性的值。
顺便说一句,
namedCaller
的一个早期版本使用Object.defineProperty
定义了每一个属性,直到我注意到让所有这些属性enumerable
有一些好处。然后,对于表示调用参数的属性,这种属性定义方法将产生与默认方法完全相同的影响。但为什么是可枚举的?因为在开发过程中,开发人员可以快速转储namedCaller
返回的包装器函数对象,并获得可用参数名称的其余部分。我在代码示例中使用的dump
方法是 JavaScript Playground 的一部分,但您可以轻松地编写类似的代码。这是一个适合检查此类包装器函数的简化实现function dumpObject(object, newLine) { var result = ""; for (var index in object) { var value = object[index]; var quote = ""; if (value) { if (value && value.constructor == Function) { var bodyIndex = value.toString().indexOf(")"); value = value.toString().substr(0, bodyIndex + 1); } else if (value.constructor == String) quote = "\""; } result += index + ": " + quote + value + quote + newLine; } return result; }另请参阅“sample-dumpObject.html”。这里,
newLine
取决于您想将文本插入何处;它可以是“\n
”、“<br/>
”等。
最后,生成的 wrapper
函数被密封。它在严格模式下可防止命名参数的拼写错误。
请注意,参数“this”是从包装器函数显式传递给包装的目标函数的。这是我将在下一节中单独讨论的一个关键部分。
7 永远不要忘记“this”
首先,“this”参数可以通过使用两个函数之一显式传递,.call
或 .apply
。例如,这里解释了它们
https://mdn.org.cn/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call
https://mdn.org.cn/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply.
它们的使用在演示示例“demo2-basic-usage.html”中进行了说明
f.call("some object", f.medium = 2, f.last = 3, f.first = 1);
f.apply("some other object", [f.medium = 2, f.last = 3, f.first = 1]);
f.targetFunction.call(110, 111); f.targetFunction.call(document, 1.4, null, 1.5);
让我们通过以下示例讨论“this”的重要性。我通过随源代码提供的四个演示样本(名称为“demo?-using-this*.html”文件)来说明其用法,请参阅。首先,让我们假设我们为单个 HTML 元素创建了一些事件处理程序。在我的示例中,这是我的 JavaScript Playground 中已经定义的元素,其 UI 的一部分,可以作为 elements.result
访问;您可以通过文本“Click here!”找到它。这是处理程序代码,也可以在文件“demo3-using-this-step1.html”中找到
elements.result.onclick = function () {
var element = document.createElement("div");
element.style.position = "absolute";
element.style.left = this.offsetLeft;
element.style.bottom = 0;
element.style.backgroundColor = "yellow";
element.style.border = "solid thin black";
element.style.borderRadius = "3px";
element.style.color = "red";
element.style.padding = "1em";
element.innerHTML = "Some text<br/>Some more text<br/><br/>Click here to close it";
element.onclick = function () { this.parentElement.removeChild(this); }
this.parentElement.appendChild(element);
} //elements.result.onclick
基本上,这个处理程序会创建一个带有某些文本的“弹出”元素,该元素可以通过单击删除。
到目前为止都很好,但假设您需要抽象出处理程序代码。原因之一是您可能希望对多个 HTML 元素重用相同的处理程序代码,但您也可能希望参数化处理程序中使用的功能,在这种情况下,它将是弹出元素的文本和样式。一个小问题是:“this”是所有事件处理程序中用于传递目标对象引用的一个重要参数。在这种情况下,它是指向被单击的 HTML 元素的引用。当然,您始终可以添加另一个显式参数来表示此引用,但这是一种笨拙的重构步骤,因为您必须在所有代码中进行替换,这些代码可能已经过充分调试和测试。而且这是完全不必要的,因为您始终可以使用 .call
或 .apply
函数显式传递“this”。它可以这样重构(“demo4-using-this-step2.html”)
function clickHandler (text, padding, color, backgroundColor, border, borderRadius) {
var element = document.createElement("div");
element.style.position = "absolute";
element.style.left = this.offsetLeft;
element.style.bottom = 0;
element.style.padding = "1em";
element.innerHTML = text;
element.style.color = color;
element.style.backgroundColor = backgroundColor;
element.style.border = border;
element.style.borderRadius = borderRadius;
element.onclick = function () { this.parentElement.removeChild(this); }
this.parentElement.appendChild(element);
} //clickHandler
elements.result.onclick = function () {
clickHandler.call(this,
"Positional arguments<br/><br/>Click here to close it",
"1em",
"red",
"yellow",
"solid thin black",
"3px");
} //elements.result.onclick
那么,我们如何使用我的按名称传递机制来实现相同的事情呢?没问题,因为“this”被正确传递了。它可能看起来像这样(参见“demo5-using-this-step3.html”)
var f = namedCaller(function(text, padding, color, backgroundColor, border, borderRadius) {
var element = document.createElement("div");
element.style.position = "absolute";
element.style.left = this.offsetLeft;
element.style.bottom = "1em";
element.style.padding = padding ? padding : "0.4em";
element.innerHTML = text;
element.style.color = color;
element.style.backgroundColor = backgroundColor;
element.style.border = border;
element.style.borderRadius = borderRadius;
element.onclick = function () { this.parentElement.removeChild(this); }
this.parentElement.appendChild(element);
});
elements.result.onclick = function () {
f.call(
this,
f.backgroundColor = "PowderBlue",
//f.color = "red",
//f.padding = "1em",
//f.borderRadius = "3px",
f.text = "Passing arguments by name!<br/><br/>Click here to close it",
f.border = "solid thin black");
} //elements.result.onclick
这个演示代码说明了用于参数化这些弹出功能的参数可以任意顺序出现或被省略。
但是,最重要的用法是与使用构造函数创建的对象集一起使用的。让我们考虑以下函数,它扮演构造函数的角色
var A = function (initialValue) {
this.value = initialValue;
this.show = function () { return this.value + 1; };
this.f = namedCaller(function (add, multiply) { return (add + this.value) * multiply; });
}
可以用按名称传递参数的方式调用它
var fReturn = a.f(a.f.add = 3, a.f.multiply = 0.5);
另请参阅演示代码示例“demo6-using-this.html”。
由于函数 a.f
中的“this”本质上用于传递另一个属性 value
的值,因此正确传递“this”的值至关重要。请注意,不需要显式传递“this”,并且这是不可接受的。这在 namedCaller
的实现中完成。
8 讨论
这种方法唯一可能令人困惑的是,参数传递的方法与调用语句中传递的参数无关。
应该很明显,我演示的调用
f(f.medium = 2, f.last = 3, f.first = 1);
完全等同于以下语句
f.medium = 2, f.last = 3, f.first = 1; f();
请注意,下一次调用 f()
将使所有参数变为 undefined
。我添加了对所有参数值(它们是 namedCaller
返回的函数对象实例的实例属性)的清理,只是为了与调用语义保持一致。
这是好是坏?我担心了一段时间,直到我意识到这没什么不对,因为这种概念特别适用于 JavaScript。在 JavaScript 中,参数集是纯粹的解释性特征。任何实际参数集都可以应用于具有任何其他形式参数集的函数,此外,这种没有严格一对一对应关系的对被广泛使用。如果在调用时,形式参数没有相应的实际参数,它将变成 undefined
对象。如果在描述形式参数较少的函数的调用中添加了一些额外的实际参数,这些额外的参数将被简单地忽略,或者,或者,使用 arguments
对象(请参阅 https://mdn.org.cn/en-US/docs/Web/JavaScript/Reference/Functions/arguments)将其计入。
所以,忽略调用圆括号中传递的参数列表中的参数在 JavaScript 中并不少见。毕竟,调用函数的开发人员需要对预期内容有所了解。在我的方法中,这更容易记住,首先要记住的是,参数集是通过赋值集传递的,而不是通过参数列表。
另一个相关的顾虑是允许混合位置参数和命名参数的想法,这是通常在具有命名方法参数的已编译语言中具有的功能。与上述解释类似的考虑导致这样一个想法:当应用于 JavaScript 时,它几乎没有意义。毕竟,调用中的位置参数不仅仅是位置参数,通常被称为必需参数,但在 JavaScript 函数调用中没有什么必需的。
最后,另一个顾虑是机制的开销和性能成本。在这里,重要的是要注意,大部分开销在于函数 namedCaller
本身,当获取并解析形式参数列表时。响应包装器函数调用时,不会发生类似的事情。如果有人仅为此调用一次而使用此机制,那是非常不可能的,如果只需要一次调用,它很可能在函数定义后立即进行。(另请参阅 IIFE,立即调用的函数表达式。)
当通过对 namedCaller
的一次调用定义的函数在同一个函数包装器对象上调用多次时,开销和性能成本与将值赋给相同数量的对象属性几乎相同,对于解释型语言来说,这并不是一个非常严重的开销。
9 结论
提出的解决方案非常紧凑,足够方便,而且可能是迄今为止最全面和最安全的解决方案,与其他任何方法相比。特殊功能是防止参数拼写错误。该解决方案仅带来最小的开销和最小的性能成本。