Funkcja to podstawowy blok kodu w JavaScript, który można zdefiniować raz i wykonywać wielokrotnie. Funkcje pozwalają organizować program w mniejsze, logiczne fragmenty, co ułatwia ponowne użycie kodu oraz jego czytelność. W tej sekcji omówimy sposoby definiowania funkcji, różne rodzaje funkcji (deklaracje, wyrażenia, funkcje strzałkowe), a także pojęcia zasięgu zmiennych i domknięć. Na koniec wyjaśnimy, co oznacza, że funkcje w JavaScript są „obiektami pierwszej klasy”.
Definiowanie funkcji: deklaracje vs wyrażenia
W JavaScript funkcję można zdefiniować na kilka sposobów. Najczęściej używane to deklaracja funkcji oraz wyrażenie funkcyjne.
- Deklaracja funkcji: Używa słowa kluczowego
function
na początku. Taka funkcja ma nazwę i można ją wywołać w kodzie. Przykład deklaracji funkcji:
function greet() {
console.log("Witaj!");
}
greet(); // wywołanie funkcji, wypisze: Witaj!
Funkcję greet
zdefiniowaliśmy przez deklarację. Takie funkcje podlegają mechanizmowi hoistingu – co oznacza, że są „wynoszone” na górę zakresu podczas wykonywania skryptu. W praktyce można wywołać funkcję deklarowaną nawet przed jej definicją w kodzie (choć dla czytelności kodu lepiej tego unikać).
- Wyrażenie funkcyjne: Funkcja może być również utworzona jako wyrażenie przypisane do zmiennej. Nie ma wtedy osobnej nazwy (lub jest to funkcja anonimowa), a dostęp do niej uzyskujemy poprzez zmienną. Przykład wyrażenia funkcyjnego:
const greet2 = function() {
console.log("Cześć!");
}
greet2(); // wypisze: Cześć!
Tutaj utworzyliśmy funkcję anonimową i przypisaliśmy ją do stałej greet2
. Takiej funkcji nie można wywołać przed linią, w której została zdefiniowana, ponieważ nie jest hoistowana w ten sam sposób co deklaracja funkcji. Wyrażenia funkcyjne są użyteczne, gdy chcemy przekazać funkcję jako wartość (np. do innej funkcji) lub definiować je warunkowo.
Oba powyższe sposoby tworzą funkcje o identycznym zachowaniu w czasie wykonywania – różnią się głównie sposobem deklaracji i zachowaniem podczas ładowania skryptu.
Funkcje strzałkowe (arrow functions)
Funkcje strzałkowe zostały wprowadzone w ES6 (ECMAScript 2015) jako krótsza składnia definiowania funkcji. Zamiast słowa kluczowego function
, używamy symbolu =>
(tzw. „strzałki”). Funkcje strzałkowe często upraszczają kod, zwłaszcza gdy definiujemy proste funkcje zwrotne (callbacki).
Przykład zwykłej funkcji vs funkcji strzałkowej robiącej to samo:
// Zwykła funkcja mnożąca liczbę przez 2
function multiplyByTwo(x) {
return x * 2;
}
// Funkcja strzałkowa mnożąca liczbę przez 2
const multiplyByTwoArrow = (x) => {
return x * 2;
};
// Można jeszcze prościej: jeśli ciało funkcji strzałkowej to jedna instrukcja, można pominąć nawiasy klamrowe i słowo return
const multiplyByTwoArrowShort = x => x * 2;
console.log(multiplyByTwo(5)); // 10
console.log(multiplyByTwoArrow(5)); // 10
console.log(multiplyByTwoArrowShort(5)); // 10
W powyższym kodzie multiplyByTwo
, multiplyByTwoArrow
i multiplyByTwoArrowShort
dają ten sam rezultat. Widać, że funkcja strzałkowa pozwala zredukować ilość kodu, zwłaszcza w przypadku funkcji jedno-liniowych. Kilka cech funkcji strzałkowych:
- Krótka składnia: brak słowa
function
, a gdy jest tylko jeden parametr można pominąć nawiasy()
, zaś gdy funkcja od razu zwraca wynik pojedynczego wyrażenia, można pominąć klamry{}
i słoworeturn
. - Brak własnego
this
: Funkcje strzałkowe nie mają własnego kontekstuthis
ani obiektuarguments
.this
wewnątrz funkcji strzałkowej odnosi się dothis
z kontekstu otaczającego (czyli jest leksykalnie wiązane). Dzięki temu funkcje strzałkowe są wygodne np. w metodach tablicowych czy obsłudze zdarzeń, bo nie zmieniają kontekstu. Jednak z tego powodu nie nadają się do definiowania metod obiektu, gdzie zazwyczaj chcemy, bythis
wskazywało na ten obiekt (o czym więcej później). - Nie mogą być konstruktorami: Nie można użyć funkcji strzałkowej z operatorem
new
do tworzenia obiektów. Są przeznaczone głównie do krótkich funkcji wykonywanych „w locie”.
Zasięg zmiennych a zakres leksykalny
Zasięg (scope) zmiennej to obszar kodu, w którym dana zmienna jest dostępna. JavaScript ma zasięg funkcyjny dla deklaracji var
oraz zasięg blokowy dla deklaracji let
i const
(wprowadzonych w ES6). Oznacza to, że:
- Zmienna zadeklarowana słowem kluczowym
var
wewnątrz funkcji jest dostępna tylko w tej funkcji (oraz w ewentualnych funkcjach zagnieżdżonych). Jeśli zadeklarujemyvar
poza funkcją, będzie globalna (dostępna w całym skrypcie). - Zmienne zadeklarowane przez
let
lubconst
są dostępne tylko wewnątrz bloku, w którym zostały zadeklarowane (np. między{ }
pętli, instrukcji warunkowej, funkcji itp.). Nie „wynoszą” się poza ten blok.
Przykładowo:
function testScope() {
var a = 1;
let b = 2;
if (true) {
var c = 3;
let d = 4;
console.log(a, b); // 1 2 (wewnątrz funkcji widzimy a i b)
}
console.log(c); // 3 (c jest dostępne, bo var ma zasięg funkcji)
console.log(d); // BŁĄD: d is not defined (d ma zasięg bloku if i poza nim nie istnieje)
}
testScope();
console.log(typeof a); // undefined (a nie istnieje poza funkcją)
W powyższym kodzie zmienna a
jest lokalna dla funkcji testScope
, podobnie b
. Zmienna c
zadeklarowana przez var
wewnątrz bloku if
jest wciąż dostępna w całej funkcji (zasięg funkcji), natomiast d
zadeklarowane przez let
istnieje tylko w bloku if
. Poza funkcją testScope
żadne z a, b, c, d
nie jest widoczne.
W JavaScript obowiązuje zasięg leksykalny (lexical scope), co oznacza, że zasięg zmiennej zależy od tego, gdzie w kodzie została zdefiniowana, a nie od tego, skąd jest wywoływana. Funkcje zagnieżdżone mają dostęp do zmiennych zdefiniowanych w otaczających je funkcjach (czyli w ich zewnętrznym leksykalnym kontekście). Ten mechanizm prowadzi nas do koncepcji domknięć.
Domknięcia (Closures)
Domknięcie to mechanizm, dzięki któremu funkcja wewnętrzna pamięta zmienne ze swojego otaczającego kontekstu, nawet jeśli ten kontekst przestanie istnieć (funkcja zewnętrzna zakończyła wykonanie). Innymi słowy, domknięcie pozwala funkcji zachować dostęp do zasięgu leksykalnego, w którym została utworzona.
Przeanalizujmy przykład domknięcia:
function utworzLicznik() {
let licznik = 0; // zmienna lokalna funkcji utworzLicznik
return function() { // zwracamy nową funkcję (anonimową)
licznik++; // korzysta ze zmiennej spoza swojego ciała
console.log("Stan licznika: " + licznik);
};
}
const licz = utworzLicznik(); // wywołujemy utworzLicznik, zwraca funkcję, którą przypisujemy do zmiennej
licz(); // Stan licznika: 1
licz(); // Stan licznika: 2
licz(); // Stan licznika: 3
W funkcji utworzLicznik
deklarujemy zmienną licznik
i zwracamy funkcję wewnętrzną, która inkrementuje licznik
i wypisuje jego wartość. Zmienna licz
staje się tą wewnętrzną funkcją. Mimo że utworzLicznik()
zakończyło działanie, jego zmienna lokalna licznik
nie zniknęła – jest przechowywana w domknięciu związanym z funkcją przypisaną do licz
. Za każdym wywołaniem licz()
, funkcja ma dostęp do swojej zamkniętej zmiennej licznik
i może ją modyfikować. To pokazuje istotę domknięć: funkcja zachowuje dostęp do zmiennych obecnych w momencie jej definicji.
Domknięcia są powszechnie wykorzystywane w JavaScript. Przykładowe zastosowania:
- Funkcje fabrykujące (factory functions): tak jak
utworzLicznik
powyżej, możemy tworzyć funkcje generujące inne funkcje z pewnym stanem „prywatnym”. - Emulacja prywatnych zmiennych: używając funkcji i domknięć można ukryć pewne dane przed zewnętrznym dostępem (moduły, IIFE).
- Callbacki z dostępem do kontekstu: np. w obsłudze zdarzeń czy timeoutów, często funkcja callback korzysta z zmiennych spoza siebie (z kontekstu, w którym została utworzona).
Zrozumienie domknięć bywa trudne dla początkujących, ale jest kluczowe dla pełnego opanowania JavaScript. W uproszczeniu: domknięcie to „pakiet” zawierający funkcję wraz z jej leksykalnym środowiskiem (zmiennymi), które są zachowane nawet po wykonaniu tej funkcji zewnętrznej.
Funkcje jako obiekty pierwszej klasy
W JavaScript funkcje są obiektami pierwszej klasy (first-class objects/citizens). Oznacza to, że traktujemy je jak każdą inną wartość w języku. Konsekwencje takiego podejścia:
- Funkcję można przypisać do zmiennej lub stałej (jak pokazano wcześniej).
- Funkcję można przekazać jako argument do innej funkcji. Np. wiele metod tablicowych czy API przeglądarki oczekuje funkcji-callbacku. Przykład:
arr.forEach(function(x) { ... })
przekazuje funkcję anonimową do wykonania na każdym elemencie tablicy. - Funkcja może być zwrócona jako wynik z innej funkcji (co widzieliśmy w przykładzie z domknięciem).
- Funkcje można przechowywać w strukturach danych (np. w tablicy:
const operacje = [dodaj, odejmij, pomnóż]
gdziedodaj
etc. są funkcjami). - Można tworzyć funkcje w locie, np. jako wyrażenia strzałkowe
() => ...
.
Dzięki temu, że funkcje są wartościami, programowanie w JavaScript wspiera paradygmat funkcyjny. Przykładowo, możemy napisać funkcję wyższego rzędu (higher-order function), która przyjmuje inną funkcję jako parametr:
function wykonajOperacje(a, b, operacja) {
return operacja(a, b); // wywołujemy przekazaną funkcję operacja z argumentami a i b
}
function dodaj(x, y) {
return x + y;
}
console.log(wykonajOperacje(3, 4, dodaj)); // 7, przekazujemy nazwę funkcji
console.log(wykonajOperacje(3, 4, (x, y) => x * y)); // 12, przekazujemy funkcję strzałkową mnożącą
W powyższym przykładzie wykonajOperacje
to funkcja, która deleguje konkretną logikę do przekazanej funkcji operacja
. Raz użyliśmy gotowej funkcji dodaj
, a raz przekazaliśmy funkcję strzałkową anonimowo przy wywołaniu. Tego typu konstrukcje są możliwe dzięki temu, że funkcje w JavaScript są obiektami pierwszej klasy i mogą być traktowane tak samo jak np. liczby czy stringi.