Existe-t-il un moyen de fournir des paramètres nommés dans un appel de fonction en JavaScript?

207

Je trouve la fonctionnalité des paramètres nommés en C # très utile dans certains cas.

calculateBMI(70, height: 175);

Que puis-je utiliser si je le souhaite en JavaScript?


Ce que je ne veux pas, c'est ceci:

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
}

Cette approche que j'ai déjà utilisée. Y a-t-il un autre moyen?

Je peux utiliser n'importe quelle bibliothèque pour ce faire.

Robin Maben
la source
Je ne pense pas que ce soit possible, mais vous pouvez essayer de mettre des indéfinis dans des endroits vides. Ce qui est vraiment mauvais. Utilisez l'objet, c'est bon.
Vladislav Qulin
14
Non, JavaScript / EcmaScript ne prend pas en charge les paramètres nommés. Désolé.
smilly92
1
Je le sais déjà. Merci. Je cherchais un moyen qui consiste à modifier ce que l'existant Functionen javascript peut faire.
Robin Maben
1
L'existant Functionen Javascript ne peut pas changer la syntaxe de base de Javascript
Gareth
2
Je ne pense pas que Javascript supporte cette fonctionnalité. Je pense que le plus proche des paramètres nommés est (1) ajouter un commentaire calculateBMI(70, /*height:*/ 175);, (2) fournir un objet calculateBMI(70, {height: 175}), ou (3) utiliser une constante const height = 175; calculateBMI(70, height);.
tfmontague

Réponses:

213

ES2015 et versions ultérieures

Dans ES2015, la déstructuration des paramètres peut être utilisée pour simuler des paramètres nommés. Il faudrait que l'appelant passe un objet, mais vous pouvez éviter toutes les vérifications à l'intérieur de la fonction si vous utilisez également des paramètres par défaut:

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

Il existe un moyen de se rapprocher de ce que vous voulez, mais il est basé sur la sortie de Function.prototype.toString [ES5] , qui dépend dans une certaine mesure de l'implémentation, de sorte qu'il peut ne pas être compatible avec plusieurs navigateurs.

L'idée est d'analyser les noms de paramètres à partir de la représentation sous forme de chaîne de la fonction afin de pouvoir associer les propriétés d'un objet au paramètre correspondant.

Un appel de fonction pourrait alors ressembler à

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

aetb sont des arguments positionnels et le dernier argument est un objet avec des arguments nommés.

Par exemple:

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);
        };
    };
}());

Que vous utiliseriez comme:

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

Il y a quelques inconvénients à cette approche (vous avez été prévenu!):

  • Si le dernier argument est un objet, il est traité comme un "objet argument nommé"
  • Vous obtiendrez toujours autant d'arguments que vous l'avez défini dans la fonction, mais certains d'entre eux peuvent avoir la valeur undefined(ce qui est différent de n'avoir aucune valeur). Cela signifie que vous ne pouvez pas utiliser arguments.lengthpour tester le nombre d'arguments transmis.

Au lieu d'avoir une fonction créant le wrapper, vous pouvez également avoir une fonction qui accepte une fonction et diverses valeurs comme arguments, telles que

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

ou même étendre Function.prototypepour que vous puissiez faire:

foo.execute(a, b, {posArg: ...});
Felix Kling
la source
Ouais ... voici un exemple pour cela: jsfiddle.net/9U328/1 (bien que vous devriez plutôt utiliser Object.definePropertyet ensemble enumerablepour false). Il faut toujours être prudent lors de l'extension des objets natifs. L'approche globale semble un peu hacky, donc je ne m'attendrais pas à ce que cela fonctionne maintenant et pour toujours;)
Felix Kling
C'est noté. Je vais commencer à mettre cela à profit. Marquer comme réponse! ... Pour l'instant;)
Robin Maben
1
Nitpick très mineur: je ne pense pas que cette approche va capturer les fonctions de flèche EcmaScript 6 . Pas une énorme préoccupation pour le moment, mais mérite d'être mentionné dans la section mises en garde de votre réponse.
NobodyMan
3
@NobodyMan: Vrai. J'ai écrit cette réponse avant que les fonctions fléchées ne soient chose. Dans ES6, je recourrais en fait à la déstructuration des paramètres.
Felix Kling
1
Concernant la question de undefined«aucune valeur», on peut ajouter que c'est exactement ainsi que fonctionnent les valeurs par défaut des fonctions JS - en les traitant undefinedcomme une valeur manquante.
Dmitri Zaitsev
71

Non - l'approche objet est la réponse de JavaScript à cela. Cela ne pose aucun problème à condition que votre fonction attend un objet plutôt que des paramètres séparés.

Mitya
la source
35
@RobertMaben - La réponse à la question spécifique posée est qu'il n'y a aucun moyen natif de collecter des vars ou des fonctions déclarées sans savoir qu'ils vivent sur un espace de noms particulier. Juste parce que la réponse est courte, elle ne nie pas sa pertinence en tant que réponse - n'êtes-vous pas d'accord? Il existe des réponses beaucoup plus courtes, du type «non, pas possible». Ils peuvent être courts, mais ils sont aussi la réponse à la question.
Mitya
3
De nos jours, il y a cependant es6: 2ality.com/2011/11/keyword-parameters.html
slacktracer
1
C'est certainement une réponse juste - les «paramètres nommés» sont une fonctionnalité de langue. L'approche objet est la deuxième meilleure chose en l'absence d'une telle fonctionnalité.
fatuhoku
32

Ce problème est une bête noire depuis un certain temps. Je suis un programmeur chevronné avec de nombreuses langues à mon actif. L'une de mes langues préférées que j'ai eu le plaisir d'utiliser est Python. Python prend en charge les paramètres nommés sans aucune astuce .... Depuis que j'ai commencé à utiliser Python (il y a quelque temps), tout est devenu plus facile. Je crois que chaque langue devrait prendre en charge les paramètres nommés, mais ce n'est tout simplement pas le cas.

Beaucoup de gens disent d'utiliser simplement l'astuce "Passer un objet" pour que vous ayez nommé des paramètres.

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

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

Et cela fonctionne très bien, sauf ..... en ce qui concerne la plupart des IDE, beaucoup d'entre nous, développeurs, s'appuient sur des indices de type / argument dans notre IDE. J'utilise personnellement PHP Storm (avec d'autres IDE JetBrains comme PyCharm pour python et AppCode pour Objective C)

Et le plus gros problème avec l'utilisation de l'astuce "Passer un objet" est que lorsque vous appelez la fonction, l'EDI vous donne un indice de type unique et c'est tout ... Comment sommes-nous censés savoir quels paramètres et types doivent entrer dans le objet arg1?

Je n'ai aucune idée des paramètres à utiliser dans arg1

Donc ... l'astuce "Passer un objet" ne fonctionne pas pour moi ... Cela provoque en fait plus de maux de tête lorsque je regarde le docblock de chaque fonction avant de savoir quels paramètres la fonction attend .... Bien sûr, c'est génial pour lorsque vous maintenez le code existant, mais c'est horrible pour écrire du nouveau code.

Eh bien, c'est la technique que j'utilise .... Maintenant, il peut y avoir des problèmes avec cela, et certains développeurs peuvent me dire que je le fais mal, et j'ai un esprit ouvert quand il s'agit de ces choses ... Je suis toujours prêt à chercher de meilleures façons d'accomplir une tâche ... Donc, s'il y a un problème avec cette technique, alors les commentaires sont les bienvenus.

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

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

De cette façon, j'ai le meilleur des deux mondes ... le nouveau code est facile à écrire car mon IDE me donne tous les conseils d'argument appropriés ... Et, tout en conservant le code plus tard, je peux voir en un coup d'œil, non seulement le valeur passée à la fonction, mais aussi le nom de l'argument. La seule surcharge que je vois est de déclarer vos noms d'argument en tant que variables locales pour éviter de polluer l'espace de noms global. Bien sûr, c'est un peu de frappe supplémentaire, mais trivial par rapport au temps qu'il faut pour rechercher des docblocks lors de l'écriture de nouveau code ou de la maintenance du code existant.

Maintenant, j'ai tous les paramètres et types lors de la création de nouveau code

Ray Perea
la source
2
La seule chose avec cette technique est le fait que vous ne pouvez pas changer l'ordre des paramètres ... Je suis personnellement d'accord avec cela.
Ray Perea
14
On dirait que cela demande juste des problèmes quand un futur responsable arrive et pense qu'il peut changer l'ordre des arguments (mais évidemment pas).
personne le
1
@AndrewMedico Je suis d'accord ... il semble que vous pouvez simplement changer l'ordre des arguments comme en Python. La seule chose que je peux dire à ce sujet, c'est qu'ils découvriront très rapidement lorsque le changement de l'ordre des arguments interrompt le programme.
Ray Perea
7
Je dirais que myFunc(/*arg1*/ 'Param1', /*arg2*/ 'Param2');c'est mieux que myFunc(arg1='Param1', arg2='Param2');, parce que cela n'a aucune chance de tromper le lecteur que tous les arguments nommés réels se produisent
Eric
3
Le modèle proposé n'a rien à voir avec les arguments nommés, l'ordre est toujours important, les noms n'ont pas besoin de rester synchronisés avec les paramètres réels, l'espace de noms est pollué par des variables inutiles et des relations implicites sont absentes.
Dmitri Zaitsev
25

Si vous voulez préciser ce que sont chacun des paramètres, plutôt que d'appeler simplement

someFunction(70, 115);

pourquoi ne pas faire ce qui suit

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

Bien sûr, c'est une ligne de code supplémentaire, mais elle gagne en lisibilité.

dav_i
la source
5
+1 pour suivre le principe KISS, et cela aide également au débogage. Cependant, je pense que chaque var devrait être sur sa propre ligne, bien qu'avec un léger impact sur les performances ( http://stackoverflow.com/questions/9672635/javascript-var-statement-and-performance ).
clairestreb
21
Il ne s'agit pas seulement de la ligne de code supplémentaire, il s'agit également de l'ordre des arguments et de les rendre facultatifs. Vous pouvez donc écrire ceci avec des paramètres nommés:, someFunction(height: 115);mais si vous écrivez, someFunction(height);vous définissez en fait la largeur.
Francisco Presencia
Dans ce cas, CoffeeScript prend en charge les arguments nommés. Il vous permettra d'écrire juste someFunction(width = 70, height = 115);. Les variables sont déclarées en haut de l'étendue actuelle dans le code JavaScript généré.
Audun Olsen du
6

Une autre façon serait d'utiliser les attributs d'un objet approprié, par exemple comme ceci:

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);

Bien sûr, pour cet exemple composé, il est quelque peu dénué de sens. Mais dans les cas où la fonction ressemble

do_something(some_connection_handle, some_context_parameter, some_value)

cela pourrait être plus utile. Il pourrait également être combiné avec l'idée "parameterfy" pour créer un tel objet à partir d'une fonction existante de manière générique. C'est-à-dire que pour chaque paramètre, il créerait un membre pouvant évaluer une version partiellement évaluée de la fonction.

Cette idée est bien sûr liée à Schönfinkeling alias Currying.

Udo Klein
la source
C'est une bonne idée et elle devient encore meilleure si vous la combinez avec les astuces d'introspection des arguments. Malheureusement, il ne fonctionne pas avec des arguments facultatifs
Eric
2

Il y a une autre manière. Si vous passez un objet par référence, les propriétés de cet objet apparaîtront dans la portée locale de la fonction. Je sais que cela fonctionne pour Safari (je n'ai pas vérifié les autres navigateurs) et je ne sais pas si cette fonctionnalité a un nom, mais l'exemple ci-dessous illustre son utilisation.

Bien que dans la pratique, je ne pense pas que cela offre une valeur fonctionnelle au-delà de la technique que vous utilisez déjà, c'est un peu plus propre sémantiquement. Et cela nécessite toujours de passer une référence d'objet ou un objet littéral.

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));
Allen Ellison
la source
2

En essayant Node-6.4.0 (process.versions.v8 = '5.0.71.60') et Node Chakracore-v7.0.0-pre8 puis Chrome-52 (V8 = 5.2.361.49), j'ai remarqué que les paramètres nommés sont presque mis en œuvre, mais cet ordre a toujours préséance. Je ne trouve pas ce que dit la norme 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

Mais l'ordre est requis !! Est-ce le comportement standard?

> f(b=10)
a=10 + b=2 = 12
jgran
la source
10
Cela ne fait pas ce que vous pensez. Le résultat de l' b=10expression est 10, et c'est ce qui est transmis à la fonction. f(bla=10)fonctionnerait également (il affecte 10 à la variable blaet transmet ensuite la valeur à la fonction).
Matthias Kestenholz
1
Cela crée également des variables aet bdans la portée globale comme effet secondaire.
Gershom
2

Fonction d'appel favec des paramètres nommés passés en tant qu'objet

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

appelle essentiellement sa composition f(...g(o))où j'utilise la syntaxe de propagation etg est une carte "contraignante" reliant les valeurs des objets à leurs positions de paramètres.

La carte de liaison est précisément l'ingrédient manquant, qui peut être représenté par le tableau de ses clés:

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

Notez les trois ingrédients découplés: la fonction, l'objet avec des arguments nommés et la liaison. Ce découplage permet une grande souplesse d'utilisation de cette construction, où la liaison peut être arbitrairement personnalisée dans la définition de la fonction et arbitrairement étendue au moment de l'appel de la fonction.

Par exemple, vous voudrez peut-être abréger heightet au widthfur het wà mesure de la définition de votre fonction, pour la rendre plus courte et plus propre, tandis que vous voulez toujours l'appeler avec des noms complets pour plus de clarté:

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

Cette flexibilité est également plus utile pour la programmation fonctionnelle, où les fonctions peuvent être arbitrairement transformées avec leur nom de paramètre d'origine perdu.

Dmitri Zaitsev
la source
0

Venant de Python, cela m'a dérangé. J'ai écrit un wrapper / Proxy simple pour le nœud qui acceptera les objets positionnels et les mots clés.

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

Vince Spicer
la source
Si je comprends bien, votre solution nécessite de distinguer les paramètres positionnels et nommés dans la définition de la fonction, ce qui signifie que je n'ai pas la liberté de faire ce choix au moment de l'appel.
Dmitri Zaitsev
@DmitriZaitsev: Dans la communauté Python (avec les arguments de mots-clés étant une fonctionnalité quotidienne), nous considérons que c'est une très bonne idée de pouvoir forcer les utilisateurs à spécifier des arguments optionnels par mot-clé; cela permet d'éviter les erreurs.
Tobias
@Tobias Forcer les utilisateurs à le faire dans JS est un problème résolu:, f(x, opt)optest un objet. Que cela aide à éviter ou à créer des erreurs (comme celles causées par une faute d'orthographe et la difficulté de se souvenir des noms de mots clés) reste une question.
Dmitri Zaitsev
@DmitriZaitsev: En Python, cela évite strictement les erreurs, car (bien sûr) les arguments de mots clés sont une fonctionnalité de langage de base là-bas. Pour les arguments contenant uniquement des mots clés , Python 3 a une syntaxe spéciale (tandis qu'en Python 2, vous devez extraire les clés du kwargs dict un par un et enfin lever un TypeErrors'il reste des clés inconnues). Votre f(x, opt)solution permet à la ffonction de faire quelque chose comme dans Python 2, mais vous devez gérer vous-même toutes les valeurs par défaut.
Tobias
@Tobias Cette proposition est-elle pertinente pour JS? Il semble décrire comment cela f(x, opt)fonctionne déjà, alors que je ne vois pas comment cela aide à répondre à la question, où par exemple vous voulez faire les deux request(url)et request({url: url})cela ne serait pas possible en créant urlun paramètre uniquement par mot-clé.
Dmitri Zaitsev
-1

Contrairement à ce que l'on croit généralement, les paramètres nommés peuvent être implémentés en JavaScript standard à l'ancienne (pour les paramètres booléens uniquement) au moyen d'une convention de codage simple et soignée, comme illustré ci-dessous.

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

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

Avertissements:

  • L'ordre des arguments doit être conservé - mais le modèle est toujours utile, car il rend évident quel argument réel est destiné à quel paramètre formel sans avoir à chercher la signature de la fonction ou à utiliser un IDE.

  • Cela ne fonctionne que pour les booléens. Cependant, je suis sûr qu'un modèle similaire pourrait être développé pour d'autres types en utilisant la sémantique de coercition de type unique de JavaScript.

user234461
la source
Vous parlez toujours par position, pas par nom ici.
Dmitri Zaitsev
@DmitriZaitsev oui, je l'ai même dit plus haut. Cependant, le but des arguments nommés est de faire comprendre au lecteur ce que signifie chaque argument; c'est une forme de documentation. Ma solution permet d'intégrer la documentation dans l'appel de fonction sans recourir aux commentaires, qui paraissent désordonnés.
user234461
Cela résout un problème différent. La question était de passer heightpar le nom quel que soit l'ordre param.
Dmitri Zaitsev
@DmitriZaitsev en fait, la question ne disait rien sur l'ordre des paramètres, ni pourquoi OP voulait utiliser des paramètres nommés.
user234461
Il le fait en se référant à C # où passer les paramètres par nom dans n'importe quel ordre est la clé.
Dmitri Zaitsev