Obsługa parametrów opcjonalnych w javascript

127

Mam statyczną funkcję javascript, która może przyjmować 1, 2 lub 3 parametry:

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

Wiem, że zawsze mogę sprawdzić, czy dany parametr jest niezdefiniowany, ale skąd mam wiedzieć, czy przekazano parametr, czy wywołanie zwrotne?

Jaki jest najlepszy sposób na zrobienie tego?


Przykłady tego, co można przekazać:

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.
źródło
2
Czy możesz pokazać przykład tego, co można przekazać?
James Black

Odpowiedzi:

162

Możesz wiedzieć, ile argumentów zostało przekazanych do twojej funkcji i możesz sprawdzić, czy twój drugi argument jest funkcją, czy nie:

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

Możesz również użyć obiektu arguments w następujący sposób:

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

Jeśli jesteś zainteresowany, zajrzyj do tego artykułu autorstwa Johna Resiga, na temat techniki symulacji przeciążania metod w JavaScript.

Christian C. Salvadó
źródło
Po co używać Object.prototype.toString.call (parameters) == "[object Function]", a nie typeof (parameters) === 'function'? Czy jest między nimi jakaś ważna różnica? PS artykuł, o którym wspomniałeś, zdaje się używać tego drugiego
Tomer Cagan
@TomerCagan Myślę, że to kwestia preferencji (ish). Pod tym pytaniem jest kilka dobrych odpowiedzi / komentarzy na ten temat .
Philiiiiiipp,
75

Eee - to by oznaczało, że wywołujesz swoją funkcję z argumentami, które nie są w odpowiedniej kolejności ... czego nie polecałbym.

Zalecałbym zamiast tego podawanie obiektu do twojej funkcji w następujący sposób:

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

Korzyści:

  • nie musisz brać pod uwagę kolejności argumentów
  • nie musisz sprawdzać typów
  • łatwiej jest zdefiniować wartości domyślne, ponieważ nie jest konieczne sprawdzanie typu
  • mniej bólów głowy. wyobraź sobie, że jeśli dodasz czwarty argument, będziesz musiał za każdym razem zaktualizować sprawdzanie typu, a co, jeśli czwarty lub kolejny argument to również funkcje?

Wady:

  • czas na refaktoryzację kodu

Jeśli nie masz wyboru, możesz użyć funkcji do wykrycia, czy obiekt jest rzeczywiście funkcją (zobacz ostatni przykład).

Uwaga: to jest właściwy sposób wykrywania funkcji:

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

isFunction( function(){} )
meder omuraliev
źródło
„to działałoby w 99% przypadków z powodu błędów ES”. Czy możesz to bardziej wyjaśnić? Dlaczego to mogło się nie udać?
jd.
Dodałem poprawny kod do wykrywania funkcji. Myślę, że błąd jest tutaj: bugs.ecmascript.org/ticket/251
meder omuraliev
Gorąco sugerowałbym karmienie tylko jednym przedmiotem. Jeśli nie, skorzystaj z metody CMS.
meder omuraliev
Och ... cholera ... Właśnie opublikowałem ten sam pomysł.
Arnis Lapsa
1
Inną możliwą wadą jest brak inteligencji. Nie uważam tego za wielką sprawę, ale należy to zauważyć.
Edyn
2

Należy sprawdzić rodzaj otrzymywanych parametrów. Może powinieneś użyć argumentstablicy, ponieważ drugi parametr może czasami być „parametrami”, a czasami „wywołaniem zwrotnym” i nazwanie go parametrami może być mylące.

Kamil Szot
źródło
2

Wiem, że to dość stare pytanie, ale ostatnio zajmowałem się tym. Daj mi znać, co myślisz o tym rozwiązaniu.

Stworzyłem narzędzie, które pozwala mi silnie wpisywać argumenty i pozostawić je opcjonalne. Zasadniczo zawijasz swoją funkcję w proxy. Jeśli pominiesz argument, jest on niezdefiniowany . Może się to wydawać dziwne, jeśli masz wiele opcjonalnych argumentów tego samego typu obok siebie. (Istnieją opcje przekazywania funkcji zamiast typów do wykonywania niestandardowych sprawdzeń argumentów, a także określanie wartości domyślnych dla każdego parametru).

Tak wygląda implementacja:

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

Dla jasności, oto co się dzieje:

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

           }
         );
};

Możesz wyświetlić kod proxyrangeArgs w moim repozytorium GitHub tutaj:

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

Oto funkcja narzędzia z kilkoma komentarzami skopiowanymi z repozytorium:

/*
 ****** 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
źródło
2

Polecam użycie ArgueJS .

Możesz po prostu wpisać swoją funkcję w ten sposób:

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

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

Rozważyłem na podstawie twoich przykładów, że chcesz, aby twój idparametr był ciągiem, prawda? Teraz getDatawymaga String idi akceptuje opcje Object parametersi Function callback. Wszystkie opublikowane przypadki użycia będą działać zgodnie z oczekiwaniami.

zVictor
źródło
1

Mówisz, że możesz mieć takie wywołania: getData (id, parameters); getData (id, callback)?

W tym przypadku nie możesz oczywiście polegać na pozycji i musisz polegać na analizie typu: getType (), a następnie, jeśli to konieczne, getTypeName ()

Sprawdź, czy dany parametr jest tablicą czy funkcją.

DmitryK
źródło
0

Myślę, że chcesz tutaj użyć typeof ():

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
źródło
0

Jeśli twój problem dotyczy tylko przeciążenia funkcji (musisz sprawdzić, czy parametr 'parameters' to 'parametry', a nie 'callback'), radziłbym nie przejmować się typem argumentu i
użyć tego podejścia . Pomysł jest prosty - użyj obiektów dosłownych, aby połączyć parametry:

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
źródło
0

Myślę, że to może być zrozumiały przykład:

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
źródło
-5

Czy możesz zastąpić tę funkcję? Czy to nie zadziała:

function doSomething(id){}
function doSomething(id,parameters){}
function doSomething(id,parameters,callback){}
J.Hendrix
źródło
8
Nie, to nie zadziała. Nie otrzymasz żadnych błędów, ale JavaScript zawsze będzie używał najnowszej zdefiniowanej funkcji.
jd.
6
Łał. Myślałem, że zwariowałeś. Właśnie to przetestowałem. Masz rację. Mój świat trochę się dla mnie zmienił. Myślę, że mam dziś dużo JavaScript do obejrzenia, aby upewnić się, że nie mam czegoś takiego w produkcji. Dziękuję za komentarz. Dałem ci +1.
J.Hendrix