Funkcje - 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

Ważne by zawsze przy tworzeniu nowych zmiennych zawsze używać słów kluczowych 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 a powinno być 1

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.

Wróćmy teraz na chwile do powyższej grafiki o zasięgu zmiennych. Według grafiki nasza nowa - jako że jest najgłębiej zagnieżdżona - ma dostęp do zasięgu funkcji firstFn() i globalnego zasięgu. Równocześnie globalny zasięg nie ma dostępu do ciała funkcji firstFn(). Co to dla nas oznacza? Że dla zwróconej funkcji (którą podstawiliśmy pod c) zasięg firstFn() staje się swoistym zamkniętym schowkiem, do którego ma dostęp tylko zwrócona funkcja.

Technika ta ma zastosowanie w momencie, gdy chcemy coś chronić przed zewnętrznym środowiskiem.

Więcej na te tematy możesz dowiedzieć się na stronie https://developer.mozilla.org/pl/docs/Web/JavaScript/Domkniecia lub https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

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.

No dobrze, a czemu to wszystkiemu ma służyć?
Jak wiemy, 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.

Ciekawostka jest taka, że IIFE dalej jest dość często stosowany, ponieważ w przypadku modułów nasz kod jest za naszymi plecami okrywany właśnie podobnym zapisem.

Zakończmy ten wywód klasycznym przykładem użycia IIFE. Przypuśćmy, że mamy kod:


var arr = [];

for (var i=0; i<3; i++) {
    arr[i] = function() {
        console.log(i);
    }
}

arr[0](); //3 ????
arr[1](); //3 ????
arr[2](); //3 ????

Czemu powyższy przykład wypisuje za każdym razem 3 - przecież powinien kolejne liczby.

Pamiętaj, że kod funkcji po napisaniu jest jeszcze nieaktywny.
W momencie odpalenia funkcji po pierwsze tworzy ona swoje zmienne lokalne (parametry oraz zmienne deklarowane przez var/let/const) oraz pobiera zmienne globalne jeżeli w jej kodzie są do takowych odwołania. W powyższym przykładzie funkcje nie mają zmiennych lokalnych, a tylko biorą zmienną i z zewnątrz. A ile wynosi zmienna i w momencie wywołania funkcji arr[0]? Pamiętaj, że zmienne deklarowane przez var nie mają zasięgu blokowego, więc zmienna i wyskoczyła poza pętlę. Tak więc w momencie wywołania funkcji arr[0] zmienna i jest równa 3.

Aby to naprawić trzeba jakoś wymusić na funkcji by stworzyła swoje prywatne zmienne:


var arr = [];
for (var i=0; i<3; i++) {
    (function(i) {
        arr[i] = function() {
            console.log(i);
        };
    })(i);
}

arr[0](); //0
arr[1](); //1
arr[2](); //2

//lub
var arr = [];
for (var i=0; i<3; i++) {
    (function() {
        var j = i;
        arr[j] = function() {
            console.log(j);
        };
    })();
}

arr[0](); //0
arr[1](); //1
arr[2](); //2
...albo zacząć używać nowszych deklaracji:

const arr = [];
for (let i=0; i<3; i++) {
    arr[i] = function() {
        console.log(i);
    }
}

arr[0](); //0

Powyższy wywód może wydawać się czysto teoretyczny, ale w praktyce dość często pojawia się na forach w nieco innej postaci:


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

for (var i=0; i<buttons.length; i++) {
    buttons[i].addEventListener("click", function() {
        console.log(i);
    });
}

Zasada działania ta sama...

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łow.

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.

Menu