JavaScript Currying w prostych słowach: zamknięcia funkcji, częściowe stosowanie, ponowne wykorzystanie
Dowiedz się, w jaki sposób kurrying przekształca funkcje JavaScript z wieloma argumentami w powtarzalne łańcuchy z jednym argumentem, w czym różni się od częściowej aplikacji oraz kiedy warto go pominąć.
Jeśli ciągle przekazujesz tę samą pierwszą wartość do funkcji, za każde wezwanie płacisz niewielką opłatę za powtórzenie. Kurryzacja pozwala na raz ustalić część argumentów funkcji i ponownie wykorzystać uzyskany wynik. Poniżej zobaczysz, czym jest kurryzacja, jak klauzule ją umożliwiają, w czym różni się od częściowego stosowania funkcji oraz kiedy rzeczywiście poprawia kod.
Co oznacza kurryzacja
Kurryzacja przekształca funkcję przyjmującą kilka argumentów w sekwencję funkcji, z których każda przyjmuje dokładnie jeden. Zwykłe wezwanie dostarcza wszystko naraz:
f(a, b, c)
Wersja kurryzowana dostarcza jedną wartość za każde wezwanie, a każde z nich zwraca następną funkcję w łańcuchu:
f(a)(b)(c)
Każda funkcja pośrednia pamięta wartości z poprzednich wezwań. Ta pamięć pochodzi od klauzul, o których mowa poniżej.
Konwertowanie zwykłej funkcji
Zacznijmy od zwykłej funkcji dodawania dwóch argumentów:
function sum(a, b) {
return a + b;
}
sum(2, 3); // 5
Niewielki pomocnik może otoczyć dowolną funkcję przyjmującą dwa argumenty: funkcja zewnętrzna bierze pierwszą wartość, zwraca funkcję, która przyjmuje drugą, a dopiero potem wywołuje oryginalną z obiema wartościami:
function curry(fn) {
return function (a) {
return function (b) {
return fn(a, b);
};
};
}
const curriedSum = curry(sum);
curriedSum(2)(3); // 5
Na co zwrócić uwagę:
curryjest funkcją wyższego rzędu: zarówno jej wejście, jak i wyjście to funkcje.- Każda funkcja, którą zwraca, oczekuje tylko jednej wartości.
- Funkcja wewnętrzna nadal może dostępować do
apo zwróceniu się funkcji zewnętrznej, ponieważ ma do niej dostęp dzięki scopingowi.
Ten pomocnik obsługuje dokładnie dwa parametry. Wersje uniwersalne sprawdzają fn.length i kontynuują zbieranie argumentów, dopóki nie zgromadzi się ich wystarczająca ilość, ale zasada pozostaje ta sama.
Praktyczny przykład: specjalistyczne loggerzy
Arytmetyka ukrywa rzeczywisty zysk, więc rozważmy loggera, który przyjmuje poziom powagi i wiadomość:
function log(level, message) {
console.log(`[${level}] ${message}`);
}
Miejsca wywołania szybko zaczynają powtarzać ten sam poziom:
log("ERROR", "Something broke");
log("ERROR", "Invalid token");
log("ERROR", "API failed");
Kurrierowany logger umożliwia utworzenie z góry skonfigurowanych loggerów tylko raz:
function log(level) {
return function (message) {
console.log(`[${level}] ${message}`);
};
}
const errorLog = log("ERROR");
const infoLog = log("INFO");
errorLog("Something broke");
errorLog("Invalid token");
infoLog("User logged in");
Poziom jest ustalony po utworzeniu errorLog i infoLog. Miejsca wywołania stają się krótsze, intencja jest łatwiejsza do zrozumienia, a specjalistyczne funkcje mogą być eksportowane lub wprowadzane w innych miejscach. To połączenie możliwości ponownego użycia i wyraźności jest głównym powodem stosowania kurrierowania.
Częściowe zastosowanie
Częściowe zastosowanie oznacza wypełnienie uprzednio niektórych argumentów istniejącej funkcji, aby uzyskać nową funkcję, bez zmiany jej struktury. JavaScript obsługuje to natywnie za pomocą bind:
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2);
double(5); // 10
Pierwszy argument funkcji bind ustawia wartość this, którą funkcja multiply ignoruje, dlatego wynosi null. Funkcja multiply nadal przyjmuje dwa argumenty; double to po prostu wariant, w którym podana jest pierwsza z tych wartości.
Currying versus częściowe stosowanie
Oba podejścia mają ten sam cel, ale różne mechanizmy:
- Currying przekształca funkcję w łańcuch funkcji przyjmujących po jednym argumencie.
- Częściowe stosowanie wprowadza niektóre argumenty już teraz, zachowując przy tym oryginalną strukturę funkcji.
Każdy krok w łańcuchu powstałym dzięki currying jest w rzeczywistości przykładem częściowego stosowania z jednym argumentem, ale częściowe stosowanie nigdy nie wymaga użycia currying.
Currying bez końca
Rozrywkowy wariant polega na ciągłym przyjmowaniu wartości, aż łańcuch zostanie zakończony pustym wywołaniem:
let infiniteSum = function (a) {
return function (b) {
if (b !== undefined) return infiniteSum(a + b);
return a;
};
};
infiniteSum(1)(2)(3)(4)(); // 10
Sposób jego działania:
- Każde wywołanie z wartością dodaje ją do bieżącej sumy i zwraca nową funkcję przenoszącą tę sumę.
- Ostatnie wywołanie
()pozostawiabjakoundefined, dzięki czemu zwracana jest suma. - Zamknięcia przenoszą sumę z jednego kroku na następny.
Wyraźna kontrola b !== undefined jest istotna. Test prawdziwości typu if (b) traktowałby 0 jako znak zakończenia, więc infiniteSum(1)(0)(2)() zatrzymałby się przy zerze zamiast zwrócić 3. Ten wzorzec jest doskonałym ćwiczeniem z zamknięć i częstym pytaniem na rozmowach kwalifikacyjnych, ale łatwo zapomniane ostatnie wywołanie sprawia, że nie nadaje się jako domyślna implementacja dla API produkcyjnych.
Zamknięcia umożliwiają to wszystko
Oto najprostszy dodawacz w formie curryzacji:
function add(a) {
return function (b) {
return a + b;
};
}
add(3)(4); // 7
add(3) zwraca funkcję wewnętrzną i kończy swoją pracę, a mimo to ta funkcja wewnętrzna nadal może odczytywać wartość a. To jest właśnie zamykanie: funkcja zachowuje dostęp do zmiennych z obszaru, w którym została zdefiniowana, nawet po tym, jak funkcja zewnętrzna się zakończyła. Ponieważ uchwycona wartość pozostaje niezmieniona, można przechowywać i ponownie używać funkcji częściowo zastosowanej:
const add3 = add(3);
add3(4); // 7
add3(10); // 13
Aby dowiedzieć się więcej na temat sposobu rozwiązywania problemu tych uchwyconych zmiennych, zapoznaj się z artykułem jak łańcuch obszarów rozwiązuje zmienne w JavaScript.
Kiedy go używać, a kiedy pominąć
Currying jest przydatne w następujących sytuacjach:
- te same argumenty pojawiają się w wielu miejscach wywołania
- poprawia czytelność kodu zamiast go komplikować
- chcesz mieć małe, kombinowalne i wielokrotnie używalne funkcje
Pomijaj to w przypadku:
- prostej funkcji jednorazowej, która nie zyskuje nic na podziale
- gdy twój zespół nie zna tego wzorca
- gdy łańcuchowe wywołania utrudniają czytelność
Podsumowanie
Kurrying polega właściwie na uporządkowaniu argumentów funkcji według momentu, w którym stają się znane: konfiguracja, np. poziom logowania lub podstawowa adresa URL, jest ustalana wcześnie, natomiast dane dla poszczególnych wywołań przychodzą później. Zamknięcia zajmują się przechowywaniem tych informacji, bind umożliwia lżejsze częściowe zastosowanie funkcji, gdy nie potrzebujesz pełnego łańcucha, a kluczowe pytanie zawsze brzmi, czy kod wywołujący staje się łatwiejszy do odczytania i ponownego użycia.
Literatura pokrewna
- Zrozumienie zamknięć w JavaScript i pętli eventów — Dowiedz się, jak zamknięcia przechowują zmienne zewnętrzne oraz jak pętla eventów porządkuje stos wywołań, mikrozadania i makrozadania za pomocą praktycznych przykładów kodu.
- Jak łańcuch horyzontu w JavaScript faktycznie rozwiązuje zmienne — Wyjaśnia mechanizm łańcucha horyzontu leksykalnego odpowiadający za wyszukiwanie zmiennych, dlaczego użycie klucza var powoduje błędy w pętlach oraz jak naturalnie wynikają z tego zamknięcia.