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.

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 cost 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 używać słów kluczowych var/let/const. W przeciwnym razie możemy nieumyślnie nadpisać zmienną globalną:


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.


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ętrznej funkcji

    {
        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 nie ogranicza jej ciało 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ę wieć swoistym schowkiem.

Spróbujmy ten sam manewr co powyższy przeprowadzić na funkcji zagnieżdżonej o jeden poziom:


function loremFn() {
    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
}

loremFn();

Zasada jak widać nic się nie zmienia.

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 counter() {
    let a = 0;

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

const c = counter();
c(); //1
c(); //2
c(); //3
c(); //4

Dzięki wykorzystaniu zewnętrznego środowiska (którym dla naszej zwróconej funkcji jest funkcja counter()) nasza funkcja c() może zapamiętać swój stan między kolejnymi odpaleniami.

Więcej na ten temat możesz dowiedzieć się na stronie https://developer.mozilla.org/pl/docs/Web/JavaScript/Domkniecia

Samo wywołująca się funkcja - IIFE

W JavaScript istnieje pewien wzorzec anonimowej funkcji, która od razu sama się wywołuje - tak zwany Immediately-invoked function expression (IIFE).

Aby go zrozumieć, stwórzmy proste wyrażenie funkcyjne:


const fn1 = function() {...}

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

Żeby teraz wywołać powyższą funkcję, musimy podać jej nazwę, za którą 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

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:


(function() {
    console.log('Jakiś tekst'); //wywoła się od razu
})();

//jest praktycznie równoznaczne z

function fn1() {
    console.log('Jakiś tekst');
}
fn1();

Alternatywnym zapisem dla powyższego jest wzór zalecany przez Douglasa Crockforda - jednego z guru JavaScript.


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

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.

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

Czym są funkcje zwrotne (tak zwane callback)?

Zacznijmy od podstaw. Do parametrów funkcji możemy przekazywać wszystko:


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

myF(1); //przekazaliśmy numer
myF("Ala"); //przekazaliśmy tekst
myF({a : 2}); //przekazaliśmy obiekt
myF([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()


[1,2,1,4].sort(function(a, b) {
    ...
})

[1,2,3,4].forEach(function(el) {
    ...
});

element.addEventListener('click', function() {
    ...
});

Mechanizm ten 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(function() {
    console.log('...');
});

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

myF(function(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], function(res) {
    console.log('Suma liczb w tablicy to: ' + res);
});

Poza przekazywaniem funkcji anonimowych możemy też przekazywać funkcje przez ich nazwę (tak zwana referencja):


function myF(fn) {
    fn();
}

function otherFn() {
    console.log(...);
}

//przekazujemy funkcję po nazwie
myF(otherFn)

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(function(a) {
    console.log(a + " ma kota") //Ala ma kota
})

W powyższym kodzie przekazujemy funkcję, która wymaga jednego atrybutu. Funkcja ta jest odpalana w funkcji myF, która przekazuje do jej parametru tekst.

Funkcje zwrotne używa się bardzo często szczególnie w środowisku Node lub pracy z synchronicznym kodem.

Trening czyni mistrza

Jeżeli chcesz sobie potrenować zdobytą wiedzę, to zadania do tego rozdziału znajdują się w w repozytorium pod adresem: https://github.com/kurs-javascript/js-podstawy w katalogu 4-funkcje, przy czym śmiało możesz robić zadania z całego repozytorium.

Dowiedz się więcej na ten temat tutaj.

A może pasuje ci ściągnąć zadania do wszystkich rozdziałów na raz? Jeżeli tak, to skorzystaj z repozytorium pod adresem https://github.com/kurs-javascript/js-all.

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.