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); //Jola

W 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:


var x = "Jola";

function fn1() {
    var a = "Ala";
    console.log(a); //Ala
    console.log(x); //Jola
}

function fn2() {
    var a = "Ola";
    console.log(a); //Ola
    console.log(x); //Jola
}

console.log(a); //błąd bo zmienna a jest dostępna tylko w funkcji
console.log(x); //Jola

Takie zamknięte środowisko może mieć spokojnie swoje zmienne, które mogą nazywać się tak samo jak zmienne ze środowiska zewnętrznego:


var x = 1;

function show() {
    //deklarujemy zmienną x dostępną tylko w tej funkcji
    //zbieżność nazw z zewnętrzną zmienną x nie przeszkadza, bo to nie są te same zmienne
    var x = 2;
    console.log(x); //2
}

show();
console.log(x); //1 - jesteśmy poza funkcją

let x = 1;

{
    let x = 2;
    console.log(x); //2
}

console.log(x); //1

W przypadku funkcji zmiennymi lokalnymi stają się też parametry.


const a = 10;
const b = 20;

function myF(a, b) {
    console.log(a, b); //"Ala", "Ola"
}

myF("Ala", "Ola");
console.log(a, b); //10, 20

Zawsze przy tworzeniu nowych zmiennych używaj var/let/const. W przeciwnym razie możemy nieumyślnie nadpisać już istniejącą zmienną:


var sum = 1;

function show(a, b) {
    //funkcja show zaczyna definiować swoje zmienne.
    //musi zadeklarować zmienne parametrów a i b
    //czy poniżej musi zdefiniować nową zmienną sum?
    //Nie, ponieważ poniżej nie ma deklaracji nowej zmiennej
    //(brakuje słowa var, let lub const)
    //funkcja więc odwołuje się tutaj do zmiennej z zewnątrz

    sum = a + b;
    console.log(sum); //wypisze 5
}

show(2, 3); //5
console.log(sum); //5 a powinno być 1

let sum = 1;
{
    sum = 2 + 3;
    console.log(sum); //5
}
console.log(sum); //5

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.

zasięg zmiennych

var lv0 = 0;

function fn1() {
    var lv1 = 1;
    console.log(lv0); //0
    console.log(lv2); //błąd - nie ma dostępu do zmiennych w wewnętrznej funkcji

    function fn2() {
        var lv2 = 2;
        console.log(lv0, lv1, lv2); //0, 1, 2
    }
}

console.log(lv1, lv2); //błąd - zewnętrzne środowisko nie ma dostępu

let lv0 = 0;

{
    let lv1 = 1;
    console.log(lv0); //0
    console.log(lv2); //błąd - nie ma dostępu do zmiennych w wewnętrznym bloku

    {
        let lv2 = 2;
        var lv2B = 2;
        console.log(lv0, lv1, lv2); //0, 1, 2
    }
}

console.log(lv1, lv2); //błąd - zewnętrzne środowisko nie ma dostępu
console.log(lv2B); //uwaga - lv2B to var. Mamy tutaj dostęp, bo zmienna ta nie jest wewnątrz funkcji

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:


let a = 0;

function myF() {
    a++; //zmienna globalna
    console.log(`a: ${a}`);
}

myF(); //a: 1
myF(); //a: 2
myF(); //a: 3
myF(); //a: 4

Za każdym razem gdy odpalamy nasza funkcję myF(), zwiększamy zmienną braną z zewnątrz.

Dodajmy do naszego przykładu dodatkową zmienną lokalną:


let a = 0;

function myF() {
    let b = 0;
    a++;
    b++;
    console.log(`a: ${a}, b: ${b}`);
}

myF(); //a: 1, b: 1
myF(); //a: 2, b: 1
myF(); //a: 3, b: 1
myF(); //a: 4, b: 1

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:


function firstFn() {
    let a = 0;

    function myF() {
        let b = 0;
        a++;
        b++;
        console.log(`a: ${a}, b: ${b}`);
    }

    myF(); //a: 1, b: 1
    myF(); //a: 2, b: 1
    myF(); //a: 3, b: 1
    myF(); //a: 4, b: 1
}

firstFn();

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:


function firstFn() {
    let a = 0;

    return function() {
        a++;
        console.log(a);
    }
}

const c = firstFn(); //podstawiam wynik funkcji firstFn czyli funkcję z linii 4
c(); //1
c(); //2
c(); //3
c(); //4

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:


function mySafeArea() {
    let x = 100;

    const show = () => {
        console.log(`x: ${x}`);
    }

    const setX = (nr) => {
        x = nr
    }

    //poniżej zwracany obiekt, który ma dostęp do powyższych zmiennych i metod
    //dzięki którym może te zmienne modyfikować
    return {
        setX,
        show
    }
}

const ob = mySafeArea();
ob.x = 150; //nie mogę tak ustawić zmiennej bo nie mam do niej dostępu z zewnątrz
ob.show(); //x: 100
ob.setX(150);
ob.show(); //x: 150

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.


(function() {
    ...
})();

Aby zrozumieć powyższy zapis, stwórzmy proste wyrażenie funkcyjne:


const fn1 = function() {...}

const fn2 = function(a) {
    console.log(a);
}

Żeby teraz wywołać powyższe funkcje, musimy podać ich nazwy, za którymi wstawimy parę nawiasów:


fn1();

fn2("ala");

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ć:


function() { ... }(); //zwróci błąd składni

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:


2 + 2.toFixed() //zwróci błąd
(2 + 2).toFixed() //na początku wykonaj równanie, potem toFixed()

Podobnie do powyższego równania wystarczy zapis funkcji objąć nawiasami:


(function() { ... })();

(function(a) {
    console.log(a)
})("ala");

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.


    (function() {...}()); //nawiasy w środku
    

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:


{
    let a = 20;
    var b = 10;
}
console.log(a); //błąd - nie mamy dostępu
console.log(b); //10

Jeżeli chcemy ograniczyć zasięg var, musimy skorzystać z funkcji:


(function() {
    let a = 20;
    var b = 10;
})();

console.log(a); //błąd - nie mamy dostępu
console.log(b); //błąd - nie ma dostępu

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.


for (i=0; i<10; i++) {
    console.log(i);
}

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:


for (i=0; i<10; i++) {
    console.log(i);
}

console.log(i); //10
console.log(window.i); //10

Spójrzmy na jeszcze jeden przykład:


<button>przycisk 1</button>
<button>przycisk 2</button>
<button>przycisk 3</button>
<button>przycisk 4</button>

//pobieram "tablicę" przycisków
const buttons = document.querySelectorAll("button");

for (el of buttons) {
    //dodaję im kliknięcie, które pokaże ich wewnętrzny tekst
    el.addEventListener("click", () => {
        console.log(el.innerText);
        el.style.background = "dodgerblue";
    });
}

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.


const buttons = document.querySelectorAll("button"); //pobieram "tablicę" przybisków

for (let el of buttons) {
    //dodaję im kliknięcie, które pokaże ich wewnętrzny tekst
    el.addEventListener("click", () => {
        console.log(el.innerText);
        el.style.background = "dodgerblue";
    });
}

Ale uwaga. Jeżeli kiedykolwiek będziesz musiał napisać podobny kod z wykorzystaniem starej składni, to nawet użycie słowa kluczowego nie rozwiąże problemu:


const buttons = document.querySelectorAll("button");

for (var el of buttons) {
    el.addEventListener("click", function(e) {
        console.log(el.innerText);
        el.style.background = "dodgerblue";
    });
}

console.log(el.innerText); //przycisk 4

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:


const buttons = document.querySelectorAll("button");

for (var el of buttons) {
    (function(btn) { //działam na zmiennej lokalnej btn która wskazuje na zmienną el
        btn.addEventListener("click", function(e) {
            console.log(btn.innerText);
            btn.style.background = "dodgerblue";
        });
    })(el);
}

console.log(el.innerText); //przycisk 4

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:


function show(a) {
   console.log(a)
}

show(1); //przekazaliśmy numer
show("Ala"); //przekazaliśmy tekst
show({a : 2}); //przekazaliśmy obiekt
show([1,2,3]); //przekazaliśmy tablicę

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):


[1,2,1,4].sort((a, b) => {
    return a - b
})

[1,2,3,4].forEach(el => {
    console.log(el);
});

element.addEventListener("click", () => {
    alert("klik");
});

Ale nic nie stoi za przeszkodą by używać referencji:


function sortFn(a, b) {
    return a - b;
}

function printText(el) {
    console.log(el);
}

function klik() {
    alert("klik");
}

[1,2,1,4].sort(sortFn);

[1,2,3,4].forEach(printText);

element.addEventListener("click", klik);

Mechanizm funkcji zwrotnych możemy też stosować w przypadku naszych własnych funkcji:


function myF(fn) {
    //pod fn trafia poniższa anonimowa funkcja
    //no to ją wywołajmy...
    fn();
}

myF(() => {
    console.log("...");
});

function randomBetween(min, max, fn) {
    const nr = Math.floor(Math.random()*(max-min+1)+min);
    fn(nr);
}

randomBetween(10, 20, res => {
    console.log("Losowa liczba to: " + res);
});

function sumTable(tab, fn) {
    let sum = 0;
    for (let i=0; i<tab.length; i++) {
        sum += tab[i];
    }
    fn(sum);
}

sumTable([1,2,3,4], res => {
    console.log("Suma liczb w tablicy to: " + res);
});

Jak wiemy, funkcje mogą przyjmować atrybuty. Nasza funkcja, którą przekazujemy do parametru także:


function myF(fn) {
    const text = "Ala"
    fn(text); //pod poniższe "a" trafi tekst "Ala"
}

myF(a => {
    console.log(a + " ma kota") //Ala ma kota
})

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.


function myFn() {
    console.log("test");
    myFn();
}

myFn();

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:


function fibonacci(n) {
    if (n===1) {
        return [0, 1];
    } else {
        const arr = fibonacci(n - 1);
        arr.push(arr[arr.length - 1] + arr[arr.length - 2]);
        return arr;
    }
}

console.log(fibonacci(10)); //[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

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().


function loop() {
    for (let i=0; i<10000; i++) {
        console.log(i);
    }

    setTimeout(() => loop(), 2000); //po 2 sekundach ponownie odpalam loop
}

loop();

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:


const arr = [ 1, 2, 3, [ 4, 5, 6, [7, 8], [9, 10, [11, 12] ] ] ];

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:


const arr = [ 1, 2, 3, [ 4, 5, 6, [7, 8], [9, 10, [11, 12] ] ] ];

function sumTab(tab) {
    let sum = 0;
    for (let i=0; i<tab.length; i++) {
        if (Array.isArray(tab[i])) {
            sum += sumTab(tab[i]);
        } else {
            sum += tab[i];
        }
    }
    return sum;
}

console.log( sumTab(arr) ); //78

Bardzo podobne w działaniu będzie tworzenie drzewa katalogów na stronie. Zamiast pojedynczych liczb w tablicy będziemy mieli pliki, a zamiast zagnieżdżonych tablic katalogi z innymi plikami. Realny przykład opisałem na swoim blogu.

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.