Manejo de parámetros opcionales en javascript

127

Tengo una función de JavaScript estático que puede tomar 1, 2 o 3 parámetros:

function getData(id, parameters, callback) //parameters (associative array) and callback (function) are optional

Sé que siempre puedo probar si un parámetro dado no está definido, pero ¿cómo podría saber si lo que se pasó fue el parámetro o la devolución de llamada?

¿Cuál es la mejor manera de hacer esto?


Ejemplos de lo que se podría pasar:

1:

getData('offers');

2:

var array = new Array();
array['type']='lalal';
getData('offers',array);

3:

var foo = function (){...}
getData('offers',foo);

4:

getData('offers',array,foo);
jd.
fuente
2
¿Puedes mostrar un ejemplo de lo que podría pasarse?
James Black el

Respuestas:

163

Puede saber cuántos argumentos se pasaron a su función y puede verificar si su segundo argumento es una función o no:

function getData (id, parameters, callback) {
  if (arguments.length == 2) { // if only two arguments were supplied
    if (Object.prototype.toString.call(parameters) == "[object Function]") {
      callback = parameters; 
    }
  }
  //...
}

También puede usar el objeto de argumentos de esta manera:

function getData (/*id, parameters, callback*/) {
  var id = arguments[0], parameters, callback;

  if (arguments.length == 2) { // only two arguments supplied
    if (Object.prototype.toString.call(arguments[1]) == "[object Function]") {
      callback = arguments[1]; // if is a function, set as 'callback'
    } else {
      parameters = arguments[1]; // if not a function, set as 'parameters'
    }
  } else if (arguments.length == 3) { // three arguments supplied
      parameters = arguments[1];
      callback = arguments[2];
  }
  //...
}

Si está interesado, eche un vistazo a este artículo de John Resig, sobre una técnica para simular la sobrecarga de métodos en JavaScript.

CMS
fuente
¿Por qué usar Object.prototype.toString.call (parámetros) == "[función de objeto]" y no typeof (parámetros) === 'función'? ¿Hay alguna diferencia importante entre ellos? PD: el artículo que mencionas parece usar este último
Tomer Cagan
@TomerCagan Creo que es una cuestión de preferencia (ish). Hay algunas buenas respuestas / comentarios sobre el tema de esta pregunta.
Philiiiiiipp
75

Er, eso implicaría que estás invocando tu función con argumentos que no están en el orden correcto ... que no recomendaría.

En su lugar, recomendaría alimentar un objeto a su función de la siguiente manera:

function getData( props ) {
    props = props || {};
    props.params = props.params || {};
    props.id = props.id || 1;
    props.callback = props.callback || function(){};
    alert( props.callback )
};

getData( {
    id: 3,
    callback: function(){ alert('hi'); }
} );

Beneficios:

  • no tiene que dar cuenta del orden de los argumentos
  • no tienes que hacer una verificación de tipo
  • es más fácil definir valores predeterminados porque no es necesaria la verificación de tipos
  • Menos dolores de cabeza. imagine que si agrega un cuarto argumento, tendría que actualizar su verificación de tipo cada vez, y ¿qué pasa si el cuarto o consecutivo también son funciones?

Inconvenientes:

  • hora de refactorizar el código

Si no tiene otra opción, podría usar una función para detectar si un objeto es realmente una función (vea el último ejemplo).

Nota: Esta es la forma correcta de detectar una función:

function isFunction(obj) {
    return Object.prototype.toString.call(obj) === "[object Function]";
}

isFunction( function(){} )
meder omuraliev
fuente
"Esto funcionaría el 99% del tiempo debido a algunos errores de ES". ¿Puedes explicarme mas? ¿Por qué podría salir mal?
jd.
Agregué el código correcto para detectar una función. Creo que el error está aquí: bugs.ecmascript.org/ticket/251
meder omuraliev
Recomiendo encarecidamente que alimente solo un objeto. Si no, use el método de CMS.
meder omuraliev
Oh ... maldición ... Acabo de publicar la misma idea.
Arnis Lapsa
1
Otro posible inconveniente es la falta de inteligencia. No considero que esto sea un gran problema, pero debería tenerse en cuenta.
Edyn
2

Debe verificar el tipo de parámetros recibidos. Tal vez debería usar la argumentsmatriz, ya que el segundo parámetro a veces puede ser 'parámetros' y a veces 'devolución de llamada' y nombrarlos parámetros puede ser engañoso.

Kamil Szot
fuente
2

Sé que esta es una pregunta bastante antigua, pero recientemente me ocupé de esto. Déjame saber lo que piensas de esta solución.

Creé una utilidad que me permite escribir argumentos y dejarlos ser opcionales. Básicamente, envuelve su función en un proxy. Si omite una discusión, no está definida . Puede ser peculiar si tiene múltiples argumentos opcionales con el mismo tipo uno al lado del otro. (Hay opciones para pasar funciones en lugar de tipos para hacer verificaciones de argumentos personalizados, así como especificar valores predeterminados para cada parámetro).

Así es como se ve la implementación:

function displayOverlay(/*message, timeout, callback*/) {
  return arrangeArgs(arguments, String, Number, Function, 
    function(message, timeout, callback) {
      /* ... your code ... */
    });
};

Para mayor claridad, esto es lo que está sucediendo:

function displayOverlay(/*message, timeout, callback*/) {
  //arrangeArgs is the proxy
  return arrangeArgs(
           //first pass in the original arguments
           arguments, 
           //then pass in the type for each argument
           String,  Number,  Function, 
           //lastly, pass in your function and the proxy will do the rest!
           function(message, timeout, callback) {

             //debug output of each argument to verify it's working
             console.log("message", message, "timeout", timeout, "callback", callback);

             /* ... your code ... */

           }
         );
};

Puede ver el código proxy de arranArgs en mi repositorio de GitHub aquí:

https://github.com/joelvh/Sysmo.js/blob/master/sysmo.js

Aquí está la función de utilidad con algunos comentarios copiados del repositorio:

/*
 ****** Overview ******
 * 
 * Strongly type a function's arguments to allow for any arguments to be optional.
 * 
 * Other resources:
 * http://ejohn.org/blog/javascript-method-overloading/
 * 
 ****** Example implementation ******
 * 
 * //all args are optional... will display overlay with default settings
 * var displayOverlay = function() {
 *   return Sysmo.optionalArgs(arguments, 
 *            String, [Number, false, 0], Function, 
 *            function(message, timeout, callback) {
 *              var overlay = new Overlay(message);
 *              overlay.timeout = timeout;
 *              overlay.display({onDisplayed: callback});
 *            });
 * }
 * 
 ****** Example function call ******
 * 
 * //the window.alert() function is the callback, message and timeout are not defined.
 * displayOverlay(alert);
 * 
 * //displays the overlay after 500 miliseconds, then alerts... message is not defined.
 * displayOverlay(500, alert);
 * 
 ****** Setup ******
 * 
 * arguments = the original arguments to the function defined in your javascript API.
 * config = describe the argument type
 *  - Class - specify the type (e.g. String, Number, Function, Array) 
 *  - [Class/function, boolean, default] - pass an array where the first value is a class or a function...
 *                                         The "boolean" indicates if the first value should be treated as a function.
 *                                         The "default" is an optional default value to use instead of undefined.
 * 
 */
arrangeArgs: function (/* arguments, config1 [, config2] , callback */) {
  //config format: [String, false, ''], [Number, false, 0], [Function, false, function(){}]
  //config doesn't need a default value.
  //config can also be classes instead of an array if not required and no default value.

  var configs = Sysmo.makeArray(arguments),
      values = Sysmo.makeArray(configs.shift()),
      callback = configs.pop(),
      args = [],
      done = function() {
        //add the proper number of arguments before adding remaining values
        if (!args.length) {
          args = Array(configs.length);
        }
        //fire callback with args and remaining values concatenated
        return callback.apply(null, args.concat(values));
      };

  //if there are not values to process, just fire callback
  if (!values.length) {
    return done();
  }

  //loop through configs to create more easily readable objects
  for (var i = 0; i < configs.length; i++) {

    var config = configs[i];

    //make sure there's a value
    if (values.length) {

      //type or validator function
      var fn = config[0] || config,
          //if config[1] is true, use fn as validator, 
          //otherwise create a validator from a closure to preserve fn for later use
          validate = (config[1]) ? fn : function(value) {
            return value.constructor === fn;
          };

      //see if arg value matches config
      if (validate(values[0])) {
        args.push(values.shift());
        continue;
      }
    }

    //add a default value if there is no value in the original args
    //or if the type didn't match
    args.push(config[2]);
  }

  return done();
}
joelvh
fuente
2

Te recomiendo que uses ArgueJS .

Simplemente puede escribir su función de esta manera:

function getData(){
  arguments = __({id: String, parameters: [Object], callback: [Function]})

  // and now access your arguments by arguments.id,
  //          arguments.parameters and arguments.callback
}

Por sus ejemplos, consideré que desea que su idparámetro sea una cadena, ¿verdad? Ahora, getDatarequiere un String idy está aceptando los opcionales Object parametersy Function callback. Todos los casos de uso que haya publicado funcionarán según lo esperado.

zVictor
fuente
1

¿Está diciendo que puede tener llamadas como estas: getData (id, parámetros); getData (id, devolución de llamada)?

En este caso, obviamente no puede confiar en la posición y debe confiar en analizar el tipo: getType () y luego, si es necesario, getTypeName ()

Compruebe si el parámetro en cuestión es una matriz o una función.

DmitryK
fuente
0

Creo que quieres usar typeof () aquí:

function f(id, parameters, callback) {
  console.log(typeof(parameters)+" "+typeof(callback));
}

f("hi", {"a":"boo"}, f); //prints "object function"
f("hi", f, {"a":"boo"}); //prints "function object"
Mark Bessey
fuente
0

Si su problema es solo con la sobrecarga de funciones (debe verificar si el parámetro 'parámetros' es 'parámetros' y no 'devolución de llamada'), le recomendaría que no se preocupe por el tipo de argumento y
utilice este enfoque . La idea es simple: use objetos literales para combinar sus parámetros:

function getData(id, opt){
    var data = voodooMagic(id, opt.parameters);
    if (opt.callback!=undefined)
      opt.callback.call(data);
    return data;         
}

getData(5, {parameters: "1,2,3", callback: 
    function(){for (i=0;i<=1;i--)alert("FAIL!");}
});
Arnis Lapsa
fuente
0

Supongo que este puede ser un ejemplo que se explica por sí mismo:

function clickOn(elem /*bubble, cancelable*/) {
    var bubble =     (arguments.length > 1)  ? arguments[1] : true;
    var cancelable = (arguments.length == 3) ? arguments[2] : true;

    var cle = document.createEvent("MouseEvent");
    cle.initEvent("click", bubble, cancelable);
    elem.dispatchEvent(cle);
}
ses
fuente
-5

¿Se puede anular la función? ¿Esto no funcionará?

function doSomething(id){}
function doSomething(id,parameters){}
function doSomething(id,parameters,callback){}
J.Hendrix
fuente
8
No, esto no funcionará. No obtendrá ningún error, pero Javascript siempre usará la última función que haya definido.
jd.
66
Guau. Pensé que estabas loco. Acabo de probarlo. Tienes razón. Mi mundo acaba de cambiar un poco para mí. Creo que tengo mucho JavaScript para mirar hoy para asegurarme de que no tengo nada como esto en producción. Gracias por el comentario. Te di un +1.
J.Hendrix