Come sovraccaricare le funzioni in javascript?


103

Approccio classico (non js) al sovraccarico:

function myFunc(){
 //code
}

function myFunc(overloaded){
 //other code
}

Javascript non permette di definire più di una funzione con lo stesso nome. Come tali, cose come questa si presentano:

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

Esiste una soluzione alternativa migliore per il sovraccarico di funzioni in javascript oltre al passaggio di un oggetto con i sovraccarichi in esso?

Il passaggio di overload può far sì che una funzione diventi rapidamente troppo dettagliata perché ogni possibile sovraccarico avrebbe quindi bisogno di un'istruzione condizionale. L'utilizzo di funzioni per realizzare l' //codeinterno di tali istruzioni condizionali può causare situazioni complicate con gli ambiti.


1
Allora stai pensando a javascript sbagliato; se hai problemi con i sovraccarichi e l'ambito, dovresti probabilmente avere un nome di metodo diverso poiché sembra che
stia

@joshcomley: i sovraccarichi e l'ambito richiedono la gestione del codice. Sto semplicemente cercando di assicurarmi che il codice che uso sia il più efficiente possibile. Gestire gli ambiti con soluzioni alternative va bene, ma preferisco almeno provare a utilizzare metodi di best practice.
Travis J

2
possibile duplicato del sovraccarico
cdeszaq

Risposte:


126

Ci sono molti aspetti del sovraccarico di argomenti in Javascript:

  1. Argomenti variabili : è possibile passare diversi set di argomenti (sia per tipo che per quantità) e la funzione si comporterà in un modo che corrisponda agli argomenti passati.

  2. Argomenti predefiniti : è possibile definire un valore predefinito per un argomento se non viene passato.

  3. Argomenti con nome : l'ordine degli argomenti diventa irrilevante e devi solo nominare gli argomenti che vuoi passare alla funzione.

Di seguito è riportata una sezione su ciascuna di queste categorie di gestione degli argomenti.

Argomenti variabili

Poiché javascript non ha il controllo del tipo sugli argomenti o la quantità richiesta di argomenti, puoi avere solo un'implementazione di myFunc()che può adattarsi a quali argomenti sono stati passati controllando il tipo, la presenza o la quantità di argomenti.

jQuery lo fa tutto il tempo. Puoi rendere alcuni degli argomenti opzionali o puoi ramificare la tua funzione a seconda di quali argomenti vengono passati.

Nell'implementazione di questi tipi di sovraccarichi, sono disponibili diverse tecniche che è possibile utilizzare:

  1. È possibile verificare la presenza di un determinato argomento controllando se il valore del nome dell'argomento dichiarato è undefined.
  2. Puoi controllare la quantità totale o gli argomenti con arguments.length.
  3. Puoi controllare il tipo di un dato argomento.
  4. Per numeri variabili di argomenti, è possibile utilizzare lo argumentspseudo-array per accedere a qualsiasi argomento dato con arguments[i].

Ecco alcuni esempi:

Diamo un'occhiata al obj.data()metodo di jQuery . Supporta quattro diverse forme di utilizzo:

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

Ognuno innesca un comportamento diverso e, senza utilizzare questa forma dinamica di sovraccarico, richiederebbe quattro funzioni separate.

Ecco come si possono discernere tutte queste opzioni in inglese e poi le combinerò tutte in codice:

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

Se il primo argomento passato a .data()è una stringa e il secondo argomento lo è undefined, il chiamante deve utilizzare questo modulo.


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

Se il secondo argomento non è indefinito, imposta il valore di una particolare chiave.


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

Se non vengono passati argomenti, restituisce tutte le chiavi / valori in un oggetto restituito.


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

Se il tipo del primo argomento è un oggetto semplice, imposta tutte le chiavi / valori da quell'oggetto.


Ecco come puoi combinare tutti questi in un set di logica 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 chiave di questa tecnica è assicurarsi che tutte le forme di argomenti che si desidera accettare siano identificabili in modo univoco e che non vi sia mai alcuna confusione su quale forma sta utilizzando il chiamante. Ciò richiede generalmente di ordinare gli argomenti in modo appropriato e assicurarsi che ci sia abbastanza unicità nel tipo e nella posizione degli argomenti da poter sempre dire quale forma viene utilizzata.

Ad esempio, se hai una funzione che accetta tre argomenti stringa:

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

Puoi facilmente rendere facoltativo il terzo argomento e puoi facilmente rilevare quella condizione, ma non puoi rendere facoltativo solo il secondo argomento perché non puoi dire quale di questi il ​​chiamante intende passare perché non c'è modo di identificare se il secondo argomento è pensato per essere il secondo argomento o il secondo argomento è stato omesso, quindi ciò che è al posto del secondo argomento è in realtà il terzo argomento:

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

Poiché tutti e tre gli argomenti sono dello stesso tipo, non puoi distinguere tra argomenti diversi, quindi non sai cosa intendeva il chiamante. Con questo stile di chiamata, solo il terzo argomento può essere facoltativo. Se volessi omettere il secondo argomento, dovrebbe essere passato come null(o qualche altro valore rilevabile) e il tuo codice rileverà che:

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

Ecco un esempio jQuery di argomenti opzionali. entrambi gli argomenti sono opzionali e assumono valori predefiniti se non vengono passati:

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

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

Ecco un esempio di jQuery in cui l'argomento può mancare o uno qualsiasi dei tre diversi tipi che ti dà quattro diversi overload:

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;
},

Argomenti denominati

Altri linguaggi (come Python) consentono di passare argomenti con nome come mezzo per passare solo alcuni argomenti e rendere gli argomenti indipendenti dall'ordine in cui sono passati. Javascript non supporta direttamente la funzionalità degli argomenti con nome. Un modello di progettazione che viene comunemente utilizzato al suo posto è passare una mappa di proprietà / valori. Questo può essere fatto passando un oggetto con proprietà e valori o in ES6 e versioni successive, potresti effettivamente passare un oggetto Map stesso.

Ecco un semplice esempio ES5:

jQuery $.ajax()accetta una forma di utilizzo in cui basta passare un singolo parametro che è un normale oggetto Javascript con proprietà e valori. Le proprietà passate determinano quali argomenti / opzioni vengono passati alla chiamata ajax. Alcuni possono essere richiesti, molti sono opzionali. Poiché sono proprietà su un oggetto, non esiste un ordine specifico. In effetti, ci sono più di 30 proprietà diverse che possono essere trasmesse a quell'oggetto, solo una (l'url) è richiesta.

Ecco un esempio:

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

All'interno $.ajax()dell'implementazione, può quindi semplicemente interrogare quali proprietà sono state passate sull'oggetto in arrivo e usarle come argomenti con nome. Questo può essere fatto con for (prop in obj)o inserendo tutte le proprietà in un array con Object.keys(obj)e quindi iterando quell'array.

Questa tecnica è usata molto comunemente in Javascript quando ci sono un gran numero di argomenti e / o molti argomenti sono opzionali. Nota: questo impone alla funzione di implementazione di assicurarsi che sia presente un insieme minimo di argomenti validi e di fornire al chiamante un feedback di debug su cosa manca se vengono passati argomenti insufficienti (probabilmente lanciando un'eccezione con un utile messaggio di errore) .

In un ambiente ES6, è possibile utilizzare la destrutturazione per creare proprietà / valori predefiniti per l'oggetto passato sopra. Questo argomento viene discusso in maggiore dettaglio in questo articolo di riferimento .

Ecco un esempio tratto da quell'articolo:

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

Questo crea le proprietà ei valori predefiniti per il start, ende steple proprietà di un oggetto passato alla selectEntries()funzione di.

Valori predefiniti per gli argomenti della funzione

In ES6, Javascript aggiunge il supporto linguistico integrato per i valori predefiniti per gli argomenti.

Per esempio:

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

multiply(5); // 5

Ulteriore descrizione dei modi in cui questo può essere utilizzato qui su MDN .


Quindi dovrei limitarmi a una funzione adattabile e accettarla come best practice per l'implementazione della facciata di sovraccarico della funzione?
Travis J

2
@TravisJ - sì, la funzione adattabile è il modo in cui vengono eseguiti i sovraccarichi in Javascript. L'altra opzione è quella di utilizzare una funzione denominata separatamente con argomenti diversi. Se le due implementazioni non hanno nulla in comune, entrambe sono una pratica accettabile. Se le due implementazioni stanno essenzialmente facendo la stessa cosa, ma solo iniziando con argomenti diversi, allora penso che renda la tua interfaccia più compatta per usare la funzione adattabile e ti dia l'opportunità di condividere il codice tra le due implementazioni correlate.
jfriend00

Aggiunti alcuni esempi di codice, spiegati sia in inglese che nel codice effettivo.
jfriend00

Ricevo un errore, un token imprevisto "=" nell'ultima versione di Safari quando provo questo resetProgressBar: function(display_errors, lockout = false).
Nick

@ Nick: in base a questa tabella di compatibilità ES6 , i valori dei parametri predefiniti dovrebbero funzionare in Safari 10 e versioni successive. Per questa pagina MDN , in Safari è indicato "nessun supporto". Non ho un Mac quindi non posso provarlo da solo. Sembra essere generalmente una delle funzionalità ES6 implementate successivamente su molti browser. Ad esempio, non sembra ancora essere in iOS9.
jfriend00

33

Il sovraccarico di una funzione in JavaScript può essere eseguito in molti modi. Tutti coinvolgono una singola funzione master che esegue tutti i processi o delega a sotto-funzioni / processi.

Una delle tecniche semplici più comuni prevede un semplice interruttore:

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

Una tecnica più elegante sarebbe quella di utilizzare un array (o un oggetto se non stai facendo sovraccarichi per ogni argomento):

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

L'esempio precedente non è molto elegante, chiunque potrebbe modificare fooArre fallirebbe se qualcuno passasse più di 2 argomenti a foo, quindi una forma migliore sarebbe quella di utilizzare un modello di modulo e alcuni controlli:

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

Ovviamente i tuoi overload potrebbero voler utilizzare un numero dinamico di parametri, quindi potresti usare un oggetto per la fnsraccolta.

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

La mia preferenza personale tende ad essere il switch, sebbene rafforzi la funzione principale. Un esempio comune di dove userei questa tecnica sarebbe un metodo accessorio / mutatore:

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

Questo è un bell'esempio di alternativa :)
Travis J

1
Bello, mi piace quella seconda tecnica
Nick Rolando

8

Non puoi eseguire il sovraccarico del metodo in senso stretto. Non come il modo in cui è supportato in javao c#.

Il problema è che JavaScript NON supporta nativamente il sovraccarico del metodo. Quindi, se vede / analizza due o più funzioni con lo stesso nome, considererà solo l'ultima funzione definita e sovrascriverà le precedenti.

Uno dei modi in cui penso sia adatto per la maggior parte dei casi è il seguente:

Diciamo che hai metodo

function foo(x)
{
} 

Invece di sovraccaricare il metodo che non è possibile in javascript , puoi definire un nuovo metodo

fooNew(x,y,z)
{
}

e quindi modificare la prima funzione come segue:

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

Se si dispone di molti di questi metodi sovraccaricati, considerare l'utilizzo switchdi semplici if-elseistruzioni.

( maggiori dettagli ) PS: il link sopra va al mio blog personale che ha ulteriori dettagli su questo.


Non è un problema, è una caratteristica.
inetphantom

4

Sto usando un approccio di sovraccarico leggermente diverso in base al numero di argomenti. Tuttavia credo che anche l'approccio di John Fawcett sia buono. Ecco l'esempio, codice basato sulle spiegazioni di 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);
            }
        };
    }

usabilità:

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

In javascript puoi implementare la funzione solo una volta e invocare la funzione senza i parametri myFunc() Quindi controlla per vedere se le opzioni sono 'indefinite'

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

3

Ho provato a sviluppare un'elegante soluzione a questo problema descritto qui . E puoi trovare la demo qui . L'utilizzo è simile a questo:

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

I metodi utilizzati per ottenere questo risultato:

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
Una funzione per gestire gli input, molto intelligente.
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

Nessun problema con il sovraccarico in JS, il pb come mantenere un codice pulito durante il sovraccarico della funzione?

Puoi usare un forward per avere un codice pulito, basato su due cose:

  1. Numero di argomenti (quando si chiama la funzione).
  2. Tipo di argomenti (quando si chiama la funzione)

      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

Poiché JavaScript non dispone di opzioni di sovraccarico della funzione , è possibile utilizzare l' oggetto . Se ci sono uno o due argomenti obbligatori, è meglio tenerli separati dall'oggetto opzioni. Di seguito è riportato un esempio su come utilizzare l'oggetto opzioni e i valori popolati sul valore predefinito nel caso in cui il valore non sia stato passato nell'oggetto opzioni.

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 + "}";

}

ecco un esempio su come utilizzare l'oggetto opzioni


2

Per questo è necessario creare una funzione che aggiunge la funzione a un oggetto, quindi verrà eseguita a seconda della quantità di argomenti che invii alla funzione:

<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

Mi piace aggiungere funzioni secondarie all'interno di una funzione genitore per ottenere la capacità di distinguere tra gruppi di argomenti per la stessa funzionalità.

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

Quello che stai cercando di ottenere è meglio farlo usando la variabile degli argomenti locali della funzione .

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

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

Puoi trovare una spiegazione migliore di come funziona qui .


E per quanto riguarda il rilevamento delle collisioni? Questo è un bel modo per usare funzioni senza parametri o per verificarne l'esistenza, ma non è proprio la migliore pratica quando si tratta di sovraccarico.
Travis J
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.