Wiele argumentów a obiekt opcji

157

Tworząc funkcję JavaScript z wieloma argumentami, zawsze mam do czynienia z takim wyborem: przekazać listę argumentów vs. przekazać obiekt opcji.

Na przykład piszę funkcję do mapowania nodeList do tablicy:

function map(nodeList, callback, thisObject, fromIndex, toIndex){
    ...
}

Zamiast tego mógłbym użyć tego:

function map(options){
    ...
}

gdzie opcje to obiekt:

options={
    nodeList:...,
    callback:...,
    thisObject:...,
    fromIndex:...,
    toIndex:...
}

Który z nich jest zalecany? Czy istnieją wytyczne dotyczące tego, kiedy używać jednej, a kiedy drugiej?

[Aktualizacja] Wydaje się, że istnieje konsensus na rzecz obiektu opcji, więc chciałbym dodać komentarz: jednym z powodów, dla których kusiło mnie, aby użyć listy argumentów w moim przypadku, było zachowanie zgodne z JavaScriptem wbudowana metoda array.map.

Christophe
źródło
2
Druga opcja daje nazwane argumenty, co moim zdaniem jest miłą rzeczą.
Werner Kvalem Vesterås
Czy są to argumenty opcjonalne czy wymagane?
I Hate Lazy
@ user1689607 w moim przykładzie ostatnie trzy są opcjonalne.
Christophe
Ponieważ ostatnie dwa argumenty są bardzo podobne, gdyby użytkownik przekazał tylko jeden lub drugi, tak naprawdę nigdy nie byłbyś w stanie wiedzieć, który z nich był zamierzony. Z tego powodu prawie potrzebujesz nazwanych argumentów. Ale doceniam, że chciałbyś zachować API podobne do natywnego API.
I Hate Lazy
1
Modelowanie po natywnym API nie jest złe, jeśli twoja funkcja robi coś podobnego. Wszystko sprowadza się do tego, „co sprawia, że ​​kod jest najbardziej czytelny”. Array.prototype.mapma proste API, które nie powinno sprawiać kłopotów żadnego częściowo doświadczonego programisty.
Jeremy J Starcher

Odpowiedzi:

160

Podobnie jak wiele innych, często wolę przekazywać funkcję options objectdo funkcji zamiast przekazywać długą listę parametrów, ale tak naprawdę zależy to od dokładnego kontekstu.

Używam czytelności kodu jako papierka lakmusowego.

Na przykład, jeśli mam to wywołanie funkcji:

checkStringLength(inputStr, 10);

Myślę, że kod jest całkiem czytelny tak, jak jest, a przekazywanie poszczególnych parametrów jest w porządku.

Z drugiej strony istnieją funkcje z takimi wywołaniami:

initiateTransferProtocol("http", false, 150, 90, null, true, 18);

Całkowicie nieczytelne, chyba że zrobisz jakieś badania. Z drugiej strony ten kod dobrze się czyta:

initiateTransferProtocol({
  "protocol": "http",
  "sync":      false,
  "delayBetweenRetries": 150,
  "randomVarianceBetweenRetries": 90,
  "retryCallback": null,
  "log": true,
  "maxRetries": 18
 });

To bardziej sztuka niż nauka, ale gdybym miał nazwać praktyczne zasady:

Użyj parametru opcji, jeśli:

  • Masz więcej niż cztery parametry
  • Dowolny z parametrów jest opcjonalny
  • Musiałeś kiedykolwiek sprawdzić funkcję, aby dowiedzieć się, jakie parametry ona potrzebuje
  • Jeśli ktoś kiedykolwiek spróbuje Cię udusić, krzycząc „ARRRRRG!”
Jeremy J Starcher
źródło
10
Świetna odpowiedź. To zależy. Uważaj na pułapki logiczne ariya.ofilabs.com/2011/08/hall-of-api-shame-boolean-trap.html
Trevor Dixon
2
O tak ... zapomniałem o tym łączu. Naprawdę zmusiło mnie to do przemyślenia działania API, a nawet przepisałem kilka fragmentów kodu po tym, jak dowiedziałem się, że robię rzeczy głupie. Dzięki!
Jeremy J Starcher
1
„Zawsze trzeba było wyszukać funkcję, aby dowiedzieć się, jakie parametry są potrzebne” - zamiast tego, używając obiektu opcji, nie zawsze trzeba będzie szukać metody, aby dowiedzieć się, jakie klucze są potrzebne i jak się je nazywa. Intellisense w IDE nie wyświetla tych informacji, podczas gdy parametry to robią. W większości IDE możesz po prostu najechać kursorem myszy na metodę, a pokaże ci ona, jakie są parametry.
simbolo
1
Jeśli jesteście zainteresowani konsekwencjami wydajności tego kawałka „najlepszych praktyk kodowania”, oto test jsPerf w obie strony: jsperf.com/function-boolean-arguments-vs-options-object/10 Zwróć uwagę na mały zwrot w trzecia alternatywa, w której używam obiektu opcji `` preset '' (stały), co można zrobić, gdy masz wiele wywołań (w czasie działania środowiska wykonawczego, np. strony internetowej) z tymi samymi ustawieniami, znanymi w czasie programowania (w skrócie : kiedy wartości opcji są zakodowane na stałe w kodzie źródłowym).
Ger Hobbelt,
2
@Sean Szczerze mówiąc, w ogóle nie używam tego stylu kodowania. Przerzuciłem się na TypeScript i używam nazwanych parametrów.
Jeremy J Starcher
28

Wiele argumentów dotyczy głównie parametrów obowiązkowych. Nie ma z nimi nic złego.

Jeśli masz opcjonalne parametry, sprawa się komplikuje. Jeśli jeden z nich polega na innych, tak że mają określoną kolejność (np. Czwarty potrzebuje trzeciego), nadal powinieneś używać wielu argumentów. Prawie wszystkie natywne metody EcmaScript i DOM działają w ten sposób. Dobrym przykładem jest openmetoda XMLHTTPrequests , w której ostatnie 3 argumenty są opcjonalne - reguła jest taka jak „bez hasła bez użytkownika” (zobacz także dokumentację MDN ).

Obiekty opcji są przydatne w dwóch przypadkach:

  • Masz tak wiele parametrów, że robi się to mylące: „Nazewnictwo” Ci pomoże, nie musisz martwić się o ich kolejność (zwłaszcza jeśli mogą się zmienić)
  • Masz opcjonalne parametry. Obiekty są bardzo elastyczne i bez żadnego porządkowania po prostu przekazujesz rzeczy, których potrzebujesz i nic więcej undefined.

W twoim przypadku polecam map(nodeList, callback, options). nodelisti callbacksą wymagane, pozostałe trzy argumenty pojawiają się tylko sporadycznie i mają rozsądne wartości domyślne.

Innym przykładem jest JSON.stringify. Możesz chcieć użyć spaceparametru bez przekazywania replacerfunkcji - wtedy musisz wywołać …, null, 4). Obiekt arguments mógłby być lepszy, chociaż nie jest tak naprawdę rozsądny dla tylko 2 parametrów.

Bergi
źródło
+1 to samo pytanie co @ trevor-dixon: czy widziałeś tę mieszankę używaną w praktyce, na przykład w bibliotekach js?
Christophe,
Przykładem mogą być metody Ajax jQuery . Akceptują [obowiązkowy] adres URL jako pierwszy argument i ogromny argument opcji jako drugi.
Bergi
bardzo dziwne! Nigdy wcześniej tego nie zauważyłem. Zawsze widziałem, że jest używany z adresem URL jako właściwością opcji ...
Christophe,
Tak, jQuery robi dziwne rzeczy ze swoimi opcjonalnymi parametrami, zachowując kompatybilność wsteczną :-)
Bergi
1
Moim zdaniem jest to jedyna rozsądna odpowiedź.
Benjamin Gruenbaum
11

Najlepszym rozwiązaniem będzie wykorzystanie podejścia „opcji jako obiektu”. Nie musisz martwić się o kolejność właściwości i jest większa elastyczność w przekazywaniu danych (na przykład parametry opcjonalne)

Utworzenie obiektu oznacza również, że opcje mogą być łatwo używane w wielu funkcjach:

options={
    nodeList:...,
    callback:...,
    thisObject:...,
    fromIndex:...,
    toIndex:...
}

function1(options){
    alert(options.nodeList);
}

function2(options){
    alert(options.fromIndex);
}
Fluidbyte
źródło
(Rozsądne) założenie jest takie, że obiekt zawsze będzie miał te same pary kluczy. Jeśli pracujesz z badziewnym / niespójnym API, masz inny problem.
backdesk
9

Myślę, że jeśli tworzysz instancję lub wywołujesz metodę obiektu, chcesz użyć obiektu opcji. Jeśli jest to funkcja, która operuje tylko jednym lub dwoma parametrami i zwraca wartość, preferowana jest lista argumentów.

W niektórych przypadkach dobrze jest używać obu. Jeśli twoja funkcja ma jeden lub dwa wymagane parametry i kilka opcjonalnych, ustaw pierwsze dwa parametry jako wymagane, a trzeci jako opcjonalny skrót opcji.

Na twoim przykładzie zrobiłbym to map(nodeList, callback, options). Lista węzłów i wywołanie zwrotne są wymagane, dość łatwo jest stwierdzić, co się dzieje, po prostu odczytując wywołanie do niego i przypomina to istniejące funkcje mapy. Wszelkie inne opcje można przekazać jako opcjonalny trzeci parametr.

Trevor Dixon
źródło
+1 interesujący. Czy widziałeś to w praktyce, na przykład w bibliotekach js?
Christophe,
7

Twój komentarz do pytania:

w moim przykładzie ostatnie trzy są opcjonalne.

Więc dlaczego tego nie zrobić? (Uwaga: to jest dość surowy JavaScript. Zwykle używałbymdefault skrótu i ​​zaktualizowałbym go opcjami przekazanymi za pomocą Object.extend lub JQuery.extend lub podobnych ...)

function map(nodeList, callback, options) {
   options = options || {};
   var thisObject = options.thisObject || {};
   var fromIndex = options.fromIndex || 0;
   var toIndex = options.toIndex || 0;
}

Tak więc, ponieważ teraz jest o wiele bardziej oczywiste, co jest opcjonalne, a co nie, wszystkie z nich są prawidłowymi zastosowaniami funkcji:

map(nodeList, callback);
map(nodeList, callback, {});
map(nodeList, callback, null);
map(nodeList, callback, {
   thisObject: {some: 'object'},
});
map(nodeList, callback, {
   toIndex: 100,
});
map(nodeList, callback, {
   thisObject: {some: 'object'},
   fromIndex: 0,
   toIndex: 100,
});
Izkata
źródło
Jest to podobne do odpowiedzi @ trevor-dixon.
Christophe,
5

Mogę się trochę spóźnić na imprezę z tą odpowiedzią, ale szukałem opinii innych programistów na ten temat i trafiłem na ten wątek.

Zdecydowanie nie zgadzam się z większością respondentów i opowiadam się za podejściem „wielu argumentów”. Moim głównym argumentem jest to, że zniechęca to innych anty-wzorców, takich jak „mutowanie i zwracanie obiektu param” lub „przekazywanie tego samego obiektu param innym funkcjom”. Pracowałem w bazach kodu, które w znacznym stopniu nadużywały tego anty-wzorca, a debugowanie kodu, który to robi, szybko staje się niemożliwe. Myślę, że jest to bardzo praktyczna reguła specyficzna dla JavaScript, ponieważ Javascript nie jest silnie wpisany na maszynie i pozwala na takie dowolnie zbudowane obiekty.

Osobiście uważam, że programiści powinni być jawni podczas wywoływania funkcji, unikać przekazywania nadmiarowych danych i unikać modyfikacji przez odniesienie. Nie chodzi o to, że ten wzorzec wyklucza pisanie zwięzłego, poprawnego kodu. Po prostu czuję, że dzięki temu twój projekt znacznie łatwiej wpadnie w złe praktyki programistyczne.

Rozważ następujący okropny kod:

function main() {
    const x = foo({
        param1: "something",
        param2: "something else",
        param3: "more variables"
    });

    return x;
}

function foo(params) {
    params.param1 = "Something new";
    bar(params);
    return params;
}


function bar(params) {
    params.param2 = "Something else entirely";
    const y = baz(params);
    return params.param2;
}

function baz(params) {
    params.params3 = "Changed my mind";
    return params;
}

Tego rodzaju nie tylko wymaga bardziej przejrzystej dokumentacji, aby określić zamiar, ale także pozostawia miejsce na niejasne błędy. Co zrobić, jeśli modyfikuje developer param1w bar()? Jak myślisz, ile czasu zajmie przeglądanie bazy kodów o wystarczającej wielkości, aby to złapać? Trzeba przyznać, że ten przykład jest nieco nieszczery, ponieważ zakłada, że ​​programiści już popełnili kilka anty-wzorców do tego momentu. Pokazuje jednak, jak przepuszczanie obiektów zawierających parametry stwarza większe pole do popełniania błędów i niejednoznaczności, wymagając większej sumienności i przestrzegania stałej poprawności.

Tylko moje dwa centy w tej sprawie!

ajxs
źródło
3

To zależy.

Opierając się na moich obserwacjach dotyczących projektowania tych popularnych bibliotek, oto scenariusze, w których powinniśmy użyć obiektu opcji:

  • Lista parametrów jest długa (> 4).
  • Niektóre lub wszystkie parametry są opcjonalne i nie zależą od określonej kolejności.
  • Lista parametrów może wzrosnąć w przyszłej aktualizacji API.
  • API zostanie wywołane z innego kodu, a nazwa API nie jest wystarczająco jasna, aby określić znaczenie parametrów. Więc może potrzebować silnej nazwy parametru dla czytelności.

I scenariusze użycia listy parametrów:

  • Lista parametrów jest krótka (<= 4).
  • Wymagana jest większość lub wszystkie parametry.
  • Parametry opcjonalne są w określonej kolejności. (tj. $ .get)
  • Łatwo rozpoznać znaczenie parametrów według nazwy API.
Lynn Ning
źródło
2

Object jest bardziej preferowany, ponieważ jeśli przekazujesz obiekt, łatwo jest rozszerzyć liczbę właściwości w tych obiektach i nie musisz zwracać uwagi na kolejność, w jakiej argumenty zostały przekazane.

happyCoda
źródło
1

W przypadku funkcji, która zwykle używa wstępnie zdefiniowanych argumentów, lepiej użyć obiektu opcji. Odwrotnym przykładem będzie funkcja, która pobiera nieskończoną liczbę argumentów, na przykład: setCSS ({wysokość: 100}, {szerokość: 200}, {background: "# 000"}).

Ilya Sidorovich
źródło
0

Przyjrzałbym się dużym projektom javascript.

Rzeczy takie jak mapa google często stwierdzają, że tworzone obiekty wymagają obiektu, ale funkcje wymagają parametrów. Myślę, że ma to związek z argumentami OPCJI.

Jeśli potrzebujesz argumentów domyślnych lub opcjonalnych, obiekt prawdopodobnie byłby lepszy, ponieważ jest bardziej elastyczny. Ale jeśli nie masz normalnych funkcjonalnych argumentów, są bardziej wyraźne.

Javascript też ma argumentsobiekt. https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Functions_and_function_scope/arguments

dm03514
źródło