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 = ''
for (let x=0; i<10; i++) {
    str += '-';
}
console.log(str);

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

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

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

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

Jak widzisz kilka razy powtarzamy linijki console.log, które tworzą kreskowy napis. 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 myPattern() {
    let str = ''
    for (let x=0; i<10; i++) {
        str += '-';
    }
    console.log(str);
}

myPattern();

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

myPattern();

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

myPattern();

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 JS i CSS w oddzielnych plikach. Dzięki temu zmianę literówki zrobisz w jednym miejscu, a nie na każdej podstronie na której wywołujesz dany kod.

Funkcję można traktować jak swego rodzaju "klocek lego", 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 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 biliotek - np jQuery, 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 JS 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 lego" 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 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 myPattern(char) {
    let str = '';
    for (let i=0; i<5; i++) {
        str += char;
    }
    console.log(str);
}

myPattern("x"); //xxxxx
myPattern("."); //.....

Funkcje mogą wykorzystywać inne funnkcje:


//korzystamy z funkcji random() i floor() by wypisać liczbę spomiędzy 1-10
function randomBetween(from, to) {
    console.log(Math.floor(Math.random() * max) + min)
}

randomBetween(1, 10);

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]

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 dla przykładu 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

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) {
    if (txt === undefined) { //bo txt i tak zostanie stworzone
        txt = "lorem";
    }

    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 przez 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 JS (ES6) rozwiązano to jeszcze lepiej. Więcej na ten temat dowiesz się tutaj.

Nasz wcześniejszy przykład z sumowaniem parametrów będzie o wiele praktyczniejszy, jeżeli zabezpieczymy go przed złym wywołaniem:


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

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

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.

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

Instrukcja return nie tylko zwraca wartość, ale i 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

function checkText(tekst) {
    return tekst;
}

//poniżej zadziała automatyczna konwersja, której dokonuje JS
if ( checkText('testujemy') ) { //"jakiś tekst" !== false
    console.log('Funkcja zwróciła true'); //to zostanie wypisane
}
if ( checkText('') ) { //"" === false
    console.log('Funkcja zwróciła false'); //to nie zostanie
}

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
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() {
    ...kod funkcji anonimowej
}

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


const myFunction = function() {
    ...kod funkcji anonimowej
}

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 kodu.

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:


//Tutaj jest ok
myFunction();

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

//Błąd
myFunction();

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ę, 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() {
    ...
});

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:


const 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 (poprzez parametry funkcji i zmienne deklarowane 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 i = 1;

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

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

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

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

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 JS 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ę (spójrz tutaj). 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();

Z powodu brakującej nazwy powyższą deklarację zamieniliśmy na wzorzec za pomocą dodatkowych nawiasów. Co by się stało, gdyby zamiast zamieniać deklarację, chcielibyśmy od razu wywołać wyrażenie:


    const foo = function() {...}();

Nie stało by się nic. Jest to całkowicie poprawny zapis funkcji, która sama się wywoła.

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ę i. 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() {
        let j = i;
        arr[i] = 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

Callback

Czym jest callback?

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


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

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

Powyższy kod wywoła kod funkcji, którą przekazaliśmy w parametrze.

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 JS. 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ę JS"
    
                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ę JS";
                }
                
  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 randomowymi 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. Kazda z nich niech przyjmuje tablicę imion.

    Pierwsza funkcja niech zwraca nową tablicę, w której imiona są zapisane duzymi literami. Druga funkcja niech zwraca nową tablicę, w której imiona mają zmienną wielkośc 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;
                    }
    
                    var newStr = '';
                    for (var 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();