Funkcje

Gdy mąż słyszy od żony idź do sklepu, kup ziemniaki, wówczas zaczyna wykonywać pewien zbiór czynności.
Za każdym razem, gdy usłyszy idź do sklepu, kup ziemniaki, wykona ten sam zbiór czynności (i tak ma być).
W Javascript czynności (fragmenty kodu) objęte klamrami do których możemy odwołać się poprzez nazwę to właśnie funkcje.

Ogólna deklaracja funkcji ma następującą postać:


function nazwaFunkcji() {
    console.log('Hej jestem fajnym tekstem');
}

//Po stworzeniu funkcji wystarczy ją wywołać poprzez podanie jej nazwy i nawiasów:
//3x wywolujemy powyższą funkcję
nazwaFunkcji();
nazwaFunkcji();
nazwaFunkcji();

No dobrze, ale po co to wszystko?

Spójrz na poniższy przykład:


let str = '';

//generuję linię
str = '';
for (let x=0; i<10; i++) {
    str += '-';
}
console.log(str);

let x = 20;
console.log("20 + 20 = ", x + 20);

str = '';
for (let x=0; i<10; i++) {
    str += '-';
}
console.log(str);

console.log("Ala ma kota");
console.log("a kot ma Alę");

str = '';
for (let x=0; i<10; i++) {
    str += '-';
}
console.log(str);

Jak widzisz kilka razy powtarzamy linijki console.log, które tworzą kreskę. To prosty przykład. Taki skrypt może zajmować o wiele, wiele więcej ekranów kodu.
Aż prosi się, by powtarzany kod wrzucić do naszej funkcji, a potem się do niej po prostu odwoływać:


function line() {
    let str = ''
    for (let x=0; i<10; i++) {
        str += '-';
    }
    console.log(str);
}

line();

let x = 20;
console.log("20 + 20 = ", x + 20);

line();

console.log("Ala ma kota");
console.log("a kot ma Alę");

line();

Nasz kod się uprościł. Jeżeli teraz będziemy chcieli zmodyfikować nasz wzorek, zrobimy to tylko w jednym miejscu, a nie w wielu.
Zadziała tutaj zasada reużywalności kodu - podobna do tej mówiącej o trzymaniu JavaScript i CSS w oddzielnych plikach. Dzięki temu zmianę literówki zrobisz w jednym miejscu, a nie na każdej podstronie i każdym miejscu gdzie używasz danego kodu.

Funkcję można traktować jak swego rodzaju "klocek" (...), który w każdej chwili możemy użyć. Wyobraź sobie, że zbudujesz sobie zestaw takich klocków, a potem w swoim kodzie będziesz je po prostu używał.
Swoje funkcje-klocki możesz spokojnie trzymać w oddzielnym pliku, który potem będziesz dołączać do wybranych stron, dzięki czemu będziesz miał dostęp do swoich funkcji. Na tej właśnie zasadzie używa się znanych bibliotek - np. jQuery, Mootols czy podobnych, które są niczym innym jak zbiorem funkcji, które ktoś dla nas napisał.

Zresztą - już nie raz w tym kursie używałeś podobnych klocków. Nie swoich, a napisanych przez innych.


Math.random();

[].push('lorem');

Math.max(1,2,3);

"ala ma kota".toUpperCase();

"marcin@gmail.com".indexOf('@');

"kot i pies".substr(1);

Widzisz te nawiasy na końcu każdej linii? Poprzedza je nazwa funkcji. Autorzy JavaScript przygotowali dla nas zestaw jakiś funkcji.

Zauważ, że w niektórych przypadkach odpalając daną funkcję (np. push(), max(), subsrt()) mogliśmy między nawiasy wstawić dodatkowe dane, które były potem przetwarzane przez taką funkcję. Te dane zwą się parametrami.

Parametry funkcji

Nasz powyższy klocek działa jak należy, ale jest sztywny jak diabli. Za każdym razem wypisuje ten sam tekst. Pójdźmy o krok dalej.
Częstokroć konieczne będzie przekazanie do funkcji określonych danych, które następnie zostaną przez nią przetworzone.
Dane takie zwane parametrami (lub atrybutami) przekazuje się wypisując je między nawiasami występującymi po nazwie funkcji:


function sum(a, b) {
    console.log(a + b);
}

sum(2, 3); //5

W powyższym przykładzie stworzyliśmy 2 miejsca na parametry - zmienne a i b. Przy wywołaniu funkcji wstawiamy w te miejsca konkretne wartości - 2 i 3.

Poniżej inne przykłady:


function writeText(name) {
    console.log(name + " ma kota");
}


writeText("Ala"); //Ala ma kota
writeText("Marysia"); //Marysia ma kota

function line(char, nr) {
    let str = '';
    for (let i=0; i<nr; i++) {
        str += char;
    }
    console.log(str);
}

line("x", 5); //xxxxx
line(".", 10); //..........
line("=.", 3); //=.=.=.

Jeżeli nasza funkcja wymaga parametrów, a dodatkowo nie została specjalnie napisana, wtedy wywołując ją musimy podać dla tych parametrów wartości. Jeżeli ich nie podamy zostaną użyte dla nich wartości undefined:


function writeText(name, age) {
    console.log(name + " ma kota, który ma " + age + "lat");
}

writeText("Ala", 5); //Ala ma kota, który ma 5 lat
writeText("Marysia"); //Marysia ma kota, który ma undefined lat
writeText(); //undefined ma kota, który ma undefined lat

Spójrz na poniższe przykłady. Zauważasz coś?


//korzystamy z funkcji random() i floor() by wypisać liczbę spomiędzy 1-10

function randomBetween(min, max) {
    console.log(  Math.floor(Math.random() * (max-min+1) + min)  );
}

randomBetween(1, 10);

We wnętrzu naszy funkcji korzystamy z innych - gotowych funkcji. Spokojnie moglibyśmy też wykorzystać nasze własne funkcje.


function fixName(name) {
    console.log(name.charAt(0).toUpperCase() + name.substr(1));
}

fixName("marcin"); //Marcin

function writeText(name) {
    console.log(fixName(name) + " ma kota");
}

writeText('kasia'); //Kasia ma kota

arguments

Javascript nie wymaga od nas, abyśmy przekazywali do funkcji wymaganą ilość parametrów.
Jeżeli nie zakładamy konkretnej liczby parametrów dla funkcji, możemy skorzystać z arguments - obiektu podobnego do tablicy, dzięki któremu możliwe jest pobieranie wszystkich przekazanych do funkcji wartości.


function sum() {
    console.log(arguments);
}

sum(); //[] (zobacz dokładniej co wyszło w konsoli, bo wynik nie jest dokładnie taki)
sum(1,2,3,4); //[1,2,3,4]
sum("ala", "ma", "kota"); //["ala", "ma", "kota"]

Obiekt ten przypomina tablicę, ale tak naprawdę nią nie jest. Oznacza to, że nie możemy na nim wykonywać metod przeznaczonych dla tablic np. forEach

Na szczęście możemy po nim robić klasyczną pętlę for:


function superSum() {
    let result = 0;
    for (let i=0; i<arguments.length; i++)
        result += arguments[i];
    }
    console.log(result);
}

superSum(1, 2, 3, 4); //10

function superSum() {
    for (let i=0; i<arguments.length; i++)
        const arg = arguments[i];
        console.log(arg.charAt(0).toUpperCase() + arg.slice(1));
    }
}

superSum("ala", "basia", "kasia"); //Ala, Basia, Kasia

Możemy to wykorzystać np. do sprawdzania ilości parametrów:


function calculate() {
    if (arguments.length < 2) {
        console.warn('Błąd: Musisz podać minimum 2 liczby');
    } else {
        let result = 0;
        for (let i=0; i<arguments.length; i++) {
            result += arguments[i];
        }
        console.log(result);
    }
}

calculate(); //Wypisze "Błąd: Musisz podać minimum 2 liczby"
calculate(3); //Wypisze "Błąd: Musisz podać minimum 2 liczby"
calculate(4, 5, 6); //Wypisze 15

lub na przykład do sumowania przekazanych liczb:


function sumNumbers() {
    let sum = 0;
    for (let i=0; i<arguments.length; i++) {
        sum += arguments[i];
    }
    console.log(sum);
}

sumNumbers(1,2,3,4); //10

W dzisiejszych czasach zamiast opierać się o arguments, zalecane jest używać rest. Pamiętaj jednak, że taki kod na chwilę obecną powinien być transpilowany na starszą wersję JavaScript, bo nie wszystkie przeglądarki obsługują parametr rest.

Domyślne parametry

Co jeżeli funkcja wymaga parametru, a ktoś go nie poda przy wywołaniu? Wtedy pod dany parametr trafi "brak wartości" czyli undefined:


function print(txt) {
    console.log("Twój tekst to " + txt);
}

print(); //"Twój tekst to undefined"

Jak to zabezpieczyć? Pamiętasz jak się sprawdza, czy dana zmienna istnieje?


if (typeof x === "undefined") {...}

Wystarczy użyć tej metody:


function printText(txt) {
    if (typeof txt === "undefined") {
        txt = "lorem";
    }

    console.log(txt);
}

//lub

function printText(txt) {
    txt = (typeof txt === "undefined")? "lorem" : txt;

    console.log(txt);
}

printText(); //lorem

Istnieje też krótsza metoda.


function printText(txt) {
    txt = txt || "lorem";

    console.log(txt);
}

printText(); //lorem

Trzeba mieć tutaj tylko na uwadze, że jeżeli pod txt podamy pusty ciąg znaków, lub cokolwiek co prze automatyczną konwersję zostanie zamienione na false, wtedy zostanie użyty domyślny "lorem". Nie zawsze jest to dobre rozwiązanie, dlatego polecam zostać przy dłuższym zapisie.


function printText(txt) {
    txt = txt || "lorem";

    console.log(txt);
}

printText(""); //lorem, a powinno być ""

W nowszej wersji JavaScript (ES6) rozwiązano to jeszcze lepiej. Więcej na ten temat dowiesz się tutaj.

Instrukcja return

W powyższych listingach nasze funkcje każdorazowo wypisywały kod w konsoli debugera. W większości przypadków nie będziemy chcieli używać debugera. Może wynik działania funkcji będziemy chcieli wstawić do przycisku na stronie, a może wypisać w okienku alert? Żadna z tych czynności nie ma nic wspólnego z konsolą debugera. Konsola jest dla nas - programistów. Dla użytkownika są inne rzeczy na stronie - buttony, html itp.

Zauważ, że jeżeli wywołasz funkcję Math.random() w wyniku dostaniesz jakiś wynik:


const nr = Math.random();
alert(nr);

Oznacza to, że funkcja random() zwróciła ci jakąś wartość. Za takie zwrócenie wartości przez funkcję odpowiedzialna jest instrukcja return.


function calculate(number1, number2) {
    const result = number1 + number2;

    return result;
}

console.log( calculate(10, 4) ); //wypisze 14

Dzięki temu, że nasza funkcja nie wypisuje wartości w konsoli, a ją zwraca, możemy ją użyć do innych celów niż tylko wypisywanie tekstów w debugerze:


function repeat(txt, nr) {
    let str = '';
    for (let i=0; i<nr; i++) {
        str += txt;
    }
    return str + ' buritto!';
}

//pobieram przycisk ze strony i wstawiam do niego tekst
//o pobieraniu elementów ze strony jeszcze się dowiesz
document.querySelector('button').innerText = repeat("tako", 3);

//lub wypisujemy to w alercie
alert(repeat('maslo', 10));

Instrukcja return nie tylko zwraca wartość, ale i definitywnie przerywa działanie danej funkcji. Po pierwszym wykonaniu return działanie funkcji zakańcza się:


function sum(a, b) {
    return a + b;
    console.log('liczę'); //nigdy nie zostanie wykonane, bo wcześniej return przerwie działanie funkcji
}

Instrukcji return może być wiele dla jednej funkcji. Zawsze jednak wykonana zostanie tylko jedna:


function isEven(number) {
    if (number % 2 === 0) {
        return true;
    } else {
        return false;
    }
}

const nr = 1;
if ( isEven(nr) ) {
    console.log('Liczba' + x + ' nie jest parzysta');
} else {
    console.log('Liczba' + x + ' jest parzysta');
}

Zauważyłeś jak w powyższym przykładzie zastosowaliśmy funkcję wraz z instrukcją "if". Jeżeli wywołujemy funkcję, w miejscu wywołania ląduje jej wynik. Czyli w powyższym przykładzie w konstrukcji if (isEven(nr)) trafia true lub false.

Poniżej to samo działanie. Funkcja zwraca wartość, którą od razu możemy wykorzystać:


function fixName(name) {
    return name.charAt(0).toUpperCase() + name.slice(1);
}

const result = fixName("piotr") + ' ' + fixName("kowalski");
console.log(result); //Piotr Kowalski

Instrukcja return może zwracać dowolną wartość. Może to być tablica:


function returnArray() {
    return [1, 2, 3] //zwracamy tablicę, którą poznasz niebawem
}

const result = returnArray();

console.log(result[0] + '|' + result[1] + '|' + result[2]); //wypisze 1|2|3

...lub obiekt:


function returnObject() {
    return {
        first: 1,
        second: 2,
        third: 3
    }
}

const result = returnObject();

console.log(result.first + '|' + result.second + '|' + result.third); //wypisze 1|2|3

lub inne funkcje, o czym przekonasz się nieco później.

Czekaj, czekaj. Nie tak prędko!
Do tego wszystkiego jeszcze dojdziemy.
Na chwilę obecną najważniejsze jest byś zapamiętał, że funkcja to zamknięty fragment kodu, który możesz wywołać z "każdego miejsca" w każdym czasie. Nie musi to być kod odpalany od razu przy pierwszym wywołaniu skryptu (tak jakbyś pisał wszystko jedno pod drugim), a później - na przykład po kliknięciu na przycisk.
Taki fragment kodu możesz potem parametryzować za pomocą atrybutów funkcji.
Dodatkowo ten fragment kodu może zwracać jakiś wynik, który bardzo łatwo można wykorzystywać przy instrukcjach sprawdzających, równaniach itp.

Zanim przejdziemy dalej, chciałbym byś zwrócił uwagę na jedną dość ważną cechę funkcji.
Spójrz na poniższy kod:


const i = Math.abs(-20);
const j = Math.pow(2, 3);
const k = Math.max(1, 2, 3);

Wywołujemy tutaj znane już nam funkcje. Zauważ, że w powyższym kodzie skupiamy się tylko na wywołaniu funkcji. Interesuje nas to, jakich parametrów wymaga dana funkcja, oraz to co ona nam zwróci.
Skąd mamy wiedzieć te rzeczy? Z opisu danych funkcji - np tego, tego i tego.

Czy jest dla nas ważne jak taka funkcja oblicza dany wynik? Może autor do wyliczenia maksymalnej liczby wykorzystał kilka ifów? A może wykorzystał jakieś sortowanie lub zastosował pętlę? Nie jest to dla nas ważne. Nas interesuje tylko to jak taki klocek użyć i co on nam zwróci. To też jest bardzo istotna cecha funkcji. Dzięki temu skupiamy się na używaniu klocków, a nie dokładnym wnikaniu jak podszedł do tematu twórca danej funkcji.

Działanie funkcji
Dla dociekliwych. Może pokusisz się o napisanie własnych alternatyw dla Math.abs(), Math.pow() i Math.max()? Funkcje te przykładowo mogły by się nazywać myAbs, myPow i myMax. Wykorzystać tutaj by trzeba pewnie jakieś if, pętlę i proste wyliczenia. Dodatkowo przy myMax przydało by się obsłużenie obiektu arguments, tak by można było podać więcej argumentów.

Wyrażenia funkcyjne

Do tej pory poznaliśmy jeden sposób deklaracji funkcji.


function myFunc() {
    ...
}
Jest to tak zwana deklaracja funkcji.

Drugi sposób zwie się wyrażeniem funkcyjnym. Funkcję od razu podstawiamy pod zmienną:


//function jako wyrażenie (expression)
const myFunction = function myFunction() {
    ...
}

Ale tutaj mała uwaga. Skoro podstawiliśmy funkcję pod zmienną, to nie potrzebuje ona nazwy:


const myFunction = function() {
    ...
}

W powyższym kodzie pod zmienną podstawiliśmy tak zwaną funkcję anonimową, czyli funkcję, która nie ma nazwy.

Wyrażenie i definicja różnią się od siebie nie tylko sposobem zapisu, ale także tym, jak taki kod jest interpretowany przez przeglądarkę.
Funkcja zadeklarowana za pomocą deklaracji jest od razu dostępna dla całego skryptu. Możemy więc odwoływać się do funkcji, która jest zadeklarowana później w kodzie. Działa tutaj mechanizm hoistingu, znany ze zmiennych. Funkcje utworzone przez deklarację, poza naszymi plecami są przenoszone na początek danego scope (zasięgu).

Przy wyrażeniu funkcyjnym mechanizm ten nie działa, a takie przedwczesne odwołanie się do funkcji jest niemożliwe. Funkcja zdefiniowana za pomocą wyrażenia musi być zadeklarowana przed jej wywołaniem:


myFunction(); //Tutaj jest ok

function myFunction() {
    console.log('...');
}

myFunction(); //Ee, ee - Błąd

const myFunction = function() {
    console.log('...');
}

W praktyce nawet jeżeli deklarujesz funkcje za pomocą definicji (czyli tak jak robiliśmy dotychczas) trzymaj się poprawnego programowania. Czyli na początku twórz funkcję i zmienne, a potem ich używaj. Nigdy na odwrót, bo psuje to logikę kodu, a i powoduje cięższe odnajdywanie się w kodzie (stąd też sporo osób woli deklarować funkcje przez wyrażenia).

Funkcje anonimowe

Jak widzisz powyżej, pod zmienną podstawiliśmy funkcję, która nie ma własnej nazwy. Taka funkcja bez nazwy zwie się funkcją anonimową. Takie funkcje bardzo często stosuje się w przypadku pracy z eventami, gdzie nie występuje nawet użycie dodatkowej zmiennej:


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

Czy na przykład przy pracy z metodami tablic:


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

[1,2,3].map(function(el) {
    return el*2;
});

Funkcję anonimową zastosujemy jeszcze w kilku innych sytuacjach - np. w eventach, callbackach czy IIFE

Zasięg zmiennych

W przypadku funkcji dochodzi nam bardzo ważne pojęcie - zasięgu zmiennych.

Do tej pory posługiwaliśmy się zmiennymi globalnymi, które były dostępne dla całego skryptu, wszystkich funkcji i w ogóle całego świata. Rozważmy prosty przykład:


let x = 10;

function sum(y) {
    x = x + y; //nie użyliśmy słowa var/let/const - nie tworzymy nowej zmiennej, a zmieniamy globalną
    return x;
}

console.log( sum(10) ); //wypisze 20
console.log( x ); //wypisze 20

Funkcja sum() nie tylko dokonuje wyliczenia sumy liczby, ale także modyfikuje naszą zmienną globalną x.

Pisząc kod funkcji powinniśmy starać się napisać go tak, by był on niezależny od środowiska zewnętrznego. Podajemy do niej jakieś parametry, funkcja je przetwarza za pomocą swojego kodu i zmiennych, po czym kończy swoje działanie zwracając jakiś wynik.
Stosując zmienne globalne nie jesteśmy w stanie tego uzyskać, bo każdorazowo zmieniamy zmienne spoza funkcji.

W Javascript możemy także korzystać także ze zmiennych lokalnych. Każda funkcja stanowi zamknięte środowisko dla zmiennych, które są deklarowane w jej ciele. Do tego środowiska (czyli to tych zmiennych) nie ma dostępu zewnętrzne środowisko:


function print() {
    let a = 20;
    console.log(a);
}

console.log(a); //blad bo zmienna a jest dostępna tylko w funkcji

Ale po co to? Takie zamknięte środowisko może mieć spokojnie swoje zmienne, które nazywają się tak samo jak zmienne ze środowiska zewnętrznego:


const k = 1;

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

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

function mySum(a, b) {
    //funkcja mySum ma w sobie 3 zmienne - a, b, result
    const result = a + b;
    return result;
}

const a = 10;
const b = 20;
const result = mySum(a, b);

Czemu w powyższych skryptach nie ma konfliktu?
Działa tutaj pewien mechanizm.
Po napisaniu kodu funkcji jest on początkowo nieaktywny. Gdy tylko wywołamy funkcję, rozpoczynając swoje działanie stworzy ona "zamknięte środowisko", w którym zacznie tworzyć zmienne, które są zadeklarowane w jej ciele (są nimi parametry funkcji i zmienne z var w ciele funkcji). Jeżeli w swoim kodzie zobaczy odwołanie do zmiennych, które nie są zadeklarowane w jej ciele, odwoła się do nich do środowiska zewnętrznego.

Poniższy przykład działa nieprawidłowo:


let sum = 1;

function show(a, b) {
    //funkcja show zaczyna definiować swoje zmienne.
    //musi zadeklarować zmienne a i b,
    //podstawiając pod nie przekazane wartości - 2, 3

    //czy poniżej musi zdefiniować nową zmienną sum?
    //Nie, bo 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

Powyższy przykład bardzo prosto naprawić:


let sum = 1;

function show(a, b) {
    let sum = a + b;
    console.log(sum); //wypisze 5
}

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

Podsumowując:

Gdy funkcja zaczyna działać, zaczyna tworzyć swoje własne zmienne (parametry funkcji i te, które są zdefiniowane w jej ciele).

Jeżeli w ciele funkcji są odwołania do zewnętrznych zmiennych, brane są one z zewnętrznego środowiska.

Funkcje mają dostęp do zmiennych z zewnątrz. ALE zewnętrzne środowisko nie ma dostępu do zmiennych wewnątrz funkcji.

Powyższe podsumowanie jest o tyle ważne, ponieważ...
Funkcje mogą być spokojnie zagnieżdżone w innych funkcjach.

Działać tu będzie ten sam mechanizm. Funkcje mają dostęp do swoich zmiennych i zmiennych z zewnątrz. Zewnętrzne środowisko nie ma dostępu do zmiennych zagnieżdżonych funkcji. Tworzy się więc pewna zagnieżdżona hierarchia. Uh...


let a1 = 1;

function kingdom() {
    let a2 = 2;
    console.log(a1); //1
    console.log(a3); //blad - nie ma dostępu do zmiennych w wewnętrznej funkcji

    function king() {
        let a3 = 3;
        console.log(a1, a2, a3); //1, 2, 3
    }
}

console.log(a1, a2); //blad - zewnętrzne środowisko nie ma dostępu

Na chwilę wróćmy do wyrażenia funkcyjnego. Pamiętasz, że anonimową funkcję podstawialiśmy pod zmienną. Idąc tym śladem, skoro funkcja jest podstawiona pod zmienną, to czy zewnętrzne środowisko ma dostęp do wyrażenia funkcyjnego, które jest zdefiniowane w danej funkcji? Tak samo jak w przypadku zmiennych - nie ma.


let kingdom = function() {
    let king = function() {

    }

    let queen = function() {

    }

    king(); //wszystko ok
}

king(); //blad - nie mamy dostępu do wartości "zmiennej" king, bo jest wewnętrzna

Domknięcia - closures

No dobrze. Wiem, że powyższe tematy na początku mogą przyprawiać o ból głowy. Jak ma boleć, to niech boli porządnie.
Omówmy jeszcze jeden mechanizm, który tutaj działa.
Gdy funkcja zaczyna działać, tworzy swoje środowisko ze swoimi zmiennymi. To wiemy. 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() {
    console.log(a);
    a++;
}

myF(); //0
myF(); //1
myF(); //2
myF(); //3

Jak widzisz, zmienna z zewnątrz trzyma swój stan między wywołaniami funkcji myF. To logiczne. Takie funkcje mogą być zagnieżdżone, a wtedy ich zewnętrznymi środowiskami są inne funkcje:


function myF() {
    let a = 0;

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

let m = myF();
m(); //0
m(); //1
m(); //2
m(); //3

Zauważ sztuczkę, którą użyliśmy. myF zwraca nową funkcję, która ma dostęp do środowiska zewnętrznego (które jest funkcją myF). Dzięki temu przy kolejnych wywołaniach nowej funkcji m() mamy dostęp do zapamiętanej zmiennej ze środowiska zewnętrznego.

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 sama się wywołuje - tak zwany Immediately-invoked function expression (IIFE).
Zanim do niego przejdziemy, powtórzmy to, co powyżej poznaliśmy.

Zadeklarujmy prostą funkcję za pomocą wyrażenia funkcyjnego:


const foo = function() {...}

//bardziej skomplikowany przykład
const goo = function(a) {
    console.log(a);
}

Żeby teraz wywołać powyższą funkcję, musimy podać jej nazwę, za którą wstawimy parę nawiasów:


    foo();

    goo("ala");

Częstokroć jednak wcale nie będziemy potrzebować nazwy funkcji, bo wewnętrzny kod chcielibyśmy wykonać tylko jeden raz.
Czyli po definicji funkcji chcielibyśmy od razu ją wywołać:


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

Powyższy kod zwróci błąd. Gdy parser napotka powyższy zapis, potraktuje go jako deklarację. Deklaracja funkcji wymaga podania nazwy, dlatego parser wyrzuci błąd. Aby to naprawić wystarczy powyższy 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 foo() {
    console.log('Jakiś tekst');
}
foo();

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


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

Tak samo jak w przypadku wywoływania zwykłych funkcji, między nawiasami możemy przekazywać parametry:


(function(win, v) {
    console.log(w.innerHeight);
})(window, ourVariable);

Tak samo też jak w klasycznych funkcjach, wnętrze tej funkcji sprawia, że zmienne i metody tam deklarowane stają się lokalnymi. Sprawia to, że wzorzec samo wywołującej się funkcji jest bardzo często wykorzystywany do zamykania całych skryptów przed zewnętrznym środowiskiem, co zabezpiecza je przed złymi ludźmi.


(function() {
    const x = 10;

    const printNumber = function() {
        console.log(x);
    }

    console.log(x);

    printNumber();
})();

console.log(x); //wyrzuci błąd, bo nie mamy tutaj dostępu do kodu z środka
printNumber(); //także zwróci błąd

No dobrze, a czemu to wszystkiemu ma służyć? Poza samo wywołującą się funkcją istnieje pewna rzecz, którą nie koniecznie musisz zauważać. W dziale o zmiennych pisałem ci o różnicach między let, const a starszym var. Zakres let i const definiują klamry (tak zwany blok).

Niestety ze zmiennymi var tak dobrze nie ma. Ich zakres definiuje ciało funkcji:


let i = 10;
{
    let i = 20;
    let a = 10;
}
console.log(i); //10
console.log(a); //blad - nie ma dostepu

var i = 10;
function myF() {
    var i = 20;
    var a = 10;
}

console.log(i); //10
console.log(a); //blad - nie ma dostępu

Gdy stosowaliśmy var, bardzo często chcieliśmy ograniczać zasięg zmiennych (jak bloki przy let i const). Musieliśmy do tego używać właśnie funkcji. Bardzo często do tego celu używało się właśnie IIFE:


var i = 10;

(function() {
    var i = 20;
    var a = 10;
})();

console.log(i); //10
console.log(a); //blad - nie ma dostępu

W przypadku let i const już nie trzeba tak czarować.

Zakończmy ten wywód klasycznym przykładem użycia IIFE:


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

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

Czemu powyższy przykład zwraca 3 - przecież powinien 0. No właśnie nie do końca. Pamiętaj, że kod funkcji po napisaniu jest jeszcze nieaktywny. Dopiero gdy go wywołamy, funkcja rozpoczyna działanie tworząc swoje środowisko i odpowiednie zmienne. W powyższym kodzie funkcja "tworzy swoje zmienne" - ale przecież tam nie ma żadnych zmiennych - ani lokalnych ani parametrów. Bierze więc 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

//lub

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

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

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

arr[0](); //0

Funkcje zwrotne

Czym są funkcje zwrotne (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('Randomowa 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.

Jest to dość często stosowana technika w JavaScript. Możemy ją spotkać na przykład w eventach lub w niektórych funkcjach tabel

Trening czyni mistrza

Poniżej zamieszczam kilka zadań, które w ramach ćwiczenia możesz wykonać. Funkcje są bardzo ważne, dlatego zadań nieco więcej.

  1. Napisz funkcję, która przyjmie jeden parametr - dowolny tekst
    Funkcja niech ZWRACA tekst "Liczba liter: ...." gdzie .... to liczba liter tekstu.
    
                function printText(txt) {
                    return "Liczba liter: " + txt.length;
                }
    
                console.log(printText("Ala ma kota"));
                
  2. Napisz funkcje, która zsumuje przekazaną do niej tablicę i zwraca wynik (sumę)
    
                function sumTable(tab) {
                    let sum = 0;
                    for (let i=0; i<tab.length; i++) {
                        sum += tab[i];
                    }
                    return sum;
                }
    
    
                const arr = [1,2,3,4];
                console.log( sumTable(arr) );
    
                console.log( sumTable([1,2,3]) );
                
  3. Napisz funkcję, która przyjmie dowolny tekst
    Funkcja niech zwraca tekst, który ma zmiksowana wielkość liter np:
    input -> Ala ma kota
    output -> AlA Ma kOtA
    
                function mix(txt) {
                    let newTxt = '';
    
                    for (let i=0; i<txt.length; i++) {
                        if (i % 2 === 0) {
                            newTxt += txt.charAt(i).toUpperCase();
                        } else {
                            newTxt += txt.charAt(i).toLowerCase();
                        }
                    }
                    return newTxt;
                }
    
                console.log( mix("Ala ma kota") );
                
  4. Napisz funkcje, która będzie wymagać 2 atrybutów.
    Funkcja niech sprawdza, czy oba atrybuty są numerami.
    Funkcja ma zwracać iloczyn (*) obu liczb.
    Jeżeli któryś z atrybutów nie jest liczba, funkcja niech zwraca false.
    NIE Wywołuj na razie tej funkcji
    
                function multiply(nr1, nr2) {
                    if (typeof nr1 !== "number" || typeof nr2 !== "number") {
                        return false;
                    } else {
                        return nr1 * nr2;
                    }
                }
    
    
                console.log(multiply(3, 2)); //6
                console.log(multiply("Ala", 2)); //false
                
  5. Napisz funkcje, która przyjmuje 2 parametry:
    1) imię - np: Ala
    2) miesiąc - np: styczeń

    Funkcja ma zwracać:
    - jeżeli miesiąc to -> grudzień, styczeń, luty
    "Ala jeździ na sankach"

    - jeżeli miesiąc to -> marzec, kwiecień, maj
    "Ala chodzi po kałużach"

    - jeżeli miesiąc to -> czerwiec, lipiec, sierpień
    "Ala się opala"

    - jeżeli miesiąc to -> wrzesień, październik, listopad "Ala zbiera liście"
    
                    function printActions(name, month) {
                        if (month === "grudzien" || month === "styczen" || month === "luty") {
                            return name + " jeździ na sankach";
                        }
                        if (month === "marzec" || month === "kwiecien" || month === "maj") {
                            return name + " chodzi po kałużach";
                        }
                        if (month === "czerwiec" || month === "lipiec" || month === "sierpien") {
                            return name + " się opala";
                        }
                        if (month === "wrzesien" || month === "pazdziernik" || month === "listopad") {
                            return name + " zbiera liście";
                        }
                    }
                
  6. Wywołaj funkcje z powyższego zadania przekazując do niej zmienne:
    - Twoje imię
    - dowolny miesiąc

    Dopisz w tej funkcji zabezpieczenie, które pozwoli wpisać miesiąc małymi lub dużymi literami Jeżeli miesiąc jest "innym słowem", funkcja niech zwraca "Ala uczy się JavaScript"
    
                function printActions(name, month) {
                    month = month.toLowerCase();
    
                    if (month === "grudzien" || month === "styczen" || month === "luty") {
                        return name + " jeździ na sankach";
                    }
                    if (month === "marzec" || month === "kwiecien" || month === "maj") {
                        return name + " chodzi po kałużach";
                    }
                    if (month === "czerwiec" || month === "lipiec" || month === "sierpien") {
                        return name + " się opala";
                    }
                    if (month === "wrzesien" || month === "pazdziernik" || month === "listopad") {
                        return name + " zbiera liście";
                    }
    
                    return name + " uczy się JavaScript";
                }
                
  7. Mamy przykładowy tekst:

    const str = "Ania|Marcin|Bartek|Piotr|Kuba|Beata|Agnieszka";
    Napisz funkcję, która przyjmie 2 atrybuty:
    - tekst
    - znak rozdziału (np |)

    Skorzystaj z odpowiedniej metody String (poszukaj w necie), tak aby rozdzielić przekazany do funkcji tekst na części za pomocą przekazanego znaku rozdziału. W wyniku rozdzielenia powinieneś dostać tablicę. Funkcja niech posegreguje tą tablicę alfabetycznie. Następnie funkcja niech połączy tą tablicę w nowy tekst wstawiając między imiona znak wcześniejszego rozdziału. Skorzystaj tutaj z innej odpowiedniej metody js.

    input -> "Ania|Marcin|Bartek"
    output -> "Ania|Bartek|Marcin"

    Wywolaj tę funkcję przekazując do niej str z początku zadania
    
                function sortString(txt, char) {
                    const tab = txt.split(char);
                    tab.sort();
    
                    const newStr = tab.join("|");
                    return newStr;
                }
    
    
                const str = "Ania|Marcin|Bartek|Piotr|Kuba|Beata|Agnieszka";
                console.log(str);
                console.log(   sortString(str, '|')   );
                
  8. Wygeneruj pod zmienną "tab" 10-elementową tablicę z losowymi liczbami.
    Napisz funkcję getMinFromTab, do której przekażesz powyższą tablicę.
    Funkcja powinna zwrócić najmniejszą liczbę z tablicy.
    Za pomocą console.log wypisz wartość zwróconą przez funkcję.
    
                function getMinFromTab(tab) {
                    var min = 1000000;
                    for (var i=0; i<tab.length; i++) {
                        if (tab[i] < min) {
                            min = tab[i]
                        }
                    }
                    return min;
                }
    
    
                //generujemy tablice
                var tab = [];
                for (var i=0; i<10; i++) {
                    tab.push(  Math.floor(Math.random() * 10)+1  );
                }
    
                //odpalamy funkcje przekazujac tablice
                console.log(  getMinFromTab(tab)  );
                
  9. Napisz 2 funkcje. Każda z nich niech przyjmuje tablicę imion.

    Pierwsza funkcja niech zwraca nową tablicę, w której imiona są zapisane dużymi literami. Druga funkcja niech zwraca nową tablicę, w której imiona mają zmienną wielkość liter. W przypadku drugiej funkcji do zamiany imion skorzystaj z funkcji z zadania 2.

    input -> ["Ania" , "Marcin" , "Bartek" , "Piotr"]
    output1 -> ["ANIA" , "MARCIN" , "BARTEK" , "PIOTR"]
    output2 -> ["AnIa" , "MaRcIn" , "BaRtEk" , "PiOtR"]
    
                function bigNames(tab) {
                    for (var i=0; i<tab.length; i++) {
                        tab[i] = tab[i].toUpperCase();
                    }
                    return tab;
                }
    
                function mixNames(tab) {
                    for (var i=0; i<tab.length; i++) {
                        tab[i] = mix(tab[i]); //mix - nasza funkcja z zadania 2
                    }
                    return tab;
                }
    
    
                var tab = ["Ania" , "Marcin" , "Bartek" , "Piotr"];
                console.log(bigNames(tab));
                console.log(mixNames(tab));
                
  10. Napisz funkcję która przyjmie 2 atrybuty:
    - tekst
    - liczbę n

    Funkcja niech zwraca napis będący zduplikowany n razy:
    input : tekst: "ala", n: 4
    output: "alaalaalaala"

    Funkcja niech sprawdza czy oba parametry są podane.
    Jeżeli parametr tekst nie jest podany lub nie jest tekstem, funkcja powinna go ustawić na pusty string "".
    Jeżeli parametr n nie jest podany, funkcja powinna ustawić go na 2.

    Przykłady użycia:

    input : tekst: "kot", n: 2
    output: "kotkot"

    input : tekst: "x", n: nie podany
    output: "xx"

    input : tekst: nie podany, n: 2
    output: ""
    
                function duplicateText(text, nr) {
                    if (typeof text !== 'string') {
                        text = '';
                    }
                    if (typeof nr === 'number') {
                        nr = 2;
                    }
    
                    let newStr = '';
                    for (let i=0; i<nr; i++) {
                        newStr += text;
                    }
    
                    return newStr;
    
                }
    
                console.log(duplicateText('maslo', 10));
                
  11. Napisz funkcję counter, która przy kolejnych wywołaniach będzie zwracać coraz większą wartość. Przy pierwszym wywołaniu powinna zwrócić 0, przy drugim 1, przy trzecim 2...
    Nie używaj tutaj zmiennej globalnej.
    
                function counter() {
                    let a = 0;
                    return function() {
                        return a++;
                    }
                }
    
                const count = counter();
                count();
                count();