Funkcje natychmiastowe w JavaScript

Dzisiaj zajmiemy się omówieniem jednej z ważniejszych moim zdaniem kwestii, a mianowicie przeanalizujemy czym są i jak się stosuje tzw. funkcje natychmiastowe w JavaScript w formie funkcji anonimowych oraz funkcji nazwanych.

W języku JavaScript zmienne mogą posiadać zakres funkcyjny (słowo var) lub blokowy (słwo let lub const). Przed wejściem w życie standardu ECMAScript 6 istniał tylko zasięg funkcyjny, poza jednym małym wyjątkiem, a mianowicie bloku catch w konstrukcji przechwytywania wyjątków try…catch. Do bloku catch(err) przekazywany jest obiekt błędu (u nas „err”), który jest widoczny wyłącznie w zakresie blokowym catch.

Funkcje natychniastowe (IIFE – Immediately Invoked Function Expression) mogą więc służyć do stworzenia nowego zakresu, imitującego w pewnym sensie zakres blokowy o czym powiemy za chwilę.

Dzięki stworzeniu zamkniętego zakresu funkcyjnego unikamy deklarowania zmiennych w zasięgu globalnym co zawsze jest złą praktyką i może czasem prowadzić do konfliktów, np. w czasie korzystania z zewnętrznych bibliotek. W kodzie JavaScript wykonywanym po stronie przeglądarki powinna być maksymalnie jedna zmienna globalna, a pozostałe zmienne powinny być „otoczone” zakresem funkcyjnym.

Tworzenie funkcji natychmiastowych IIFE

Natychmiastowe wyrażenia funkcyjne można tworzyć jako samodzielne lub wynik ich „działania” przypisać bezpośrednio do zmiennej:

//Wariant 1
(function () {
   //kod funkcji
})();

//Wariant 2:
var myVar = (function (){
   //kod funkcji, której wynik zostanie zwrócony
   //i zapisany w zmiennej myVar
})();

Oba warianty są jak najbardziej poprawne, a wybór którego użyć zależy od tego co chcemy w danej chwili wykonać. Samodzielnie stosowanie natychmiastowych wyrażeń funkcyjnych (wariant 1) jest najczęściej stosowane w celu stworzenia nowego, zamkniętego zakresu dla zmiennych, dzięki czemu unikamy tworzenia zmiennych globalnych.

Wewnątrz takiej funkcji natychmiastowej mogą znajdować się np. wywołania funkcji odpowiedzialnych za animacje na stronie, obsługa zdarzeń w interakcji z użytkownikiem (np. metody addEventListener) itp.

W wariancie drugim funkcję natychmiastową zastosowano w celu stworzenia oddzielnego zasięgu dla zmiennych i dla kodu, którego wynik będzie zapisany w zmiennej myVar. Pod hasłem „wynik funkcji” w tym wypadku należy rozumieć wartość zwracaną przez jawną lub niejawną instrukcję return.

Funkcje natychmiastowe, jak wskazuje ich nazwa, wykonywane są od razu w miejscu ich deklaracji. Odpowiedzialne są za to dodatkowe nawiasy okrągłe. Porównajmy deklarację funkcji „zwykłej” i natychmiastowej (w dwóch wariantach zapisu nawiasów):

function A () {
    //kod funkcji
}

A(); //dopiero tutaj następuje wywołanie funkcji A

(function B () {
    //kod funkcji
})();

(function C () {
    //kod funkcji
}());

Funkcja A jest deklaracją zwykłą i nie powoduje natychmiastowego wywołania tej funkcji. Musimy więc zrobić to samodzielnie poprzez zapis A(). Funkcje B i C są z kolei natychmiastowymi wyrażeniami funkcyjnymi i zostaną wykonane od razu.

Funkcje natychmiastowe mają dwie dodatkowe pary nawiasów. Zwróć uwagę na dwa sposoby zapisu końcowych nawiasów „()” pełniących rolę wywoływacza funkcji. Obie formy zapisu są poprawne i nie będziemy wdawać się w dyskusję nad wyższością któregoś z nich. Ja preferuję zapis pierwszy (funkcja B) jednakże najczęściej wybór sposobu zapisu jest podyktowany zasadami panującymi w danym zespole programistów.

Funkcje IIFE przypisywane do zmiennych

Przypisanie IIFE do zmiennej jest często stosowanym wzorcem tworzenia obiektu lecz z przekazaniem tylko wybranych jego metod. Istnieją również inne sposoby tworzenia tego typu zamkniętych struktur (jak chociażby wykorzystanie modułów wprowadzonych w ES6) ale jest to nadal często stosowana praktyka dlatego postanowiłem ją omówić.

var myLibrary = (function (){
   var a, b, c; //zmienne w zasięgu funkcji IIFE
   function myFunc(a,b) {
       return a+b;
   }
   function myFuncPrivate () {
       //wnętrze funkcji prywatnej
   }
   //kod funkcji, np. sprawdzający obsługę różnych
   //metod przez środowisko

   return {
       func: myFunc
   };
})();

myLibrary.func(1,2); //3

W powyższym kodzie stworzyliśmy zmienną globalną myLibrary, która zawiera wszystkie metody, które chcemy wielokrotnie wykorzystywać w kodzie. Do jej stworzenia potrzebowaliśmy jednak dodatkowych zmiennych i funkcji prywatnych. w JavaScript nie ma możliwości jawnego deklarowania zmiennych i funkcji jako private lub public jak w innych językach obiektowych. Zastosowaliśmy więc pewną sztuczkę z funkcją natychmiastową, ale jej wartość zwrotna, czyli instrukcja return zwraca obiekt, zawierający wyłącznie jedną metodę func, która jest bezpośrednią referencją do funkcji myFunc. Z zewnątrz nie ma dostępu do funkcji myFuncPrivate.

Uwaga na formę zapisu instrukcji return!

Stosując taki wzorzec tworzenia obiektów z metodami publicznymi i prywatnymi musimy zwracać uwagę na właściwą formę zapisu instrukcji return. Łatwo w tym momencie o nieuwagę i przypadkowe zwrócenie przez wyrażenie IIFE zupełnie nieoczekiwanego przez nas wyniku. Spójrzmy na poniższy przykład:

let a = (function () {
    //rób coś "prywatnego"
    return {
        func: function add (num) {
                  console.log(num + 5);
              }
    };
})();

a.func(4); //9  4+5=9


let b = (function () {
    //rób coś "prywatnego"
    return
    {
        func: function add (num) {
                  console.log(num + 5);
              }
    }
})();

b.func(4); //TypeError

W drugim przypadku (zmienna „b”) za słowem return daliśmy enter i dopiero od nowej linii zapisaliśmy nawias klamrowy. Problemem jest jednak w tym wypadku automatyczne dodawanie średników przez silnik JavaScript wg ściśle określonych zasad. Ponieważ nawias klamrowy może istnieć jako samodzielny element składniowy (określa tutaj po prostu wydzielony blok kodu) to za słowem return został automatycznie dodany średnik. W konsekwencji otrzymaliśmy więc:

return; // odpowiada return undefined;

Zmiennej b nie przypisaliśmy więc żadnego obiektu lecz wartość typu prostego undefined, która jak wiadomo nie posiada metody o nazwie func.

W takich wypadkach wychwycenie błędu może ułatwić stosowanie funkcji nazwanych zamiast anonimowych. Konsola przeglądarki Chrome wyświetli nam informację, że dla undefined nie można wywołać metody add. Jeśli użylibyśmy zapisu:

        func: function (num) {
                  console.log(num + 5);
              }

to niestety nie będziemy mieli pełnego podglądu na stos wywołań i w wielu przypadkach dłużej będziemy poszukiwać błędu. Należy jednak mieć na uwadze fakt, że w dalszym kodzie nie możemy odnieść się do funkcji o nazwie add gdyż w zwracanym obiekcie taka właściwość nie istnieje, a tak na prawdę w przypadku obiektów JavaScript zawsze odwołujemy się do jego właściwości. Metodą nazywa się właściwość, którą można wywołać jak funkcję. Musimy więc odwołać się do ustalonej przez nas nazwy „publicznej” czyli „func”.

Funkcje natychmiastowe z parametrami

Wcześniej omówiłem jeden z częściej stosowanych przypadków, czyli wykorzystanie natychmiastowych wyrażeń funkcyjnych w celu stworzenia nowego, nieglobalnego zakresu dla zmiennych i funkcji. Jedną z praktyk stosowanych przy projektowaniu stron internetowych jest stworzenie w każdym z plików obsługujących podstronę (zdarzenia, animacje itp.) jednej funkcji natychmiastowej, stanowiącej „otoczkę” dla wszystkich zmiennych i funkcji deklarowanych w danym skrypcie. Zabezpiecza nas to przed ryzykiem kolizji nazw zmiennych czy funkcji w kilku skryptach ładowanych dla danej strony. Kolejne skrypty nadpisałyby wcześniejsze zmienne.

Wzorzec ten ma jednak jeszcze jedną zaletę, która niestety nie zawsze jest wykorzystywana. Otóż funkcje IIFE, jak wszystkie funkcje w JavaScript mogą przyjmować parametry. W przypadku funkcji IIFE możemy od razu w wywołaniu funkcji określić wartości tych parametrów. Może brzmi to nieco tajemniczo, więc najlepiej przejdźmy do konkretnego przykładu:

(function (win, doc) {
    let fotos = [...doc.querySelectorAll('img.myFotos')],
        width = win.innerWidth;
})(window, document);

Funkcja natychmiastowa przyjmuje dwa parametry (win, doc), którym przypisaliśmy referencje do obiektów odpowiednio window i document (w ostatnich nawiasach).

Co nam daje takie przypisanie? Otóż w powyższym przykładzie praktycznie nic. Ale w praktyce rzadko zdarza się, abyśmy na stronie ograniczali się wyłącznie do zadeklarowania 1-2 zmiennych związanych z DOM lub BOM. Przy większej ilości takich deklaracji zastosowanie parametrów z przypisaną referencją zwiększy wydajność kodu.

Jest to bezpośrednio związane z tematyką tzw. łańcucha prototypów w JavaScript. Jeśli w naszej funkcji użylibyśmy zapisu:

let width = window.innerWidth;

to w pierwszej kolejności wyszukiwana jest zmienna window w zasięgu naszej funkcji natychmiastowej. Nie udaje się jej znaleźć, w związku z czym przechodzimy w górę łańcucha protytype i docieramy aż do obiektu globalnego window.

Cofnijmy się zatem do wcześniejszego przykładu z użyciem referencji zapisanej w parametrze „win”. W tym wypadku parametr win „wchodzi” do funkcji natychmiastowej z zapamiętanym odniesieniem do globalnego obiektu window, więc w czasie przypisywania wartości nowej zmiennej width nie ma potrzeby wychodzenia po za zasięg funkcji w celu stopniowego przeszukiwania łańcucha prototypów. Ponad to wzorzec ten pozwala nam stosować krótsze formy dla zapisu obiektów window, document, location itp.

Nazwane funkcje natychmiastowe

Na zakończenie wspomnę tylko, że natychmiastowe wyrażenia funkcyjne wcale nie muszą być pisane w formie funkcji anonimowych. Bez problemu można im nadać nazwę jak każdej innej funkcji w języku JavaScript.

(function myFunc(){
    //kod funkcji natychmiastowej
    myFunc(); //wywołanie rekurencyjne
})();

Zastosowanie nazwanej funkcji może być przydatne gdybyśmy potrzebowali zastosować odwołania rekurencyjne, choć jest to rzadko spotykane w przypadku funkcji natychmiastowych. Łatwo w tym wypadku o przepełnienie stosu wywołań, należy więc dokładnie zabezpieczyć się i kontrolować poziom rekurencji.

Jeśli zajdzie taka potrzeba to warto cofnąć się krok wstecz i ponownie przeanalizować problem oraz nasz kod i upewnić się, że na pewno rekurencja w funkcji natychmiastowej będzie dobrym rozwiązaniem.