Funkcje i zmienne - tematy dodatkowe
Poniżej zajmiemy się dodatkowymi tematami związanymi z działaniem funkcji.
Zasięg zmiennych
Zmienne dzielimy na globalne i lokalne. Do tych pierwszych dostęp ma cały skrypt. Te drugie - jak sama nazwa wskazuje - są lokalne.
Podczas pisania naszych skryptów powinniśmy starać się używać jak najmniej zmiennych globalnych, ponieważ są narażone na potencjalne niepożądane zmiany. Oczywiście wszystko zależy od zaawansowania danego skryptu.
W przypadku const i let zasięg zmiennych jest blokowy, co oznacza "od klamry do klamry".
let x = "Jola";
{
let a = "Ala";
console.log(a); //Ala
console.log(x); //Jola
}
{
let a = "Ola"; //zmienna lokalna w tym bloku
console.log(a); //Ola
console.log(x); //Jola
}
console.log(a); //error - nie ma takiej zmiennej
console.log(x); //JolaW przypadku zmiennych deklarowanych za pomocą var ich zasięg jest funkcyjny. Zasada jest podobna do const i let, z tym, że tutaj zasięg definiuje ciało danej funkcji:
Takie zamknięte środowisko może mieć spokojnie swoje zmienne, które mogą nazywać się tak samo jak zmienne ze środowiska zewnętrznego:
W przypadku funkcji zmiennymi lokalnymi stają się też parametry.
Zawsze przy tworzeniu nowych zmiennych używaj var/let/const. W przeciwnym razie możemy nieumyślnie nadpisać już istniejącą zmienną:
Zarówno funkcje jak i bloki mogą być spokojnie zagnieżdżone w innych funkcjach i blokach.
Działać tu będzie ten sam mechanizm co powyżej. Funkcje/bloki mają dostęp do swoich zmiennych i zmiennych z zewnątrz. Zewnętrzne środowisko nie ma dostępu do zmiennych w zagnieżdżonych blokach i funkcjach. Tworzy się więc pewna zagnieżdżona hierarchia.

Domknięcia - closures
Gdy funkcja zaczyna działać, tworzy swoje środowisko ze swoimi zmiennymi. Tymi zmiennymi są zmienne lokalne (zadeklarowane za pomocą var/let/const) oraz parametry funkcji. Gdy znajdzie odwołanie do zmiennych z zewnątrz, pobierze je stamtąd. Ok. To przejdźmy do mini przykładu:
Za każdym razem gdy odpalamy nasza funkcję myF(), zwiększamy zmienną braną z zewnątrz.
Dodajmy do naszego przykładu dodatkową zmienną lokalną:
Za każdym wywołaniem naszej funkcji zmienna lokalna tworzona jest na nowo, natomiast zmienna globalna jest brana z zewnątrz, dzięki czemu między kolejnymi odpaleniami naszej funkcji trzyma swój stan. Zewnętrzne środowisko dla naszej funkcji staje się więc swoistym schowkiem.
Spróbujmy ten sam manewr co powyższy przeprowadzić na funkcji zagnieżdżonej o jeden poziom:
Zasada jak widać nic się nie zmienia - zmienił się tylko zasięg do którego funkcja myF() ma dostęp.
Funkcje mogą zwracać dowolne rzeczy - w tym inne funkcje. Dla takich funkcji także możemy wykorzystać "zewnętrzne środowisko" do zapamiętywania stanu między kolejnymi wywołaniami:
Dzięki wykorzystaniu zewnętrznego środowiska (którym dla naszej zwróconej funkcji jest funkcja firstFn() i zewnętrzne) nasza funkcja c() może zapamiętać swój stan między kolejnymi odpaleniami.
Dodatkowo zwrócona funkcja ma dostęp do zasięgu funkcji firstFn() i jej zewnętrznego zasięgu. Równocześnie globalny zasięg nie ma dostępu do ciała funkcji firstFn(). Oznacza to, że zwróconej funkcji zasięg firstFn() staje się swoistym zamkniętym schowkiem, do którego ma dostęp tylko zwrócona funkcja.
Zasada ta tyczy się nie tylko funkcji, ale także obiektów:
Technika ta ma zastosowanie w momencie, gdy chcemy coś chronić przed zewnętrznym środowiskiem.
IIFE
IIFE (Immediately-invoked function expression) - czyli samo wywołujące się wyrażenie funkcyjne to wzorzec funkcji, która sama się wywołuje.
Aby zrozumieć powyższy zapis, stwórzmy proste wyrażenie funkcyjne:
Żeby teraz wywołać powyższe funkcje, musimy podać ich nazwy, za którymi wstawimy parę nawiasów:
Częstokroć jednak wcale nie będziemy potrzebować nazwy funkcji, bo wewnętrzny kod chcielibyśmy wykonać tylko jeden raz i to od razu. Czyli po definicji funkcji chcielibyśmy od razu ją wywołać:
Powyższy kod zwróci błąd. Aby to naprawić, wystarczy skorzystać z zasad matematyki, gdzie nawiasami okrywamy część równania, która powinna się wykonać w pierwszej kolejności:
Podobnie do powyższego równania wystarczy zapis funkcji objąć nawiasami:
I tak właśnie powstał nasz wzorzec samo wywołującej się anonimowej funkcji:
Alternatywnym zapisem dla powyższego jest wzór zalecany przez Douglasa Crockforda - jednego z guru JavaScript.
Czemu jest on zalecany? Rozchodzi się o psie jajka, ale to tylko sprawy estetyczne.
A do czego to może być wykorzystane? Zakres let i const w przeciwieństwie do var mają zasięg blokowy:
Jeżeli chcemy ograniczyć zasięg var, musimy skorzystać z funkcji:
W przeszłości - gdy używało się głównie var, stosowanie IIFE było dość powszechną metodą ograniczania zasięgu. W dzisiejszych czasach wielu programistów zabezpiecza tak całe swoje kody.
Gdy zaczniemy dzielić nasz kod na oddzielne pliki (moduły) nasz kod będzie automatycznie okrywany podobnym zapisem (1).
Problem ze zmiennymi
W rozdziale o zmiennych powiedziałem Tobie, żebyś zawsze tworzył zmienne używając słowa kluczowego.
Powyższy kod - mimo, że bardzo krótki - zawiera błąd. Stworzyliśmy zmienną i, ale nie używając słowa kluczowego. Pętla się wykona, natomiast rezultat może być dla nas nieprzewidywalny. Nieumyślnie stworzyliśmy zmienną globalną, która z automatu przypisana jest do obiektu window:
Spójrzmy na jeszcze jeden przykład:
Klikając na dowolny z przycisków będziesz zmieniał tylko ostatni. Czemu tak się dzieje? Znowu zawinił ten sam błąd. Robiąc pętlę po przyciskach skorzystałem ze zmiennej el. Tak samo jak w pierwszym przykładzie nie utworzyłem jej korzystając ze słowa kluczowego let/var, więc automatycznie stała się globalną. Gdybyśmy wypisali ją po zakończeniu pętli, efekt byłby podobny jak w przypadku pierwszego przykładu - wskazywała by na ostatni element tablicy.
Po zakończeniu pętli my jako użytkownicy klikamy na przycisk, który uruchamia funkcję odnoszącą się do tej zmiennej globalnej.
Wracamy więc do kluczowej zasady: zawsze twórz zmienne używając słów kluczowych.
Ale uwaga. Jeżeli kiedykolwiek będziesz musiał napisać podobny kod z wykorzystaniem starej składni, wtedy nawet użycie słowa kluczowego nie rozwiąże problemu:
Zasięg zmiennych var określa ciało funkcji a nie blok, stąd zmienna el "wypadła" poza pętlę stając się zmienną globalną. Żeby to naprawić moglibyśmy skorzystać z IIFE:
W dzisiejszych czasach traktuj to raczej jako ciekawostkę, chociaż któż wie co tam spotakasz na swojej ścieżce kariery...
Funkcje zwrotne
Do parametrów funkcji możemy przekazywać dowolny typ danych:
Skoro możemy przekazać wszystko, to także i funkcję, którą wewnątrz naszej funkcji możemy wywołać.
Działanie takie bardzo często będziemy stosować podczas pisania kodu. Poniżej przykład zastosowania tego mechanizmu dla sort(), forEach(), map() i addEventListener().
Dość często przy takich działaniach używamy tak zwanych funkcji anonimowych (funkcji które nie mają własnej nazwy):
Ale nic nie stoi za przeszkodą by używać referencji:
Mechanizm funkcji zwrotnych możemy też stosować w przypadku naszych własnych funkcji:
Jak wiemy, funkcja może przyjmować atrybuty. Nasza funkcja, którą przekazujemy do parametru także:
Do tematu funkcji zwrotnych jeszcze wrócimy w jednym z kolejnych rozdziałów.
Rekurencja
Funkcja rekurencyjna to taka funkcja, która wywołuje sama siebie.
Przykładem zastosowania rekurencji może być np. rysowanie fraktali czy wyliczanie ciągu Fibonacciego, gdzie każda kolejna liczba w tym ciągu to suma dwóch poprzednich:
Naszym celem zazwyczaj nie jest rysowanie ani fraktali, ani wyliczanie ciągów matematycznych. Porozmawiajmy więc o bardziej realnych zastosowaniach.
Jedno z zastosowań rekurencji spotyka się w przypadku pracy z interwałami. Gdy odpalany cyklicznie kod, który potencjalnie może być zbyt długo wykonywany, zamiast setInterval lepiej zastosować rekurencję i setTimeout().
Dokładnie ten przykład opisałem w rozdziale o interwałach.
Innym przykładem może być spacerowanie po strukturach zagnieżdżonych.
Wyobraź sobie, że musisz zsumować poniższą tablicę, która może mieć dowolną liczbę poziomów zagnieżdżonych tablic z elementami:
Równocześnie w ramach treningu (albo i wstecznej kompatybilności) nie możesz użyć żadnej z dostępnych metod takich jak flat() czy reduce(). Pozostaje więc rekurencja:
Last updated