在JavaScript中,是否有一种方法可以在函数调用中提供命名参数?

433
C#,例如,允许使用命名参数,如下所示:
calculateBMI(70, height: 175);

我觉得这非常有用。我如何在JavaScript中实现类似的效果?
我尝试过做一些类似的事情,比如:
myFunction({ param1: 70, param2: 175 });

function myFunction(params){
  // Check if params is an object
  // Check if the parameters I need are non-null
  // Blah blah
}

但这样看起来有些尴尬。有没有更简单的方法?

1
我不认为这是可能的,但你可以尝试在空白处放置一些未定义的内容。这样做非常糟糕。使用对象,它更好。 - Vladislav Qulin
39
不好意思,JavaScript/EcmaScript不支持命名参数。 - smilly92
2
зҺ°жңүзҡ„JavaScriptдёӯзҡ„Functionж— жі•жӣҙж”№JavaScriptзҡ„ж ёеҝғиҜӯжі•гҖӮ - Gareth
1
Coffeescript支持命名参数。 - mquandalle
6
我认为JavaScript不支持这个功能。我认为最接近命名参数的方法是(1)添加注释calculateBMI (70,/*height:*/ 175);,(2)提供一个对象 calculateBMI(70, {height: 175}),或者(3)使用一个常量 const height = 175; calculateBMI(70, height); - tim-montague
显示剩余5条评论
13个回答

435

ES2015及以后版本

在ES2015中,参数解构可用于模拟命名参数。这需要调用者传递一个对象,但如果你还使用默认参数,则可以避免在函数内部进行的所有检查:

myFunction({ param1 : 70, param2 : 175});

function myFunction({param1, param2}={}){
  // ...function body...
}

// Or with defaults, 
function myFunc({
  name = 'Default user',
  age = 'N/A'
}={}) {
  // ...function body...
}

ES5

有一种方法可以接近你想要的效果,但它基于Function.prototype.toString[ES5]的输出结果,这在某种程度上依赖于实现,因此可能不跨浏览器兼容。

思路是从函数的字符串表示中解析参数名称,以便将一个对象的属性与对应的参数关联起来。

然后函数调用可能会像这样:

func(a, b, {someArg: ..., someOtherArg: ...});

其中ab是位置参数,而最后一个参数是具有命名参数的对象。

例如:

var parameterfy = (function() {
    var pattern = /function[^(]*\(([^)]*)\)/;

    return function(func) {
        // fails horribly for parameterless functions ;)
        var args = func.toString().match(pattern)[1].split(/,\s*/);

        return function() {
            var named_params = arguments[arguments.length - 1];
            if (typeof named_params === 'object') {
                var params = [].slice.call(arguments, 0, -1);
                if (params.length < args.length) {
                    for (var i = params.length, l = args.length; i < l; i++) {
                        params.push(named_params[args[i]]);
                    }
                    return func.apply(this, params);
                }
            }
            return func.apply(null, arguments);
        };
    };
}());

你可以这样使用:

var foo = parameterfy(function(a, b, c) {
    console.log('a is ' + a, ' | b is ' + b, ' | c is ' + c);     
});

foo(1, 2, 3); // a is 1  | b is 2  | c is 3
foo(1, {b:2, c:3}); // a is 1  | b is 2  | c is 3
foo(1, {c:3}); // a is 1  | b is undefined  | c is 3
foo({a: 1, c:3}); // a is 1  | b is undefined  | c is 3 

演示

这种方法存在一些缺点(请注意!):

  • 如果最后一个参数是一个对象,它会被视为“命名参数对象”
  • 你总会得到和函数定义中一样多的参数,但其中一些可能的值是undefined(这与根本没有值不同)。这意味着你不能使用arguments.length来测试传递了多少个参数。

除了让函数创建包装器之外,你还可以编写一个接受函数和各种值作为参数的函数,例如:

call(func, a, b, {posArg: ... });

甚至可以扩展Function.prototype,这样你就可以执行:

foo.execute(a, b, {posArg: ...});

1
非常微小的一点建议:我认为这种方法无法捕获 EcmaScript 6 箭头函数。目前不是一个很大的问题,但在你回答中的注意事项部分可能值得提及。 - NobodyMan
3
@NobodyMan:没错。在箭头函数出现之前,我写了这个答案。在ES6中,我会使用参数解构。 - Felix Kling
1
关于 undefined 与“没有值”的问题,可以补充说明这正是JS函数默认值的工作方式——将 undefined 视为缺失的值。 - Dmitri Zaitsev
我不理解这个缺陷的含义:“如果最后一个参数是一个对象,则将其视为‘命名参数对象’” - 有人能解释一下吗? - Yarin
1
@Yarin:这个“解决方案”假设如果一个对象作为函数调用的最后一个参数传递,那么该对象包含“命名”参数的值。但是,如果函数自然地期望传递一个对象作为参数,则该假设不起作用。例如,请考虑function deleteUser(user) { ... }deleteUser({name: ...})无法工作,因为该对象被解释为“命名参数对象”。您必须编写deleteUser({user: {name: ...}}) - Felix Kling
显示剩余10条评论

98

不需要 - 对于这个问题,JavaScript的对象方法是解决方案。只要您的函数期望一个对象而不是单独的参数,那么就没有问题。


43
被问到的具体问题的答案是,没有一种本地方法可以收集声明的变量或函数,而不知道它们存在于特定的命名空间中。虽然这个答案很简短,但它并不因此不适合作为一个答案 - 您不会同意吗?还有更短的答案,比如“不可能”,虽然它们很短,但也是问题的答案。 - Mitya
4
现如今有ES6这个东西:http://www.2ality.com/2011/11/keyword-parameters.html。 - slacktracer
3
这绝对是一个公正的回答——“命名参数”是一种语言特性。在没有这样的特性的情况下,对象方法是次佳选择。 - fatuhoku
当使用对象方法时,如果我们需要重命名参数名称,则重构可能会成为一个问题。我们需要进入每个调用并更改参数对象;有很大的机会忽略某个调用并在运行时遇到问题。 - NSaran

48
很多人建议使用“传递对象”技巧来使用命名参数。
/**
 * My Function
 *
 * @param {Object} arg1 Named arguments
 */
function myFunc(arg1) { }

myFunc({ param1 : 70, param2 : 175});

这个方法通常很有效,但是......对于大多数集成开发环境(IDE)而言,我们这些开发者很多时候都会依赖 IDE 中的类型/参数提示。我个人使用 PhpStorm(以及其他 JetBrains 的 IDE,比如针对 Python 的PyCharm 和 针对 Objective-C 的 AppCode)。

然而,“传递对象”技巧最大的问题在于,当你调用函数时,IDE 只会给出一个单一的类型提示......我们应该如何知道哪些参数和类型应该放入 arg1 对象中呢?

I have no idea what parameters should go in arg1

所以... “传递对象”技巧对我不起作用... 它实际上会导致更多的麻烦,因为我需要查看每个函数的文档块,才能知道函数期望什么参数... 当然,它对于维护现有代码很好,但编写新代码时却很糟糕。

好吧,这是我使用的技术... 现在,可能会有一些问题,一些开发人员可能会告诉我我做错了,当涉及到这些事情时,我持开放态度... 我总是愿意寻找更好的完成任务的方法... 所以,如果这种技术存在问题,则欢迎评论。

/**
 * My Function
 *
 * @param {string} arg1 Argument 1
 * @param {string} arg2 Argument 2
 */
function myFunc(arg1, arg2) { }

var arg1, arg2;
myFunc(arg1='Param1', arg2='Param2');

这样,我就可以兼顾两个方面。在编写新代码时,我的IDE会给出所有正确的参数提示,所以编写起来很容易。而在以后维护代码时,我一眼就能看到传递给函数的值和参数名称。我唯一觉得有点麻烦的是要将参数名称声明为局部变量,以避免污染全局命名空间。当然,这需要额外输入一些内容,但与编写新代码或维护现有代码时查找文档块所需的时间相比,这是微不足道的。

Now, I have all the parameters and types when creating new code

更新 - 2022

JavaScript 现在可以利用 ES6 中的对象解构语法来实现类似于命名参数的能力。大多数较新的浏览器都支持这个特性 参见浏览器支持情况

其工作方式如下:


// Define your function like this
function myFunc({arg1, arg2, arg3}) {
    // Function body
}

// Call your function like this
myFunc({arg1: "value1", arg2: "value2", arg3: "value3"})

// You can also have default values for arguments
function myFunc2({firstName, lastName, age = 21}) {
    // Function body
}

// And you can call it with or without an "age" argument
myFunc({firstName: "John", lastName: "Doe"}) // Age will be 21
myFunc({firstName: "Jane", lastName: "Doe", age: 22})

最好的部分是,现在大多数集成开发环境都支持这种语法,并且您可以获得良好的参数提示支持。

TypeScript

对于那些使用TypeScript的人,您可以使用以下语法来完成相同的操作。
function myFunc(
    {firstName, lastName, age = 21}:
    {firstName: string, lastName: string, age?: number}
) {
    // Function body
}

或者,使用一个接口

interface Params {
    firstName: string
    lastName: string
    age?: number
}

function myFunc({firstName, lastName, age = 21}: Params) {
    // Function body
}

3
这个技术的唯一限制在于你无法改变参数的顺序……不过我个人并不介意。 - Ray Perea
29
当未来的维护者认为他们可以更改参数顺序(但实际上不行)时,似乎这只会带来麻烦。 - nobody
12
我认为 myFunc(/*arg1*/ 'Param1', /*arg2*/ 'Param2');myFunc(arg1='Param1', arg2='Param2'); 更好,因为前者不会让读者误以为有任何实际的命名参数。 - Eric
10
提议的模式与命名参数无关,顺序仍然很重要,名称不需要与实际参数保持同步,命名空间会受到不必要变量的污染,并暗示了不存在的关系。 - Dmitri Zaitsev
4
抱歉,但这并不值得称之为“技巧”,它没有回答问题因为它没有提供命名参数,它引入了不必要的变量,如果您只考虑myFunc(arg2='Param2', arg1='Param1');或者myFunc(arg2='Param2');的话,它是完全误导的。这是一个完全错误的模式,只是一个可怕的答案。请删除! - Elmar Zander
显示剩余14条评论

27

如果您希望清楚地表明每个参数的含义,而不仅仅是称之为

someFunction(70, 115);

请按照以下步骤操作:

var width = 70, height = 115;
someFunction(width, height);

当然,这是多了一行代码,但它在可读性方面胜出。


6
遵循KISS原则 +1,这也有助于调试。然而,我认为每个变量应该放在自己的一行上,尽管会稍微影响性能(参考https://dev59.com/-WHVa4cB1Zd3GeqPm3Ja)。 - clairestreb
53
重点不仅在于多一行代码,还在于参数顺序和可选性。因此,你可以使用命名参数来编写这个函数:someFunction(height: 115);,但如果你写成 someFunction(height);,实际上是设置了宽度。 - Francisco Presencia
1
在这种情况下,CoffeeScript支持命名参数。它将允许您仅编写 someFunction(width = 70, height = 115);。生成的JavaScript代码中,变量在当前作用域的顶部声明。 - Audun Olsen

7

另一种方法是使用适当对象的属性,例如:

function plus(a,b) { return a+b; };

Plus = { a: function(x) { return { b: function(y) { return plus(x,y) }}}, 
         b: function(y) { return { a: function(x) { return plus(x,y) }}}};

sum = Plus.a(3).b(5);

当然,对于这个虚构的示例来说,它有些毫无意义。但在函数看起来像以下情况下:
do_something(some_connection_handle, some_context_parameter, some_value)

这可能更有用。它也可以与“parameterfy”想法结合起来,以一种通用的方式从现有函数创建这样一个对象。对于每个参数,它将创建一个成员,可以计算函数的部分求值版本。

当然,这个想法与Schönfinkeling aka Currying相关。


1
这是一个不错的想法,如果你将其与参数内省技巧相结合,它会变得更好。不幸的是,它无法处理可选参数。 - Eric

3

使用对象作为命名参数调用函数 f

o = {height: 1, width: 5, ...}

基本上是在调用它的组合f(...g(o)),我使用了扩展语法,而g是一个“绑定”映射,将对象值与其参数位置连接起来。

绑定映射正是缺失的成分,可以通过其键的数组来表示:

// map 'height' to the first and 'width' to the second param
binding = ['height', 'width']

// take binding and arg object and return aray of args
withNamed = (bnd, o) => bnd.map(param => o[param])

// call f with named args via binding
f(...withNamed(binding, {hight: 1, width: 5}))

请注意三种解耦组成部分:函数、带有命名参数的对象和绑定。这种解耦允许使用此构造具有很大的灵活性,其中绑定可以在函数定义中任意自定义,并且可以在函数调用时任意扩展。
例如,您可能希望在函数定义中缩写heightwidthhw,以使其更短、更简洁,同时仍然希望以完整名称进行调用以增加清晰度:
// use short params
f = (h, w) => ...

// modify f to be called with named args
ff = o => f(...withNamed(['height', 'width'], o))

// now call with real more descriptive names
ff({height: 1, width: 5})

这种灵活性在函数式编程中也更为有用,因为函数可以任意转换而不会丢失原始参数名称。


2

还有一种方法。如果你通过引用传递对象,那么这个对象的属性将出现在函数的本地作用域中。我知道这对Safari有效(没有检查其他浏览器),我不知道这个功能是否有名称,但以下示例说明了它的用法。

虽然实际上我认为除了你已经使用的技术之外,它并没有提供任何功能价值,但从语义上来说更加清晰。并且仍然需要传递对象引用或对象文字。

function sum({ a:a, b:b}) {
    console.log(a+'+'+b);
    if(a==undefined) a=0;
    if(b==undefined) b=0;
    return (a+b);
}

// will work (returns 9 and 3 respectively)
console.log(sum({a:4,b:5}));
console.log(sum({a:3}));

// will not work (returns 0)
console.log(sum(4,5));
console.log(sum(4));

2
这虽然是伪代码,但我相信它会起作用(我知道它在TypeScript中起作用;我正在将其应用于JavaScript)。
// Target Function
const myFunc = (a=1,b=2,c=3) => {a+b+c}

// Goal usage:
myFunc(a=5, b=6) // 14
myFunc(c=0) // 3

// Set your defaults
const myFuncDefaults = {a:1, b:2, c:3};

// Override them with passed parameters
const myFuncParams = (params) => { return Object.assign(myFuncDefaults, params)}


// Use the overloaded dict as the input
const myFunc2 = (params) => {
  let {a, b, c} = myFuncParams(params);
  return myFunc(a, b, c)
}

// Usage:
myFunc({a:5, b:6}) // 14
myFunc({c:0}) // 3

// Written more succinctly:
const myFunc = (params) => {
  let {a,b,c} = Object.assign({a:1, b:2, c:3}, params)
  return a + b + c
}

就算不值得一提,TypeScript 也可以通过提示使这个过程更加顺畅:

interface IParams {
  a: number;
  b: number;
  c: number;
}

const myFunc = (params: Partial<IParams>): number => {
  const default: IParams = {a:1, b:2, c:3};
  let {a, b, c} = Object.assign(default, params)
  return a + b + c
}

1

注意:我的2016年的回答在评论中被指出不正确且误导性。

尝试使用Node-6.4.0(process.versions.v8 ='5.0.71.60'),Node Chakracore-v7.0.0-pre8和Chrome-52(V8 = 5.2.361.49)后,我注意到命名参数几乎已经实现,但顺序仍然具有优先权。我找不到ECMA标准中的相关内容。

>function f(a=1, b=2){ console.log(`a=${a} + b=${b} = ${a+b}`) }

> f()
a=1 + b=2 = 3
> f(a=5)
a=5 + b=2 = 7
> f(a=7, b=10)
a=7 + b=10 = 17

但是需要有顺序!!这是标准行为吗?

> f(b=10)
a=10 + b=2 = 12

14
这并不是你想象中的那样。b=10 表达式的结果是 10,这就是传递给函数的值。 f(bla=10) 也可以实现同样的效果(它将 10 赋值给变量 bla 并在之后将该值传递给函数)。 - Matthias Kestenholz
4
这也会在全局范围内创建 ab 变量,作为副作用。 - Gershom Maes
Re *"我的2016年的回答"*:你是指这个回答吗?如果是的话,最好删除它(或修复它)。 - Peter Mortensen
我认为这两个被点赞的评论帮助我更好地理解了ES-6+中的命名参数,并且对于新手或来自其他行为不同的语言的人仍然有用。我可能是错的。 - jgran

1

如果我理解正确的话,你的解决方案需要在函数定义中区分位置参数和命名参数,这意味着我在调用时没有自由选择。 - Dmitri Zaitsev
在 Python 社区中(使用关键字参数是日常操作),我们认为强制用户通过关键字指定可选参数是一个非常好的想法;这有助于避免错误。 - Tobias
@Tobias 强制用户在JS中这样做是一个已解决的问题:f(x, opt),其中opt是一个对象。 它是否有助于避免或创建错误(例如由拼写错误引起的错误和记住关键字名称的痛苦)仍然是一个问题。 - Dmitri Zaitsev
@DmitriZaitsev:在Python中,这严格避免了错误,因为(当然)关键字参数是其中一个核心语言特性。对于仅限关键字参数,Python 3有特殊语法(而在Python 2中,您需要逐个从kwargs dict中弹出键,并最终引发TypeError如果还有未知键)。您的f(x,opt)解决方案_允许_f函数像Python 2一样执行某些操作,但您需要自己处理所有默认值。 - Tobias
@Tobias 这个提议是否适用于JS?它似乎描述了f(x, opt)已经如何工作,而我不明白这如何有助于回答问题,例如您想要执行request(url)request({url: url})的情况,通过将url设置为仅限关键字参数是不可能的。 - Dmitri Zaitsev
我只是回答了你的评论,“这在Javascript中是一个已解决的问题”。不,它并不是,并且可能永远不会是,因为存在不同的语言概念。在JS中,你只能强制用户提供一个选项对象;而在Python中,函数签名可以告诉你每个关键字仅选项的确切名称。 - Tobias

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接