Manipulando parâmetros opcionais em javascript

127

Eu tenho uma função javascript estática que pode levar 1, 2 ou 3 parâmetros:

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

Sei que sempre posso testar se um determinado parâmetro é indefinido, mas como saber se o que foi passado foi o parâmetro ou o retorno de chamada?

Qual é a melhor maneira de fazer isso?


Exemplos do que pode ser passado em:

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.
fonte
2
Você pode mostrar um exemplo do que poderia ser passado?
7289 James Black

Respostas:

163

Você pode saber quantos argumentos foram passados ​​para sua função e pode verificar se seu segundo argumento é uma função ou não:

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

Você também pode usar o objeto de argumentos desta maneira:

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];
  }
  //...
}

Se você estiver interessado, dê uma olhada neste artigo de John Resig, sobre uma técnica para simular a sobrecarga de método no JavaScript.

CMS
fonte
Por que usar Object.prototype.toString.call (parâmetros) == "[Função do objeto]" e não typeof (parâmetros) === 'função'? Existe alguma diferença importante entre eles? PS o artigo que você menciona parecem usar o último
Tomer Cagan
@TomerCagan Eu acho que é uma questão de preferência (ish). Existem boas respostas / comentários sobre o tópico nesta pergunta.
275166 Philleyiiiipp
75

Er - isso implicaria que você está invocando sua função com argumentos que não estão na ordem correta ... o que eu não recomendaria.

Em vez disso, eu recomendaria alimentar um objeto para sua função:

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

Benefícios:

  • você não precisa dar conta da ordem dos argumentos
  • você não precisa fazer a verificação de tipo
  • é mais fácil definir valores padrão porque nenhuma verificação de tipo é necessária
  • menos dores de cabeça. imagine se você incluísse um quarto argumento, teria que atualizar sua verificação de tipo sempre e se o quarto ou consecutivo também funcionarem?

Desvantagens:

  • hora de refatorar o código

Se você não tiver escolha, poderá usar uma função para detectar se um objeto é realmente uma função (veja o último exemplo).

Nota: Esta é a maneira correta de detectar uma função:

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

isFunction( function(){} )
meder omuraliev
fonte
"isso funcionaria 99% do tempo devido a alguns erros do ES". Você pode explicar mais? Por que isso pode dar errado?
jd.
Eu adicionei o código correto para detectar uma função. Eu acredito que o bug está aqui: bugs.ecmascript.org/ticket/251
meder omuraliev
Eu sugiro que você alimente apenas um objeto. Caso contrário, use o método do CMS.
meder omuraliev
Oh ... caramba ... Acabei de postar a mesma idéia.
Arnis Lapsa 7/10/09
1
Uma outra desvantagem possível é a falta de inteligência. Eu não considero isso um grande problema, mas deve ser observado.
Edyn
2

Você deve verificar o tipo de parâmetros recebidos. Talvez você deva usar a argumentsmatriz, já que o segundo parâmetro às vezes pode ser 'parameters' e, às vezes, 'callback', e nomear os parâmetros pode ser enganoso.

Kamil Szot
fonte
2

Sei que essa é uma pergunta bastante antiga, mas lidei com isso recentemente. Deixe-me saber o que você acha desta solução.

Criei um utilitário que me permite digitar argumentos fortemente e deixá-los opcionais. Você basicamente envolve sua função em um proxy. Se você pular um argumento, ele será indefinido . Pode ficar estranho se você tiver vários argumentos opcionais do mesmo tipo, um ao lado do outro. (Existem opções para passar funções em vez de tipos para fazer verificações personalizadas de argumentos, além de especificar valores padrão para cada parâmetro.)

É assim que a implementação se parece:

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

Para maior clareza, eis o que está acontecendo:

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 ... */

           }
         );
};

Você pode ver o código do proxy organizeArgs no meu repositório GitHub aqui:

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

Aqui está a função de utilitário com alguns comentários copiados do repositório:

/*
 ****** 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
fonte
2

Eu recomendo que você use o ArgueJS .

Você pode apenas digitar sua função desta maneira:

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

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

Eu considerei pelos seus exemplos que você deseja que seu idparâmetro seja uma string, certo? Agora, getDataestá exigindo um String ide está aceitando os opcionais Object parameterse Function callback. Todos os casos de uso que você postou funcionarão conforme o esperado.

zVictor
fonte
1

Você está dizendo que pode ter chamadas como estas: getData (id, parameters); getData (id, retorno de chamada)?

Nesse caso, você obviamente não pode confiar na posição e precisa analisar o tipo: getType () e, se necessário, getTypeName ()

Verifique se o parâmetro em questão é uma matriz ou uma função.

DmitryK
fonte
0

Eu acho que você deseja usar typeof () aqui:

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
fonte
0

Se o seu problema é apenas com sobrecarga de função (você precisa verificar se o parâmetro 'parameters' é 'parameters' e não 'callback'), recomendo que você não se preocupe com o tipo de argumento e
use essa abordagem . A ideia é simples - use objetos literais para combinar seus 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
fonte
0

Acho que isso pode ser um exemplo auto-explicativo:

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
fonte
-5

Você pode substituir a função? Isso não vai funcionar:

function doSomething(id){}
function doSomething(id,parameters){}
function doSomething(id,parameters,callback){}
J.Hendrix
fonte
8
Não, isso não vai funcionar. Você não receberá nenhum erro, mas o Javascript sempre usará a função mais recente que você definiu.
jd.
6
Uau. Eu pensei que você era louco. Eu apenas testei. Você está certo. Meu mundo mudou um pouco para mim. Acho que tenho muito JavaScript para examinar hoje para garantir que não haja nada parecido com isso em produção. Obrigado pelo comentário. Eu te dei um +1.
7269 J.Hendrix