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

我发现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
}

这个方法我已经用过了。还有别的办法吗?

我可以使用任何库来做到这一点。

307692 次浏览

不——对象方法是JavaScript对此的答案。如果你的函数需要一个对象而不是单独的参数,这就没有问题。

ES2015及后续版本

在ES2015中,< em >参数destructuring < / em >可以用来模拟命名参数。它需要调用者传递一个对象,但是如果你也使用默认参数,你可以避免函数内部的所有检查:

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 < em > <一口> [ES5] < /一口> < / em >的输出,在某种程度上依赖于实现,所以它可能不跨浏览器兼容。

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

函数调用就像这样

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

DEMO

这个方法有一些缺点(你已经被警告过了!):

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

与其让函数创建包装器,还可以让函数接受函数和各种值作为参数,例如

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

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

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

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

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也就是curiling有关。

如果你想弄清楚每个参数是什么,而不仅仅是调用

someFunction(70, 115);

做以下几点:

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

当然,这是额外的一行代码,但它在可读性上更胜一筹。

很多人说只要使用“传递一个对象”就可以了。这样你就有了命名参数。

/**
* My Function
*
* @param {Object} arg1 Named arguments
*/
function myFunc(arg1) { }


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

这很有效,除了…当涉及到大多数IDE时,我们很多开发人员依赖于IDE中的类型/参数提示。我个人使用PhpStorm(以及其他JetBrains ide,如PyCharm用于Python, AppCode用于objective - c)。

而使用“传递对象”的最大问题;窍门是当你调用函数时,IDE会给你一个单一的类型提示,就是这样…我们怎么知道什么参数和类型应该进入arg1对象?

I have no idea 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给了我所有适当的参数提示。而且,在以后维护代码时,我不仅可以一目了然地看到传递给函数的值,还可以看到参数的名称。我看到的唯一开销是将参数名声明为局部变量,以避免污染全局名称空间。当然,这是一些额外的输入,但与编写新代码或维护现有代码时查找文档块所花费的时间相比,这是微不足道的。

现在,当创建新代码时,我有所有的参数和类型

更新- 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})

最好的部分是现在大多数IDE都支持这种语法,并且您可以获得良好的参数提示支持

打印稿

对于那些使用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
}

还有另一种方法。如果通过引用传递对象,则该对象的属性将出现在函数的局部作用域中。我知道这适用于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));

NB。正如评论中提到的,我2016年的答案是不正确的,具有误导性。

尝试Node-6.4.0 (process.versions. Node-6.4.0)。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

来自Python,这让我很困扰。我为节点编写了一个简单的包装器/代理程序,它将接受位置对象和关键字对象。

https://github.com/vinces1979/node-def/blob/master/README.md

与通常认为的相反,命名参数可以在标准的旧式JavaScript中(仅用于布尔参数)通过简单、整洁的编码约定实现,如下所示。

function f(p1=true, p2=false) {
...
}


f(!!"p1"==false, !!"p2"==true); // call f(p1=false, p2=true)

警告:

  • 必须保留参数的顺序——但这种模式仍然有用,因为它可以很明显地显示哪个实际参数对应哪个形式参数,而不必为函数签名进行grep或使用IDE。

  • 这只适用于布尔值。但是,我相信可以使用JavaScript独特的类型强制语义为其他类型开发类似的模式。

调用函数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}))

注意三个分离的成分:函数,带有命名参数的对象和绑定。这种解耦为使用此构造提供了很大的灵活性,其中绑定可以在函数的定义中任意定制,并在函数调用时任意扩展。

例如,在函数的定义中,你可能想要将heightwidth缩写为hw,以使其更简短和干净,同时为了清晰起见,你仍然想要使用全名调用它:

// 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})

这种灵活性对于函数式编程也更有用,在函数式编程中,可以任意转换函数,而丢失其原始参数名。

这确实是伪代码,但我相信它会起作用(我知道它在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
}

是的,嗯,有点。我找到了两个解决方案。我只解释一个。

在这个解决方案中,我们放弃了位置参数。

我们可以使用一个对象(几乎与Python dict相同)来传递参数。

在这个例子中,我使用函数来生成一个图像文件的名称:

// First we define our function with just ONE argument
function name_of_img(img_desc){


// With this step, any undefined value will be assigned a value
if(img_desc.size == undefined) {img_desc.size = "400x500"}
if(img_desc.format == undefined) {img_desc.format = ".png"}


console.log(img_desc.size + img_desc.format)
}


// Notice inside our function we're passing a dict/object


name_of_img({size: "200x250", format : ".jpg"})
// In Python name_of_img(size="200x250" , format="jpg")
// returns "200x250.jpg"


name_of_img({size: "1200x950"})
// In Python name_of_img(size="1200x950")
// returns "1200x950.png"

我们可以修改这个例子,所以我们也可以使用位置参数,我们也可以修改它,所以非有效参数可以传递,我想我会做一个关于这个的GitHub存储库。