¿Cómo sobrecargar funciones en javascript?

103

Enfoque clásico (no js) de sobrecarga:

function myFunc(){
 //code
}

function myFunc(overloaded){
 //other code
}

Javascript no permitirá que se defina más de una función con el mismo nombre. Como tal, aparecen cosas como esta:

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

¿Existe una mejor solución para la sobrecarga de funciones en javascript que no sea pasar un objeto con las sobrecargas en él?

Pasar sobrecargas puede hacer que una función se vuelva demasiado detallada porque cada posible sobrecarga necesitaría una declaración condicional. El uso de funciones para lograr el //codeinterior de esas declaraciones condicionales puede causar situaciones complicadas con ámbitos.

Travis J
fuente
1
Entonces estás pensando en JavaScript incorrecto; si tiene problemas con las sobrecargas y el alcance, probablemente debería tener un nombre de método diferente, ya que parece que está haciendo otro trabajo
joshcomley
@joshcomley: las sobrecargas y el alcance requieren código para su manejo. Simplemente estoy tratando de asegurarme de que el código que utilizo sea lo más eficiente posible. El manejo de ámbitos con soluciones alternativas está bien, pero prefiero al menos intentar utilizar los métodos de mejores prácticas.
Travis J
2
posible duplicado de sobrecarga
cdeszaq

Respuestas:

126

Hay varios aspectos de la sobrecarga de argumentos en Javascript:

  1. Argumentos variables : puede pasar diferentes conjuntos de argumentos (tanto en tipo como en cantidad) y la función se comportará de una manera que coincida con los argumentos que se le pasen.

  2. Argumentos predeterminados : puede definir un valor predeterminado para un argumento si no se pasa.

  3. Argumentos con nombre : el orden de los argumentos se vuelve irrelevante y usted simplemente nombra los argumentos que desea pasar a la función.

A continuación se muestra una sección sobre cada una de estas categorías de manejo de argumentos.

Argumentos variables

Debido a que javascript no tiene verificación de tipo en los argumentos o la cantidad requerida de argumentos, solo puede tener una implementación myFunc()que se adapte a los argumentos que se le pasaron al verificar el tipo, la presencia o la cantidad de argumentos.

jQuery hace esto todo el tiempo. Puede hacer que algunos de los argumentos sean opcionales o puede ramificar su función dependiendo de qué argumentos se le pasen.

Al implementar estos tipos de sobrecargas, tiene varias técnicas diferentes que puede utilizar:

  1. Puede comprobar la presencia de cualquier argumento determinado comprobando si el valor del nombre del argumento declarado es undefined.
  2. Puede verificar la cantidad total o los argumentos con arguments.length.
  3. Puede verificar el tipo de cualquier argumento dado.
  4. Para números variables de argumentos, puede usar la argumentspseudoarreglo para acceder a cualquier argumento dado con arguments[i].

Aquí hay unos ejemplos:

Veamos el obj.data()método de jQuery . Admite cuatro formas diferentes de uso:

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

Cada uno desencadena un comportamiento diferente y, sin usar esta forma dinámica de sobrecarga, requeriría cuatro funciones separadas.

Así es como uno puede discernir entre todas estas opciones en inglés y luego las combinaré todas en código:

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

Si el primer argumento que se pasa a .data()es una cadena y el segundo argumento es undefined, entonces la persona que llama debe utilizar este formulario.


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

Si el segundo argumento no está indefinido, establezca el valor de una clave en particular.


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

Si no se pasan argumentos, devuelva todas las claves / valores en un objeto devuelto.


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

Si el tipo del primer argumento es un objeto simple, establezca todas las claves / valores de ese objeto.


Así es como puede combinar todos ellos en un conjunto de lógica de 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 clave de esta técnica es asegurarse de que todas las formas de argumentos que desea aceptar sean identificables de forma única y que nunca haya confusión sobre la forma que utiliza la persona que llama. Esto generalmente requiere ordenar los argumentos de manera adecuada y asegurarse de que haya suficiente singularidad en el tipo y la posición de los argumentos para que siempre pueda saber qué forma se está utilizando.

Por ejemplo, si tiene una función que toma tres argumentos de cadena:

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

Puede hacer que el tercer argumento sea opcional y puede detectar fácilmente esa condición, pero no puede hacer que solo el segundo argumento sea opcional porque no puede decir cuál de estos quiere pasar la persona que llama porque no hay forma de identificar si el segundo El argumento está destinado a ser el segundo argumento o el segundo argumento se omitió, por lo que lo que está en el lugar del segundo argumento es en realidad el tercer argumento:

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

Dado que los tres argumentos son del mismo tipo, no puede distinguir la diferencia entre los diferentes argumentos, por lo que no sabe lo que pretendía la persona que llama. Con este estilo de llamada, solo el tercer argumento puede ser opcional. Si quisiera omitir el segundo argumento, tendría que pasarlo como null(o algún otro valor detectable) en su lugar y su código detectaría eso:

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

Aquí hay un ejemplo de jQuery de argumentos opcionales. Ambos argumentos son opcionales y toman valores predeterminados si no se pasan:

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

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

Aquí hay un ejemplo de jQuery en el que puede faltar el argumento o cualquiera de los tres tipos diferentes, lo que le brinda cuatro sobrecargas diferentes:

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

Argumentos nombrados

Otros lenguajes (como Python) permiten pasar argumentos con nombre como un medio para pasar solo algunos argumentos y hacer que los argumentos sean independientes del orden en que se pasan. Javascript no admite directamente la función de argumentos con nombre. Un patrón de diseño que se usa comúnmente en su lugar es pasar un mapa de propiedades / valores. Esto se puede hacer pasando un objeto con propiedades y valores o en ES6 y superior, podría pasar un objeto Map en sí.

Aquí hay un ejemplo simple de ES5:

jQuery's $.ajax()acepta una forma de uso en la que simplemente le pasa un único parámetro que es un objeto Javascript normal con propiedades y valores. Las propiedades que le pase determinan qué argumentos / opciones se pasan a la llamada ajax. Algunos pueden ser necesarios, muchos son opcionales. Dado que son propiedades de un objeto, no existe un orden específico. De hecho, hay más de 30 propiedades diferentes que se pueden pasar a ese objeto, solo se requiere una (la URL).

He aquí un ejemplo:

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

Dentro de la $.ajax()implementación, puede simplemente interrogar qué propiedades se pasaron en el objeto entrante y usarlas como argumentos con nombre. Esto se puede hacer con for (prop in obj)o incorporando todas las propiedades en una matriz Object.keys(obj)y luego iterando esa matriz.

Esta técnica se usa muy comúnmente en Javascript cuando hay una gran cantidad de argumentos y / o muchos argumentos son opcionales. Nota: esto impone la responsabilidad de la función de implementación para asegurarse de que haya un conjunto mínimo de argumentos válidos y para darle a la persona que llama algunos comentarios de depuración sobre lo que falta si se pasan argumentos insuficientes (probablemente lanzando una excepción con un mensaje de error útil) .

En un entorno ES6, es posible utilizar la desestructuración para crear propiedades / valores predeterminados para el objeto pasado anteriormente. Esto se discute con más detalle en este artículo de referencia .

Aquí hay un ejemplo de ese artículo:

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

Esto crea propiedades y valores predeterminados para las propiedades start, endy stepen un objeto pasado a la selectEntries()función.

Valores predeterminados para argumentos de función

En ES6, Javascript agrega soporte de lenguaje integrado para valores predeterminados para argumentos.

Por ejemplo:

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

multiply(5); // 5

Descripción adicional de las formas en que esto se puede utilizar aquí en MDN .

jfriend00
fuente
Entonces, ¿debería seguir con una función adaptable y aceptarla como la mejor práctica para implementar la fachada de sobrecarga de funciones?
Travis J
2
@TravisJ: sí, la función adaptable es la forma en que se realizan las sobrecargas en Javascript. Su otra opción es usar una función nombrada por separado con diferentes argumentos. Si las dos implementaciones no tienen nada en común, entonces cualquiera de las dos es una práctica aceptable. Si las dos implementaciones hacen esencialmente lo mismo, pero solo comienzan con argumentos diferentes, entonces creo que hace que su interfaz sea más compacta para usar la función adaptable y le da la oportunidad de compartir código entre las dos implementaciones relacionadas.
jfriend00
Se agregaron algunos ejemplos de código, explicados tanto en inglés como en código real.
jfriend00
Recibo un error, token inesperado '=' en la última versión de Safari cuando intento esto resetProgressBar: function(display_errors, lockout = false).
Nick
@Nick: según esta tabla de compatibilidad de ES6 , los valores de los parámetros predeterminados deberían funcionar en Safari 10 y versiones posteriores. Según esta página de MDN , dice "sin soporte" en Safari. No tengo una Mac, así que no puedo probarla yo mismo. En general, parece ser una de las funciones de ES6 implementadas más tarde en muchos navegadores. Por ejemplo, todavía no parece que esté en iOS9.
jfriend00
33

La sobrecarga de una función en JavaScript se puede realizar de muchas formas. Todos ellos involucran una única función maestra que realiza todos los procesos o delega en subfunciones / procesos.

Una de las técnicas simples más comunes implica un simple cambio:

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 técnica más elegante sería usar una matriz (u objeto si no está haciendo sobrecargas para cada argumento):

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

Ese ejemplo anterior no es muy elegante, cualquiera podría modificarlo fooArry fallaría si alguien pasa más de 2 argumentos a foo, por lo que una mejor forma sería usar un patrón de módulo y algunas verificaciones:

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

Por supuesto, sus sobrecargas pueden querer usar un número dinámico de parámetros, por lo que podría usar un objeto para la fnscolección.

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

Mi preferencia personal tiende a ser el switch, aunque aumenta la función maestra. Un ejemplo común de dónde usaría esta técnica sería un método de acceso / mutador:

function Foo() {} //constructor
Foo.prototype = {
    bar: function (val) {
        switch (arguments.length) {
        case 0:
            return this._bar;
        case 1:
            this._bar = val;
            return this;
        }
    }
}
zzzzBov
fuente
Este es un buen ejemplo de una alternativa :)
Travis J
1
Bien, me gusta esa segunda técnica
Nick Rolando
8

No puede realizar una sobrecarga de métodos en sentido estricto. No como la forma en que se admite en javao c#.

El problema es que JavaScript NO admite de forma nativa la sobrecarga de métodos. Entonces, si ve / analiza dos o más funciones con los mismos nombres, solo considerará la última función definida y sobrescribirá las anteriores.

Una de las formas en que creo que es adecuada para la mayoría de los casos es la siguiente:

Digamos que tienes un método

function foo(x)
{
} 

En lugar de sobrecargar el método que no es posible en javascript , puede definir un nuevo método

fooNew(x,y,z)
{
}

y luego modifique la primera función de la siguiente manera:

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

Si tiene muchos de estos métodos sobrecargados, considere usar switchsolo if-elsedeclaraciones.

( más detalles ) PD: el enlace anterior va a mi blog personal que tiene detalles adicionales sobre esto.

Aniket Thakur
fuente
Eso no es un problema, es una característica.
inetphantom
4

Estoy usando un enfoque de sobrecarga un poco diferente según el número de argumentos. Sin embargo, creo que el enfoque de John Fawcett también es bueno. Aquí el ejemplo, código basado en las explicaciones de John Resig (Autor de jQuery).

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

usabilidad:

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 :)
Valentin Rusk
fuente
4

En javascript, puede implementar la función solo una vez e invocar la función sin los parámetros myFunc() . Luego, verifique si las opciones están 'indefinidas'

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

Intenté desarrollar una solución elegante a este problema que se describe aquí . Y puedes encontrar la demostración aquí . El uso se ve así:

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

Los métodos utilizados para lograr esto:

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;
};
stamat
fuente
1
Una función para manejar las entradas, muy inteligente.
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"
John Fawcett
fuente
3

No hay problema con la sobrecarga en JS, el pb ¿cómo mantener un código limpio cuando se sobrecarga la función?

Puede usar un reenvío para tener un código limpio, basado en dos cosas:

  1. Número de argumentos (al llamar a la función).
  2. Tipo de argumentos (al llamar a la función)

      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 ....   
Abdennour TOUMI
fuente
2

Dado que JavaScript no tiene opciones de sobrecarga de funciones , se puede usar el objeto en su lugar. Si hay uno o dos argumentos necesarios, es mejor mantenerlos separados del objeto de opciones. A continuación, se muestra un ejemplo sobre cómo utilizar el objeto de opciones y los valores rellenados al valor predeterminado en caso de que el valor no se haya pasado en el objeto de opciones.

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

}

aquí hay un ejemplo sobre cómo usar el objeto de opciones

Vlad Bezden
fuente
2

Para esto, necesita crear una función que agregue la función a un objeto, luego se ejecutará dependiendo de la cantidad de argumentos que envíe a la función:

<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>
Max Cabrera
fuente
0

Me gusta agregar subfunciones dentro de una función principal para lograr la capacidad de diferenciar entre grupos de argumentos para la misma funcionalidad.

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;
};
km6zla
fuente
0

Lo que está tratando de lograr se hace mejor utilizando la variable de argumentos locales de la función .

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

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

Puede encontrar una mejor explicación de cómo funciona esto aquí .

jbarnett
fuente
¿Y la detección de colisiones? Esta es una buena forma de usar funciones sin parámetros o de verificar su existencia, pero no es la mejor práctica cuando se trata de sobrecarga.
Travis J