Funkcje to zbiór zgrupowanych instrukcji, które uruchamiamy poprzez podanie ich nazwy.
Każda taka funkcja po wywołaniu wykonuje swój wewnętrzny kod, a następnie może zwrócić nam jakąś wartość.
Ogólna deklaracja funkcji ma postać:
function kwadrat(liczba) {
const wynik = liczba * liczba; //możemy też po prostu zwrócić liczba * liczba
return wynik;
}
czyli
function kwadrat(liczba) {
return liczba * liczba;
}
//Po stworzeniu funkcji wystarczy ją wywołać poprzez podanie jej nazwę:
kwadrat(2); //4
kwadrat(4); //16
kwadrat(7); //49
Funkcję można traktować jak swego rodzaju podprogramy.
Można je umieścić w oddzielnym pliku, później dołączać do wybranych stron
(tak powstają biblioteki np. jQuery).
Dla każdej funkcji możemy utworzyć parametry.
Dzięki nim możemy przekazywać dane dla danej funkcji.
function suma(a, b) {
return a + b;
}
document.write( suma(7, 3) ); //10
document.write( suma(4, 5) ); //9
function lineText(name, pet) {
document.write(name + " ma " + pet);
}
lineText("Ola", "kota"); //Ola ma kota
lineText("Ala", "psa"); //Ala ma psa
Jeżeli nasza funkcja wymaga jakiś wartości, a my ich nie podamy, zostaną użyte dla nich wartości undefined
:
function writeText(name, age) {
document.write(`${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"
Jeżeli funkcja wymaga podania wartości przy jej wywołaniu, a my ich nie podamy, w jej wnętrzu będą one wynosić undefined
:
function printText(txt) {
document.write("Twój tekst to " + txt);
}
printText("kot"); //"Twój tekst to kot"
printText(); //"Twój tekst to undefined"
Dla parametrów możemy też ustawiać domyślne wartości. Wystarczy po nazwie parametru ustawić mu domyślną wartość:
function print(name = "Michał", status = "najlepszy") {
document.write(name + " jest " + status);
}
print(); //"Michał jest najlepszy"
print("Karol"); //"Karol jest najlepszy"
print("Paweł", "wysoki"); //"Paweł jest wysoki"
print(undefined, "wysoki"); //"Michał jest wysoki" - undefined jest traktowane jak niepodanie wartości
Wystarczy wewnątrz funkcji przeprowadzić test, czy dana zmienna ma wartość:
function printText(txt) {
if (typeof txt === "undefined") {
txt = "brak tekstu";
}
document.write(txt);
}
//lub
function printText(txt) {
txt = (typeof txt === "undefined")? "brak tekstu" : txt;
document.write(txt);
}
printText(); //"brak tekstu"
Istnieje też krótsza metoda.
function printText(txt) {
txt = txt || "brak tekstu";
document.write(txt);
}
printText(); //"brak tekstu"
Metoda ta jednak nie zawsze się sprawdza dlatego stosujmy dłuższy zapis.
function printText(txt) {
txt = txt || "brak tekstu";
document.write(txt);
}
printText(""); //"brak tekstu", a powinno być ""
JavaScript nie wymaga od nas, abyśmy przekazywali do funkcji wymaganą przez daną funkcję ilość wartości.
Jeżeli nie zakładamy konkretnej liczby parametrów dla funkcji, możemy skorzystać z właściwości arguments, która zawiera w sobie wszystkie przekazane wartości:
function sum() {
document.write(arguments);
}
sum(); //[] Arguments
sum(1, 2, 3, 4); //[1, 2, 3, 4] Arguments
sum("ala", "ma", "kota"); //["ala", "ma", "kota"] Arguments
Obiekt arguments jest tablico podobny, ale tak naprawdę nie jest tablicą.
Oznacza to, że nie możemy na nim wykonywać metod przeznaczonych dla tablic np. forEach
:
function superSum() {
const result = arguments.forEach(function(el) { //błąd, forEach jest dla tablic
document.write(el);
});
return result;
}
superSum(1, 2, 3, 4);
W dzisiejszych czasach zamiast operować na obiekcie arguments
,
zalecane jest używanie rest operator, który zbiera przekazane argumenty w postaci klasycznej tablicy.
function superSum(...args) {
document.write(args); //[1, 2, 3, 4]
}
superSum(1, 2, 3, 4);
function superSum(...param) {
let result = param.reduce(function(a, b) {
return a + b;
});
return result;
}
superSum(1, 2, 3, 4);
Zapis ten umożliwia zbieranie w jedną zmienną (będącą tablicą) wielu parametrów przekazywanych do funkcji:
function myF(...param) {
document.write(param); //[1, 2, 3, 4, 5]
}
myF(1,2,3,4,5);
function myF(...param) {
const newTab = [...param];
newTab.push("Ala");
document.write(param, newTab); //[1,2,3], [1,2,3,"Ala"]
}
myF(1,2,3);
Rest operator możemy też wykorzystywać do pobierania w formie tablicy "pozostałych" wartości:
function printAbout(name = "Ala", ...other) {
document.write("To jest " + name);
if (other.length) {
document.write(`${name} ma zwierzaki: ${other.join()}`);
}
}
printAbout("Marcin", "pies", "kot"); //To jest Marcin. Marcin ma zwierzaki: pies,kot
printAbout(); //To jest Ala
Pamiętaj, że rest musi występować jako ostatni w parametrach:
function myF(a, b, ...numbers) {
}
function myF(a, ...numbers, b) { //błąd : Rest parameter must be last formal parameter
}
Każda funkcja zwraca jakąś wartość.
Domyślnie jest nią undefined
.
Aby zwrócić naszą wartość, posłużymy się instrukcją return
:
function calculate(number1, number2) {
const result = number1 + number2;
return result;
}
calculate(10, 4) //wypisze 14
function randomBetween(min = 0, max = 10) {
return Math.floor(Math.random()*(max-min+1)+min);
}
//wstawiam wynik do body
document.body.innerText = randomBetween(1, 100);
//wykorzystuję funkcję do powtarzania tekstu
document.write( "kot".repeat(randomBetween(1, 6)) );
//dodaję 2 losowe liczby
document.write( randomBetween(1, 6) + randomBetween(1, 10) );
//generuję tablicę z liczbami 1-100
const tab = [];
for (let i=0; i<10; i++) {
tab.push(randomBetween(1, 100);
}
if (randomBetween(1, 10)) { //w miejscu gdzie używamy funkcji pojawia się wynik
...
}
function sum(a, b) {
return a + b;
document.write(a + b);
document.write("Test"); //nigdy nie zostanie wykonane, bo wcześniej return przerwie działanie funkcji
}
W wielu edytorach kod leżący za return będzie miał przytłumione kolory, co symbolizuje, że taki kod nigdy sie nie wykona:
function getStatus(number) {
if (number < 20) {
return "bad"
}
if (number < 30) {
return "medium"
}
return "good"
}
document.write(getStatus(10));
document.write(getStatus(25));
function fixName(name) {
return name.charAt(0).toUpperCase() + name.slice(1);
}
const result = fixName("piotr") + " " + fixName("kowalski");
document.write(result); //Piotr Kowalski
Instrukcja return może zwracać dowolną wartość. Może to być tablica:
function returnArray(size) {
return new Array(size).fill(0).map((el, key) => key);
}
const result = returnArray(10); //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
document.write(result[0]);
...lub obiekt:
function returnObject() {
return {
first: "ala",
second: "bala",
third: "cala"
}
}
document.write(returnObject().first); //"ala"
lub inne funkcje, o czym przekonasz się nieco później.
Do tej pory poznaliśmy jeden sposób tworzenia funkcji:
function printText() {
...
}
printText();
Jest to tak zwana deklaracja funkcji. Drugi sposób zwie się wyrażeniem funkcyjnym:
const printText = function() {
...
}
printText();
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. Wynika to z faktu działania mechanizmu hoistingu, który przenosi taką deklarację na początek kodu. Możemy więc odwoływać się do funkcji, która jest zadeklarowana później w kodzie.
myFunction(); //Tutaj jest ok
function myFunction() {
document.write("...");
}
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(); //Błąd
const myFunction = function() {
document.write("...");
}
Czy oznacza to, że deklaracje zawsze będą lepszym wyborem? Niekoniecznie.
Po pierwsze gdy tworzymy funkcje za pomocą wyrażenia funkcyjnego poprzedzonego słowem let/const,
możemy ograniczyć zakres jej działania.
Po drugie możemy skrócić jej zapis za pomocą funkcji strzałkowej.
Jak już sobie powiedzieliśmy, funkcje możemy stworzyć na dwa najczęściej stosowane sposoby - jako deklarację i jako wyrażenie funkcyjne.
//deklaracja
function myFn() {
}
//wyrażenie funkcyjne
const myFn = function() {
}
Ten drugi sposób możemy też zapisać za pomocą tak zwanej funkcji strzałkowej, co w dzisiejszych czasach jest bardzo popularne.
//wyrażenie funkcyjne
const myFn = function() {
}
//za pomocą funkcji strzałkowej
const myFn = () => {
}
Zamiast słowa function pojawia nam się fat arrow (gruba strzałka).
Dodatkowo taki zapis możemy jeszcze bardziej skrócić, o czym przekonamy się już za moment.
Przy skracaniu zapisu takiej funkcji obowiązuje nas kilka dość prostych zasad.
Jeżeli funkcja wymaga tylko jednego parametru, wtedy mogę (ale nie muszę) pominąć nawiasy:
const myF = function(a) {
document.write(a * a);
}
const myF = a => {
document.write(a * a);
}
Jeżeli parametrów jest więcej, lub nie ma żadnego, wtedy nawiasy muszą zostać:
const myF = function(a, b) {
document.write(a * b);
}
const myF = (a, b) => {
document.write(a * b);
}
const myF = function() {
document.write("Ala ma kota");
}
const myF = () => {
document.write("Ala ma kota");
}
//dlatego niektórzy przy funkcji, która nie wymaga parametrów
//dodają jakiś parametr - dzięki temu zyskują jeden znak mniej 😏
const myF = _ => {
}
Jeżeli funkcja ma tylko jedną instrukcję to mogę pominąć klamry:
const myF = function(a) {
document.write( a * a );
}
const myF = a => document.write( a * a );
Jeżeli jedyną instrukcją funkcji jest instrukcja return
, także i ją możemy zredukować:
const myF = function(a) {
return a * a;
}
const myF = a => a * a;
Natomiast jeżeli funkcja ma więcej instrukcji, klamry muszą pozostać:
const myF = function(a, b) {
const result = a * b;
document.write( "Wynik mnożenia to", result );
return result;
}
const myF = (a, b) => {
const result = a * b;
document.write( "Wynik mnożenia to", result );
return result;
}
Jeżeli jedyną instrukcją jest zwracanie obiektu, wtedy zachodzi konflikt między redukcją klamer, a klamrami obiektu.
W takim przypadku zwracany obiekt trzeba objąć nawiasami:
const getObj = function(name) {
return { team : name, score : 0 }
}
const getObj = name => { team : name, score : 0 } //błąd
const getObj = name => ({ team : name, score : 0 }) //ok
Poza skróceniem zapisu wyrażeń funkcyjnych, najczęściej funkcje strzałkowe pojawiają się tam, gdzie używaliśmy funkcji anonimowych czyli przy wszelakich zdarzeniach, sortowaniach tablic, metodach forEach(), fetch() itp.
Funkcja strzałkowa bardzo upraszcza ich zapis:
const tabUsers = ["ala", "bala", "cala"];
//zamiast
tabUsers.forEach(function(el) {
document.write(el.toUpperCase());
});
//mogę
tabUsers.forEach(el => {
document.write(el.toUpperCase())
});
//lub jeszcze bardziej
tabUsers.forEach(el => document.write(el.toUpperCase()) );
const tabNr = [1, 2, 3];
//Tworzymy nową tablicę z liczbami 2x większymi
//zamiast
const tab2 = tabNr.map(function(el) {
return el * 2;
});
//mogę
const tab2 = tabNr.map(el => el * 2);
const tabUsers = [
{ name : "Marcin", age: 18 },
{ name : "Ania", age: 16 },
{ name : "Agnieszka", age: 16}
];
//sprawdzamy czy wszyscy użytkownicy są pełnoletni
//zamiast
const allMinors = tabUsers.every(function(el) {
return el.age > 18;
});
//mogę
const allMinors = tabUsers.every(el => el.age > 18);
const tabUsers = [
{ name : "Marcin", age: 18 },
{ name : "Ania", age: 16 },
{ name : "Agnieszka", age: 16}
];
//sprawdzamy czy niektórzy użytkownicy są pełnoletni
//poprzednio
const isSomeOfAge = tabUsers.some(function(el) {
return el.age > 18
});
//teraz
const isSomeOfAge = tabUsers.some(el => el.age > 18);
Wyrażenie funkcyjne bardzo często będzie występować jako tak zwana funkcja anonimowa, czyli taka, która ani nie ma nazwy, ani nie została podstawiona pod zmienną. Taka funkcja będzie występować najczęściej przy funkcjach, które przekazujemy jako parametry:
document.addEventListener("click", function() {
document.write("klik");
});
[1,2,3].forEach(function(el) {
document.write(el);
});
[1,2,3].sort(function(a, b) {
return a - b;
});
Funkcja rekurencyjna to taka, która wywołuje sama siebie.
function myFn() {
document.write("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;
}
}
document.write(fibonacci(10)); //[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
Jedno z zastosowań rekurencji spotyka się w przypadku pracy z interwałami.
Gdy uruchamiamy 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++) {
document.write(i);
}
setTimeout(() => loop(), 2000); //po 2 sekundach ponownie odpalam loop
}
loop();
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] ] ];
Pozostaje 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;
}
document.write( 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.