Comment surcharger les fonctions en javascript?


103

Approche classique (non-js) de la surcharge:

function myFunc(){
 //code
}

function myFunc(overloaded){
 //other code
}

Javascript ne permet pas de définir plus d'une fonction avec le même nom. En tant que tel, des choses comme celle-ci apparaissent:

function myFunc(options){
 if(options["overloaded"]){
  //code
 }
}

Existe-t-il une meilleure solution de contournement pour la surcharge de fonctions en javascript autre que de passer un objet avec les surcharges?

La transmission de surcharges peut rapidement rendre une fonction trop verbeuse car chaque surcharge possible nécessiterait alors une instruction conditionnelle. L'utilisation de fonctions pour accomplir l' //codeintérieur de ces instructions conditionnelles peut entraîner des situations délicates avec les étendues.


1
Alors vous pensez mal javascript; si vous avez des problèmes avec les surcharges et la portée, vous devriez probablement avoir un nom de méthode différent car il semble que cela fasse un autre travail
joshcomley

@joshcomley - Les surcharges et la portée nécessitent du code à gérer. J'essaie simplement de m'assurer que le code que j'utilise est aussi efficace que possible. La gestion des étendues avec des solutions de contournement est bien, mais je préfère au moins essayer d'utiliser les meilleures méthodes.
Travis J

2
duplication possible d' une surcharge
Bonnes

Réponses:


126

Il y a plusieurs aspects à la surcharge d'arguments en Javascript:

  1. Arguments variables - Vous pouvez transmettre différents ensembles d'arguments (en type et en quantité) et la fonction se comportera d'une manière qui correspond aux arguments qui lui sont passés.

  2. Arguments par défaut - Vous pouvez définir une valeur par défaut pour un argument s'il n'est pas passé.

  3. Arguments nommés - L'ordre des arguments devient sans importance et vous ne nommez que les arguments que vous souhaitez transmettre à la fonction.

Vous trouverez ci-dessous une section sur chacune de ces catégories de gestion des arguments.

Arguments de variable

Comme javascript n'a pas de vérification de type sur les arguments myFunc()ou de quantité d'arguments requise, vous ne pouvez avoir qu'une seule implémentation de qui peut s'adapter aux arguments qui lui ont été passés en vérifiant le type, la présence ou la quantité d'arguments.

jQuery fait cela tout le temps. Vous pouvez rendre certains des arguments optionnels ou vous pouvez créer une branche dans votre fonction en fonction des arguments qui lui sont passés.

Lors de l'implémentation de ces types de surcharges, vous pouvez utiliser plusieurs techniques différentes:

  1. Vous pouvez vérifier la présence d'un argument donné en vérifiant si la valeur du nom d'argument déclaré est undefined.
  2. Vous pouvez vérifier la quantité totale ou les arguments avec arguments.length.
  3. Vous pouvez vérifier le type de tout argument donné.
  4. Pour des nombres variables d'arguments, vous pouvez utiliser le argumentspseudo-tableau pour accéder à n'importe quel argument donné avec arguments[i].

Voici quelques exemples:

Regardons la obj.data()méthode de jQuery . Il prend en charge quatre formes d'utilisation différentes:

obj.data("key");
obj.data("key", value);
obj.data();
obj.data(object);

Chacun déclenche un comportement différent et, sans utiliser cette forme dynamique de surcharge, nécessiterait quatre fonctions distinctes.

Voici comment on peut discerner toutes ces options en anglais, puis je les combinerai toutes en code:

// get the data element associated with a particular key value
obj.data("key");

Si le premier argument passé à .data()est une chaîne et le deuxième argument est undefined, l'appelant doit utiliser ce formulaire.


// set the value associated with a particular key
obj.data("key", value);

Si le deuxième argument n'est pas indéfini, définissez la valeur d'une clé particulière.


// get all keys/values
obj.data();

Si aucun argument n'est passé, renvoie toutes les clés / valeurs d'un objet retourné.


// set all keys/values from the passed in object
obj.data(object);

Si le type du premier argument est un objet simple, définissez toutes les clés / valeurs de cet objet.


Voici comment vous pouvez combiner tous ces éléments dans un seul ensemble de logique javascript:

 // method declaration for .data()
 data: function(key, value) {
     if (arguments.length === 0) {
         // .data()
         // no args passed, return all keys/values in an object
     } else if (typeof key === "string") {
         // first arg is a string, look at type of second arg
         if (typeof value !== "undefined") {
             // .data("key", value)
             // set the value for a particular key
         } else {
             // .data("key")
             // retrieve a value for a key
         }
     } else if (typeof key === "object") {
         // .data(object)
         // set all key/value pairs from this object
     } else {
         // unsupported arguments passed
     }
 },

La clé de cette technique est de s'assurer que toutes les formes d'arguments que vous souhaitez accepter sont identifiables de manière unique et qu'il n'y a jamais de confusion sur la forme utilisée par l'appelant. Cela nécessite généralement de classer les arguments de manière appropriée et de s'assurer que le type et la position des arguments sont suffisamment uniques pour que vous puissiez toujours dire quelle forme est utilisée.

Par exemple, si vous avez une fonction qui prend trois arguments de chaîne:

obj.query("firstArg", "secondArg", "thirdArg");

Vous pouvez facilement rendre le troisième argument facultatif et vous pouvez facilement détecter cette condition, mais vous ne pouvez pas rendre seulement le deuxième argument facultatif car vous ne pouvez pas dire lequel de ces arguments l'appelant veut passer car il n'y a aucun moyen d'identifier si le second L'argument est censé être le deuxième argument ou le deuxième argument a été omis, donc ce qui est à la place du deuxième argument est en fait le troisième argument:

obj.query("firstArg", "secondArg");
obj.query("firstArg", "thirdArg");

Étant donné que les trois arguments sont du même type, vous ne pouvez pas faire la différence entre les différents arguments et vous ne savez donc pas ce que l'appelant a voulu. Avec ce style d'appel, seul le troisième argument peut être facultatif. Si vous vouliez omettre le deuxième argument, il devrait être passé comme null(ou une autre valeur détectable) à la place et votre code le détecterait:

obj.query("firstArg", null, "thirdArg");

Voici un exemple jQuery d'arguments optionnels. les deux arguments sont facultatifs et prennent des valeurs par défaut s'ils ne sont pas passés:

clone: function( dataAndEvents, deepDataAndEvents ) {
    dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
    deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;

    return this.map( function () {
        return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
    });
},

Voici un exemple jQuery où l'argument peut être manquant ou l'un des trois types différents qui vous donne quatre surcharges différentes:

html: function( value ) {
    if ( value === undefined ) {
        return this[0] && this[0].nodeType === 1 ?
            this[0].innerHTML.replace(rinlinejQuery, "") :
            null;

    // See if we can take a shortcut and just use innerHTML
    } else if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
        (jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) &&
        !wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) {

        value = value.replace(rxhtmlTag, "<$1></$2>");

        try {
            for ( var i = 0, l = this.length; i < l; i++ ) {
                // Remove element nodes and prevent memory leaks
                if ( this[i].nodeType === 1 ) {
                    jQuery.cleanData( this[i].getElementsByTagName("*") );
                    this[i].innerHTML = value;
                }
            }

        // If using innerHTML throws an exception, use the fallback method
        } catch(e) {
            this.empty().append( value );
        }

    } else if ( jQuery.isFunction( value ) ) {
        this.each(function(i){
            var self = jQuery( this );

            self.html( value.call(this, i, self.html()) );
        });

    } else {
        this.empty().append( value );
    }

    return this;
},

Arguments nommés

D'autres langages (comme Python) permettent de passer des arguments nommés comme moyen de ne transmettre que quelques arguments et de rendre les arguments indépendants de l'ordre dans lequel ils sont passés. Javascript ne prend pas directement en charge la fonctionnalité des arguments nommés. Un modèle de conception couramment utilisé à sa place consiste à transmettre une carte de propriétés / valeurs. Cela peut être fait en passant un objet avec des propriétés et des valeurs ou dans ES6 et au-dessus, vous pouvez en fait passer un objet Map lui-même.

Voici un exemple simple ES5:

jQuery's $.ajax()accepte une forme d'utilisation où vous ne lui passez qu'un seul paramètre qui est un objet Javascript régulier avec des propriétés et des valeurs. Les propriétés que vous passez déterminent quels arguments / options sont passés à l'appel ajax. Certains peuvent être requis, beaucoup sont facultatifs. Puisqu'il s'agit de propriétés sur un objet, il n'y a pas d'ordre spécifique. En fait, il y a plus de 30 propriétés différentes qui peuvent être transmises à cet objet, une seule (l'url) est requise.

Voici un exemple:

$.ajax({url: "http://www.example.com/somepath", data: myArgs, dataType: "json"}).then(function(result) {
    // process result here
});

À l'intérieur de l' $.ajax()implémentation, il peut alors simplement interroger les propriétés qui ont été transmises à l'objet entrant et les utiliser comme arguments nommés. Cela peut être fait avec for (prop in obj)ou en obtenant toutes les propriétés dans un tableau avec Object.keys(obj)puis en itérant ce tableau.

Cette technique est très couramment utilisée en Javascript lorsqu'il y a un grand nombre d'arguments et / ou de nombreux arguments sont optionnels. Remarque: cela impose à la fonction d'implémentation de s'assurer qu'un ensemble minimal d'arguments valides est présent et de donner à l'appelant des commentaires de débogage sur ce qui manque si des arguments insuffisants sont passés (probablement en lançant une exception avec un message d'erreur utile) .

Dans un environnement ES6, il est possible d'utiliser la déstructuration pour créer des propriétés / valeurs par défaut pour l'objet passé ci-dessus. Ceci est discuté plus en détail dans cet article de référence .

Voici un exemple de cet article:

function selectEntries({ start=0, end=-1, step=1 } = {}) {
    ···
};

Cela crée des propriétés et des valeurs par défaut pour les propriétés start, endet stepsur un objet passé à la selectEntries()fonction.

Valeurs par défaut des arguments de fonction

Dans ES6, Javascript ajoute la prise en charge du langage intégré pour les valeurs par défaut des arguments.

Par exemple:

function multiply(a, b = 1) {
  return a*b;
}

multiply(5); // 5

Une description plus détaillée des façons dont cela peut être utilisé ici sur MDN .


Je devrais donc m'en tenir à une fonction adaptable et l'accepter comme meilleure pratique pour implémenter la façade de surcharge de fonction?
Travis J

2
@TravisJ - oui, la fonction adaptable est la façon dont les surcharges sont effectuées en Javascript. Votre autre option consiste à utiliser une fonction nommée séparément avec différents arguments. Si les deux implémentations n'ont rien en commun, alors l'une ou l'autre est une pratique acceptable. Si les deux implémentations font essentiellement la même chose, mais en commençant par des arguments différents, je pense que cela rend votre interface plus compacte pour utiliser la fonction adaptable et cela vous donne la possibilité de partager du code entre les deux implémentations associées.
jfriend00

Ajout de quelques exemples de code, expliqués en anglais et en code réel.
jfriend00

J'obtiens une erreur, un jeton inattendu '=' sur la dernière version de Safari lorsque j'essaye ceci resetProgressBar: function(display_errors, lockout = false).
Nick

@Nick - Selon ce tableau de compatibilité ES6 , les valeurs des paramètres par défaut devraient fonctionner dans Safari 10 et versions ultérieures. Selon cette page MDN , il est dit "pas de support" dans Safari. Je n'ai pas de Mac, je ne peux donc pas le tester moi-même. Il semble généralement être l'une des fonctionnalités ES6 implémentées plus tard dans de nombreux navigateurs. Par exemple, il ne semble pas encore être dans iOS9.
jfriend00

33

La surcharge d'une fonction en JavaScript peut être effectuée de plusieurs manières. Tous impliquent une seule fonction principale qui exécute tous les processus ou les délègue à des sous-fonctions / processus.

L'une des techniques simples les plus courantes implique un simple changement:

function foo(a, b) {
    switch (arguments.length) {
    case 0:
        //do basic code
        break;
    case 1:
        //do code with `a`
        break;
    case 2:
    default:
        //do code with `a` & `b`
        break;
    }
}

Une technique plus élégante serait d'utiliser un tableau (ou un objet si vous ne faites pas de surcharge pour chaque nombre d'arguments):

fooArr = [
    function () {
    },
    function (a) {
    },
    function (a,b) {
    }
];
function foo(a, b) {
    return fooArr[arguments.length](a, b);
}

Cet exemple précédent n'est pas très élégant, n'importe qui pourrait le modifier fooArr, et il échouerait si quelqu'un passait plus de 2 arguments à foo, donc une meilleure forme serait d'utiliser un modèle de module et quelques vérifications:

var foo = (function () {
    var fns;
    fns = [
        function () {
        },
        function (a) {
        },
        function (a, b) {
        }
    ];
    function foo(a, b) {
        var fnIndex;
        fnIndex = arguments.length;
        if (fnIndex > foo.length) {
            fnIndex = foo.length;
        }
        return fns[fnIndex].call(this, a, b);
    }
    return foo;
}());

Bien sûr, vos surcharges peuvent vouloir utiliser un nombre dynamique de paramètres, vous pouvez donc utiliser un objet pour la fnscollection.

var foo = (function () {
    var fns;
    fns = {};
    fns[0] = function () {
    };
    fns[1] = function (a) {
    };
    fns[2] = function (a, b) {
    };
    fns.params = function (a, b /*, params */) {
    };
    function foo(a, b) {
        var fnIndex;
        fnIndex = arguments.length;
        if (fnIndex > foo.length) {
            fnIndex = 'params';
        }
        return fns[fnIndex].apply(this, Array.prototype.slice.call(arguments));
    }
    return foo;
}());

Ma préférence personnelle a tendance à être le switch, bien que cela renforce la fonction principale. Un exemple courant où j'utiliserais cette technique serait une méthode accesseur / mutateur:

function Foo() {} //constructor
Foo.prototype = {
    bar: function (val) {
        switch (arguments.length) {
        case 0:
            return this._bar;
        case 1:
            this._bar = val;
            return this;
        }
    }
}

Ceci est un bel exemple d'une alternative :)
Travis J

1
Nice, j'aime cette 2ème technique
Nick Rolando

8

Vous ne pouvez pas effectuer de surcharge de méthode au sens strict. Pas comme la façon dont il est pris en charge dans javaou c#.

Le problème est que JavaScript ne prend PAS en charge nativement la surcharge de méthode. Donc, s'il voit / analyse deux fonctions ou plus avec les mêmes noms, il considérera simplement la dernière fonction définie et écrasera les précédentes.

Une des façons dont je pense qu'elle convient à la plupart des cas est la suivante -

Disons que vous avez une méthode

function foo(x)
{
} 

Au lieu de surcharger la méthode qui n'est pas possible en javascript, vous pouvez définir une nouvelle méthode

fooNew(x,y,z)
{
}

puis modifiez la 1ère fonction comme suit -

function foo(x)
{
  if(arguments.length==2)
  {
     return fooNew(arguments[0],  arguments[1]);
  }
} 

Si vous avez de nombreuses méthodes surchargées, envisagez de n'utiliser switchque des if-elseinstructions.

( plus de détails ) PS: Le lien ci-dessus va vers mon blog personnel qui contient des détails supplémentaires à ce sujet.


Ce n'est pas un problème, c'est une fonctionnalité.
inetphantom

4

J'utilise une approche de surcharge un peu différente basée sur le nombre d'arguments. Cependant, je crois que l'approche de John Fawcett est également bonne. Voici l'exemple, code basé sur les explications de John Resig (jQuery's Author).

// o = existing object, n = function name, f = function.
    function overload(o, n, f){
        var old = o[n];
        o[n] = function(){
            if(f.length == arguments.length){
                return f.apply(this, arguments);
            }
            else if(typeof o == 'function'){
                return old.apply(this, arguments);
            }
        };
    }

utilisabilité:

var obj = {};
overload(obj, 'function_name', function(){ /* what we will do if no args passed? */});
overload(obj, 'function_name', function(first){ /* what we will do if 1 arg passed? */});
overload(obj, 'function_name', function(first, second){ /* what we will do if 2 args passed? */});
overload(obj, 'function_name', function(first,second,third){ /* what we will do if 3 args passed? */});
//... etc :)

4

En javascript, vous pouvez implémenter la fonction une seule fois et appeler la fonction sans les paramètres.Vous myFunc() vérifiez ensuite si les options sont `` indéfinies ''

function myFunc(options){
 if(typeof options != 'undefined'){
  //code
 }
}

3

J'ai essayé de développer une solution élégante à ce problème décrit ici . Et vous pouvez trouver la démo ici . L'utilisation ressemble à ceci:

var out = def({
    'int': function(a) {
        alert('Here is int '+a);
    },

    'float': function(a) {
        alert('Here is float '+a);
    },

    'string': function(a) {
        alert('Here is string '+a);
    },

    'int,string': function(a, b) {
        alert('Here is an int '+a+' and a string '+b);
    },
    'default': function(obj) {
        alert('Here is some other value '+ obj);
    }

});

out('ten');
out(1);
out(2, 'robot');
out(2.5);
out(true);

Les méthodes utilisées pour y parvenir:

var def = function(functions, parent) {
 return function() {
    var types = [];
    var args = [];
    eachArg(arguments, function(i, elem) {
        args.push(elem);
        types.push(whatis(elem));
    });
    if(functions.hasOwnProperty(types.join())) {
        return functions[types.join()].apply(parent, args);
    } else {
        if (typeof functions === 'function')
            return functions.apply(parent, args);
        if (functions.hasOwnProperty('default'))
            return functions['default'].apply(parent, args);        
    }
  };
};

var eachArg = function(args, fn) {
 var i = 0;
 while (args.hasOwnProperty(i)) {
    if(fn !== undefined)
        fn(i, args[i]);
    i++;
 }
 return i-1;
};

var whatis = function(val) {

 if(val === undefined)
    return 'undefined';
 if(val === null)
    return 'null';

 var type = typeof val;

 if(type === 'object') {
    if(val.hasOwnProperty('length') && val.hasOwnProperty('push'))
        return 'array';
    if(val.hasOwnProperty('getDate') && val.hasOwnProperty('toLocaleTimeString'))
        return 'date';
    if(val.hasOwnProperty('toExponential'))
        type = 'number';
    if(val.hasOwnProperty('substring') && val.hasOwnProperty('length'))
        return 'string';
 }

 if(type === 'number') {
    if(val.toString().indexOf('.') > 0)
        return 'float';
    else
        return 'int';
 }

 return type;
};

1
Une fonction pour gérer les entrées, très astucieuse.
Travis J

3

https://github.com/jrf0110/leFunc

var getItems = leFunc({
  "string": function(id){
    // Do something
  },
  "string,object": function(id, options){
    // Do something else
  },
  "string,object,function": function(id, options, callback){
    // Do something different
    callback();
  },
  "object,string,function": function(options, message, callback){
    // Do something ca-raaaaazzzy
    callback();
  }
});

getItems("123abc"); // Calls the first function - "string"
getItems("123abc", {poop: true}); // Calls the second function - "string,object"
getItems("123abc", {butt: true}, function(){}); // Calls the third function - "string,object,function"
getItems({butt: true}, "What what?" function(){}); // Calls the fourth function - "object,string,function"

3

Pas de problème de surcharge dans JS, le pb comment maintenir un code propre lors de la surcharge de la fonction?

Vous pouvez utiliser un transfert pour avoir un code propre, basé sur deux choses:

  1. Nombre d'arguments (lors de l'appel de la fonction).
  2. Type d'arguments (lors de l'appel de la fonction)

      function myFunc(){
          return window['myFunc_'+arguments.length+Array.from(arguments).map((arg)=>typeof arg).join('_')](...arguments);
       }
    
        /** one argument & this argument is string */
      function myFunc_1_string(){
    
      }
       //------------
       /** one argument & this argument is object */
      function myFunc_1_object(){
    
      }
      //----------
      /** two arguments & those arguments are both string */
      function myFunc_2_string_string(){
    
      }
       //--------
      /** Three arguments & those arguments are : id(number),name(string), callback(function) */
      function myFunc_3_number_string_function(){
                let args=arguments;
                  new Person(args[0],args[1]).onReady(args[3]);
      }
    
       //--- And so on ....   


2

Étant donné que JavaScript n'a pas d' options de surcharge de fonction , l'objet peut être utilisé à la place. S'il y a un ou deux arguments requis, il est préférable de les séparer de l'objet options. Voici un exemple sur la façon d'utiliser l'objet options et les valeurs remplies à la valeur par défaut au cas où la valeur n'était pas passée dans l'objet options.

function optionsObjectTest(x, y, opts) {
    opts = opts || {}; // default to an empty options object

    var stringValue = opts.stringValue || "string default value";
    var boolValue = !!opts.boolValue; // coerces value to boolean with a double negation pattern
    var numericValue = opts.numericValue === undefined ? 123 : opts.numericValue;

    return "{x:" + x + ", y:" + y + ", stringValue:'" + stringValue + "', boolValue:" + boolValue + ", numericValue:" + numericValue + "}";

}

voici un exemple d'utilisation de l'objet d'options


2

Pour cela, vous devez créer une fonction qui ajoute la fonction à un objet, puis elle s'exécutera en fonction de la quantité d'arguments que vous envoyez à la fonction:

<script > 
//Main function to add the methods
function addMethod(object, name, fn) {
  var old = object[name];
  object[name] = function(){
    if (fn.length == arguments.length)
      return fn.apply(this, arguments)
    else if (typeof old == 'function')
      return old.apply(this, arguments);
  };
}


  var ninjas = {
   values: ["Dean Edwards", "Sam Stephenson", "Alex Russell"]
};

//Here we declare the first function with no arguments passed
  addMethod(ninjas, "find", function(){
    return this.values;
});

//Second function with one argument
  addMethod(ninjas, "find", function(name){
    var ret = [];
    for (var i = 0; i < this.values.length; i++)
      if (this.values[i].indexOf(name) == 0)
        ret.push(this.values[i]);
    return ret;
  });

//Third function with two arguments
  addMethod(ninjas, "find", function(first, last){
    var ret = [];
    for (var i = 0; i < this.values.length; i++)
      if (this.values[i] == (first + " " + last))
        ret.push(this.values[i]);
    return ret;
  });


//Now you can do:
ninjas.find();
ninjas.find("Sam");
ninjas.find("Dean", "Edwards")
</script>

0

J'aime ajouter des sous-fonctions dans une fonction parent pour obtenir la possibilité de différencier les groupes d'arguments pour la même fonctionnalité.

var doSomething = function() {
    var foo;
    var bar;
};

doSomething.withArgSet1 = function(arg0, arg1) {
    var obj = new doSomething();
    // do something the first way
    return obj;
};

doSomething.withArgSet2 = function(arg2, arg3) {
    var obj = new doSomething();
    // do something the second way
    return obj;
};

0

Ce que vous essayez de réaliser est mieux fait en utilisant la variable d' arguments locaux de la fonction .

function foo() {
    if (arguments.length === 0) {
        //do something
    }
    if (arguments.length === 1) {
        //do something else
    }
}

foo(); //do something
foo('one'); //do something else

Vous pouvez trouver une meilleure explication de comment cela fonctionne ici .


Et qu'en est-il de la détection de collision? C'est une bonne façon d'utiliser des fonctions sans paramètre ou de vérifier l'existence, mais ce n'est pas vraiment la meilleure pratique en matière de surcharge.
Travis J
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.