Funkcje w JavaScript

Cześć! To jest fragment książki JavaScript od podstaw, która ma pomóc w nauce programowania od zera.

Wydzielanie części kodu

W programowaniu wielokrotnie wykonuje się te same lub podobne operacje. Naturalnym jest, że zamiast pisać je po wielokroć, wolelibyśmy napisać powtarzalny kod tylko raz, a potem go wywoływać kiedy tylko chcemy. Właśnie tutaj z pomocą przychodzą funkcje.

Żeby je lepiej zrozumieć wyobraź sobie Jacka, który bardzo lubi hamburgery. Stanowią jego posiłek jako: śniadanie, obiad i kolację. Aby zjeść takiego hamburgera wymagane jest jednak kilka kroków przygotowań: podsmażenie mięsa, podsmażenie bułki, dodanie pomidora i wreszcie sama konsumpcja. Jego dzień można opisać następująco:

console.log("Wstań z łóżka");
console.log("Umyj zęby");
console.log("Podsmaż mięso");
console.log("Podsmaż bułkę");
console.log("Dodaj pomidora");
console.log("Zjedz hamburgera");
console.log("Pracuj");
console.log("Podsmaż mięso");
console.log("Podsmaż bułkę");
console.log("Dodaj pomidora");
console.log("Zjedz hamburgera");
console.log("Pracuj");
console.log("Podsmaż mięso");
console.log("Podsmaż bułkę");
console.log("Dodaj pomidora");
console.log("Zjedz hamburgera");
console.log("Graj w gry");
console.log("Idź spać");

Powtarzalny proces przygotowywania i konsumpcji hamburgera nie tylko jest uciążliwy, ale także zaciemnia obraz tego, jak naprawdę wygląda dzień Jacka. Możemy poprawić ten kod poprzez wyciągnięcie funkcji makeAndEatHamburger. Jej ciało będzie zawierało poszczególne kroki związane z przygotowaniem i konsumpcją, dzięki czemu będziemy mogli je uruchomić poprzez wywołanie tej funkcji.

// Definicja funkcji
function makeAndEatHamburger() {
  console.log("Podsmaż mięso");
  console.log("Podsmaż bułkę");
  console.log("Dodaj pomidora");
  console.log("Zjedz hamburgera");
}

console.log("Wstań z łóżka");
console.log("Umyj zęby");
makeAndEatHamburger(); // Użycie funkcji
console.log("Pracuj");
makeAndEatHamburger(); // Użycie funkcji
console.log("Pracuj");
makeAndEatHamburger(); // Użycie funkcji
console.log("Graj w gry");
console.log("Idź spać");

Standardowa funkcja zaczyna się od słówka function, potem następuje jej nazwa, nawiasy zwykłe, w których mogą znajdować się definicje parametrów, i wreszcie ciało funkcji, które umieszczamy w nawiasach klamrowych. Ciało zawiera zbiór instrukcji, które zostaną kolejno wywołane, gdy wywołana zostanie funkcja. Następnie wywołujemy zdefiniowaną funkcję przez użycie jej nazwy, a po niej nawiasów okrągłych.

Zwróć uwagę na różnicę między definicją funkcji a jej wywołaniem. Definicja zaczyna się od słówka function i określa, co pod daną nazwą powinno stać. Wywołanie to po prostu nazwa funkcji i nawias okrągły. Ono wywołuje kod zawarty w definicji. W ciele funkcji możemy użyć wszystko, czego nauczyliśmy się do tej pory — zmienne, pętle czy nawet same funkcje.

Czy czegoś Ci to nie przypomina? Tak, funkcji używamy od samego początku książki. log, w znanym nam już dobrze console.log, jest właśnie funkcją. Dlatego wywołujemy ją przy użyciu nawiasu, a zajmuje się ona wypisywaniem wartości do konsoli.

Użycie funkcji do ukrycia wielu operacji pod pojedynczą nazwą jest bardzo ważne. Pomyśl tylko o dowolnym działaniu, na przykład wyjściu do sklepu. Co robisz? Wkładasz buty, ubierasz się w kurtkę, wychodzisz z domu... ale każda z tych operacji to składowa wielu mniejszych. Czym jest na przykład zakładanie butów? Nałożenie na nogę, wiązanie sznurówek... Czym jest to wiązanie sznurówek? Jest to skoordynowana sekwencja ruchów wielu mięśni. Myślimy o abstrakcyjnych działaniach, pod którymi stoi wiele mniejszych działań. W programowaniu to funkcje służą do ukrywania sekwencji działań za pojedynczą nazwą.

function goToStore() {
  wearShoes()
  putOnJacket()
  leaveHouse()
  // ...
}

function wearShoes() {
  putOnShoe()
  tieShoelace()
  // ...
}

// ...

Funkcja może reprezentować nawet bardzo złożone czynności, a sama być prosta, bo używa tylko nieco mniej złożonych elementów. Tak właśnie skonstruowane są programy.

Jak działają funkcje?

Przeanalizujmy krok po kroku jak działają funkcje. Program wykonuje kolejne instrukcje linijka po linijce. Gdy wywoływana jest funkcja, przeskakujemy na sam początek jej ciała i od tego miejsca idziemy dalej. Gdy dojdziemy do końca (albo słówka return, o którym powiemy niedługo), wracamy do miejsca, gdzie funkcja była wywołana i stamtąd kontynuujemy. Tak w skrócie działają funkcje. Poniżej znajduje się prezentacja przykładowych funkcji wraz z kolejnymi numerami reprezentującymi kolejność ich wywołania.

function firstFunction() {
  console.log("3"); // 3
  console.log("4"); // 4
}

console.log("1"); // 1
console.log("2"); // 2
firstFunction();
console.log("5"); // 5
secondFunction();
console.log("8"); // 8

function secondFunction() {
  console.log("6"); // 6
  console.log("7"); // 7
}

// Wypisze się kolejno 1 2 3 4 5 6 7 8

Warto spędzić chwilę i poćwiczyć wyobrażanie sobie jak idzie nasz program po kolejnych linijkach, jak przeskakuje do funkcji, a potem jak przeskakuje z powrotem do miejsca wywołania.

Odnoszenie się do elementów spoza funkcji

W funkcji możemy korzystać ze wszystkiego, co zostało zdefiniowane — na przykład do zmiennej albo do innych funkcji.

let whoToCheer = "Czytelniku";

function cheer() {
  console.log("Cześć " + whoToCheer + "!");
}

cheer(); // Cześć Czytelniku!
whoToCheer = "Wszystkim";
cheer(); // Cześć Wszystkim!

function cheerTwoTimes() {
  cheer();
  cheer();
}

cheerTwoTimes(); // Wypisze:
// Cześć Wszystkim!
// Cześć Wszystkim!

Parametry i argumenty funkcji

Większość funkcji chciałaby mieć jednak wartości tylko dla siebie. Jest to możliwe: wystarczy zdefiniować zmienne wewnątrz nawiasów w definicji funkcji. Takie zmienne nazywane są parametrami. Nie wymagają one żadnego słówka let i definiują to, co powinno być przekazane do konkretnej funkcji. Z drugiej strony, przy wywołaniu funkcji powinniśmy podać wartości, które będą przekazane do tych parametrów. Takie wartości nazywane są argumentami. W poniższym przykładzie whoToCheer to parametr funkcji, a "Czytelniku", "Wszystkim" oraz 42 to wartości używane jako argumenty.

function cheer(whoToCheer) {
  console.log("Cześć " + whoToCheer + "!");
}

cheer("Czytelniku"); // Cześć Czytelniku!
cheer("Wszystkim"); // Cześć Wszystkim!
cheer(42); // Cześć 42!

Funkcja może mieć więcej parametrów, a wywołanie składać się z większej liczby argumentów — w takim przypadku oddzielamy je przecinkami.

To bardzo ważny mechanizm, gdyż bez niego większość funkcji nie miałaby sensu. Pomyśl tylko o funkcji log. Gdybyśmy nie mieli możliwości przekazania argumentu, to jak określilibyśmy, co chcemy wypisać?

Objaśnienie wywołania funkcji poprzez podstawienie ciała

Na potrzeby wyjaśniania bardziej złożonych przypadków powstała jeszcze jedna metoda objaśniania jak działają funkcje. Ta już nie jest dosłowna, tylko metaforyczna. Możemy sobie wyobrazić, że wywołanie funkcji zastępuje jej ciało. W takim wypadku parametry możemy zapisać jako osobne zmienne oraz przypisać do nich wartości argumentów.

function cheer(whoToCheer) {
  console.log("Cześć " + whoToCheer + "!");
}

cheer("Czytelniku"); // Cześć Czytelniku!

// Jest jednoznaczne z:
const whoToCheer = "Czytelniku";
console.log("Cześć " + whoToCheer + "!");

Będziemy wracali do tej metody, gdyż jest ona wyjątkowo przydatna przy bardziej skomplikowanych funkcjach. Będę nazywał ją objaśnieniem wywołania funkcji poprzez podstawienie ciała.

Brakujące lub nadmiarowe argumenty

JavaScript pozwala, abyśmy wywołali funkcję z mniejszą liczbą argumentów, niż funkcja się spodziewa. Parametry, dla których zabrakło argumentów, przyjmą wtedy wartość undefined, podobnie do zwykłych zmiennych, których wartości nie zostały określone. Nadmiarowe argumenty zostaną natomiast zignorowane.

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

printAll("A", "B", "C"); // A, B
printAll("A", "B"); // A, B
printAll("A"); // A, undefined
printAll(); // undefined, undefined

Ćwiczenie: Funkcje

Napisz funkcję, która:

  • wypisuje sumę dwóch liczb przekazywanych jako argumenty,
  • wypisuje kolejne liczby od a do b, gdzie ab to parametry funkcji (zakładamy, że wartość a < b),
  • wypisuje określoną liczbę gwiazdek w jednej linii,
  • wypisuje kwadrat z gwiazdek o określonym rozmiarze,
  • wypisuje trójkąt równoramienny z gwiazdek o określonym rozmiarze.

Przykłady użycia poniżej.

printSum(1, 2); // 3
printSum(3, 4); // 7
printSum(20, 10); // 30

printNumbers(2, 4);
// 2
// 3
// 4

printStars(3); // ***
printStars(5); // *****
printStars(8); // ********

printSquare(2);
// **
// **

printSquare(3);
// ***
// ***
// ***

printTriangle(3);
// *
// **
// ***

printTriangle(4);
// *
// **
// ***
// ****

Odpowiedzi na końcu książki.

Wynik funkcji

Poznaliśmy funkcje już na lekcjach matematyki. Tam były one definiowane, aby obliczyć wynik dla wartości wejściowych. W szkole poznaliśmy funkcje podnoszące do kwadratu, modulo czy silni. Możemy takie funkcje zdefiniować również w programowaniu. Aby zwrócić wartość z funkcji, musimy użyć słowa return i dzięki temu zostanie ona zwrócona z wywołania tej funkcji.

function returnNumber() {
  return 42;
}

const result = returnNumber();
console.log(result); // 42

Zdefiniujmy więc funkcję do obliczenia kwadratu liczby (ang. square).

function square(x) {
  return x * x;
}

console.log(square(2)); // 4
console.log(square(4)); // 16
console.log(square(10)); // 100

W przypadku wartości bezwzględnej (ang. absolute value) nie jest już tak łatwo. Ta funkcja powinna zwracać:

  • wartość wejściową, gdy jest ona większa lub równa zero,
  • przeciwność wartości wejściowej, gdy jest ona mniejsza od zera.

Aby to zrobić, możemy wykorzystać if-else.

function absolute(x) {
  if (x >= 0) {
    return x;
  } else {
    return -x;
  }
}

console.log(absolute(0)); // 0
console.log(absolute(2)); // 2
console.log(absolute(-2)); // 2
console.log(absolute(10)); // 10
console.log(absolute(-10)); // 10

return natychmiast kończy wywołanie funkcji i zwraca wynik. Dlatego formalnie blok else nie jest potrzebny, bo jeśli pierwszy warunek używa return, to nic się po tej komendzie już w tej funkcji nie wykona. W związku z tym, działanie powyższych funkcji będzie analogiczne do poniższych.

function square(x) {
  return x * x;
  console.log("To się nigdy nie wypisze");
}

function absolute(x) {
  if (x >= 0) {
    return x;
  }
  return -x;
}

Wreszcie silnia (ang. factorial). Silnia z n jest to iloczyn liczb od 1 do n, czyli to, co powstaje w wyniku ich pomnożenia. Dla przykładu silnia z 5 jest równa 1 * 2 * 3 * 4 * 5, a więc 120. Dla liczb mniejszych od 1 zakłada się, że ich silnia jest równa 1. Kolejne liczby uzyskamy przy pomocy pętli for. Będziemy je uwzględniać w finalnym wyniku przy pomocy *=. Także przy użyciu if zabezpieczymy się przed liczbami mniejszymi lub równymi 1.

function factorial(num) {
  if (num <= 1) {
    return 1;
  }
  let acc = 1; // skrót od accumulator
  for (var i = 2; i <= num; i++) {
    acc *= i; // lub acc = acc * i
  }
  return acc;
}

console.log(factorial(0)); // 1
console.log(factorial(1)); // 1
console.log(factorial(2)); // 2
console.log(factorial(3)); // 6
console.log(factorial(4)); // 24
console.log(factorial(5)); // 120

Ćwiczenie: Funkcje zwracające wartości

Napisz funkcję, która...

  • zamienia liczbę dni na liczbę milisekund (jedna sekunda to 1000 milisekund).
  • oblicza wielkość pola powierzchni trójkąta prostokątnego, na podstawie długości jego prostopadłych boków (wzór to a * b / 2).
  • zwraca największą z trzech wartości.

Przykłady użycia poniżej.

console.log(daysToMillis(1)); // 86400000
console.log(daysToMillis(3)); // 259200000

console.log(triangleArea(1, 1)); // 0.5
console.log(triangleArea(10, 20)); // 100

console.log(biggestOf(2, 3, 1)); // 3
console.log(biggestOf(2, 3, 5)); // 5
console.log(biggestOf(3, 3, 1)); // 3

Odpowiedzi na końcu książki.

Funkcje matematyczne

Pisanie funkcji matematycznych jest dobrym ćwiczeniem, ale by każdy programista nie powtarzał tej samej pracy, te najważniejsze zostały już zdefiniowane w samym JavaScript. Aby je wywołać, powinniśmy zacząć od Math, a potem użyć skróconej nazwy funkcji. Oto kilka przydatnych funkcji:

  • Math.abs(x) - wartość bezwzględna,
  • Math.pow(x, y) - potęga (jest to więc alternatywa do operatora **),
  • Math.min(x, y), Math.min(x, y, z, ...) - najmniejsza z wartości,
  • Math.max(x, y), Math.max(x, y, z, ...) - największa z wartości,
  • Math.log(x), Math.log2(x), Math.log10(x) - odpowiednio: logarytm naturalny, logarytm o podstawie 2, logarytm dziesiętny.
  • Math.sin(x), Math.cos(x), ... - sinus, cosinus itp.

Obiekt ten zawiera również kilka stałych, na przykład Math.PI.

console.log(Math.abs(10)); // 10
console.log(Math.abs(-10)); // 10

console.log(Math.pow(10, 2)); // 100
console.log(Math.pow(10, -2)); // 0.01

console.log(Math.min(10, 2)); // 2
console.log(Math.min(2, 10)); // 2
console.log(Math.min(2, 1, 3)); // 1

console.log(Math.max(10, 2)); // 10
console.log(Math.max(2, 10)); // 10
console.log(Math.max(2, 3, 1)); // 3

console.log(Math.log(10)); // 2.302585092994046

console.log(Math.log2(2)); // 1
console.log(Math.log2(4)); // 2
console.log(Math.log2(10)); // 3.321928094887362

console.log(Math.log10(2)); // 0.3010299956639812
console.log(Math.log10(10)); // 1
console.log(Math.log10(100)); // 2

console.log(Math.PI); // 3.141592653589793

Dodatkowo bardzo przydatną funkcją jest Math.random(), która generuje losową wartość między 0 a 1 (bez 1). Bardzo przydaje się, gdy chcemy do programu wprowadzić pewną losowość.

console.log(Math.random());
// Za każdym razem inna wartość od 0, ale mniejsza od 1,
// na przykład 0.3926951220278445

Warto nauczyć się wyszukiwać potrzebne funkcje w internecie. Wpisując w Google "js power", z łatwością znajdziesz dokumentację funkcji Math.pow z przykładami użycia. Jeśli poszukasz funkcji, której nie ma w JavaScript, na przykład "js factorial", to znajdziesz liczne sposoby, jak inni napisali tę funkcję. Problem może sprawić tylko zapamiętanie wszystkich nazw, tak by wyszukiwanie było szybkie i efektywne. Dlatego przy programowaniu warto uczyć się nazw funkcji, konceptów, algorytmów. Nie musisz jednak od razu wszystkiego wiedzieć.

Ćwiczenie: JavaScript jako kalkulator matematyczny

Użyj języka JavaScript, aby obliczyć wyniki poniższych równań matematycznych:

  • 42π4^2 * \pi
  • log10(2030+40)log_{10} (20 * 30 + 40)
  • log2105log_2 10^{-5}
  • (3)7|(-3)^{7}| (te poziome kreski to w matematyce wartość bezwzględna)

Odpowiedzi na końcu książki.

Funkcje jako wartości

W rozdziale Wartości wspominaliśmy, że funkcje są jednym z typów wartości. Rzeczywiście tak skonstruowany jest JavaScript. Kiedy definiujemy funkcję o danej nazwie, tak naprawdę definiujemy zmienną o takiej nazwie. Zmienną, którą można wypisać lub też przypisać do innej zmiennej.

function add(a, b) {
  return a + b;
}

console.log(add(1, 2)); // 3

console.log(add);
// Wypisze:
// ƒ add(a, b) {
//   return a + b
// }

console.log(typeof add); // function

const plus = add;
console.log(plus(1, 2)); // 3

Zauważ, że kiedy wywoływaliśmy funkcję, zarówno teraz, jak i wcześniej, to tak naprawdę wywoływaliśmy zmienną. Jeśli zmienna reprezentuje funkcję, to można za nią postawić nawias okrągły, by ją wywołać. Tak więc nic nie stoi na przeszkodzie, by wywołać plus(1, 2).

Kiedy definiujemy funkcję, nie musimy nadawać jej nazwy. Definicja funkcji sama zwraca odwołanie do tej funkcji jako wartości, a to możemy przypisać do zmiennej.

const add = function(a, b) {
  return a + b;
}
console.log(add(1, 2)); // 3

Funkcje, które nie zawierają w swojej definicji nazwy, znane są jako funkcje anonimowe, czyli bezimienne (jak nasz Gall Anonim). Można by argumentować, że przecież funkcja w powyższym przykładzie ma nazwę add. Czasem dla uproszczenia tak się mówi, ale będąc bardziej precyzyjnym, funkcja ta nie ma nazwy (jest anonimowa), za to wskazuje na nią zmienna o nazwie add.

function add1(a, b) { // funkcja nazwana
  return a + b;
}
console.log(add1(1, 2)); // 3

const add2 = function(a, b) { // funkcja anonimowa
  return a + b;
}
console.log(add2(1, 2)); // 3

Co istotne, zmienną reprezentującą funkcję można przekazać jako argument do innej funkcji.

function callFunction(fun) {
  fun();
}

function printName() {
  console.log("Maciek");
}

const printSecondName = function() {
  console.log("Marta");
}

printName(); // Maciek
printSecondName(); // Marta

callFunction(printName); // Maciek
callFunction(printSecondName); // Marta
callFunction(function() {
  console.log("Figa");
}); // Figa

Aby zrozumieć jak powyższy przykład działa, użyjmy objaśnienia wywołania funkcji poprzez podstawienie jej ciała. Tutaj jednak odbędzie się to w dwóch krokach - najpierw podstawimy ciało funkcji callFunction, a potem funkcji przekazanej jako argument.

callFunction(printName); // Maciek
callFunction(printSecondName); // Marta
callFunction(function() {
  console.log("Figa");
}); // Figa

// Jest równoznaczne z

printName(); // Maciek
printSecondName(); // Marta
const fun = function() {
  console.log("Figa");
};
fun();  // Figa

// Jest równoznaczne z

console.log("Maciek"); // Maciek
console.log("Marta"); // Marta
console.log("Figa"); // Figa

Ponieważ funkcje są wartościami, możemy je również zwracać z innych funkcji.

function add(a, b) {
  return a + b;
}
function returnAdd() {
  return add;
}

const result = returnAdd()(1, 2);
console.log(result); // 3

const myAdd = returnAdd();
console.log(myAdd(1, 2)); // 3

Już w rozdziale Elementy programowania obiektowego przekonamy się, że wykorzystywanie funkcji jako wartości jest naturalne i częste. Jeśli chodzi o ich przekazywanie jako argument do innych funkcji, w rozdziale Iteracja po tablicy zobaczymy jak bardzo się to przydaje. Najpierw jednak pomówmy o obiektach.

Ćwiczenie: Funkcje jako wartości

Dla poniższego kodu:

function speak(before, after) {
  if(before && typeof before === "function") {
    before();
  }
  console.log("Mowa...");
  if(after && typeof after === "function") {
    after();
  }
}

function cheerKids() {
  console.log("Hej, dzieci");
}
function cheerAll() {
  cheerKids();
  console.log("Witam rodziców");
}
function bless() {
  console.log("Zdrowia!");
}

Sprawdzenie, czy before oraz after to są typu "function" zabezpiecza nas przed niepoprawnym użyciem funkcji speak.

Pamiętając, że undefined w warunku jest falsy, czego się spodziewasz, dla poniższych wywołań funkcji speak?

  • speak()
  • speak(cheerKids)
  • speak(cheerAll)
  • speak(cheerAll, bless)
  • speak(bless)
  • speak(undefined, bless)

Odpowiedzi na końcu książki.