Funkcje i zmienne - tematy dodatkowe
Poniżej zajmiemy się dodatkowymi tematami związanymi z działaniem funkcji.
Zasięg zmiennych
Zmienne dzielimy na globalne i lokalne.
Do tych pierwszych dostęp ma cały skrypt. Te drugie - jak sama nazwa wskazuje - są lokalne.
Podczas pisania naszych skryptów powinniśmy starać się używać jak najmniej zmiennych globalnych, ponieważ są narażone na potencjalne niepożądane zmiany. Oczywiście wszystko zależy od zaawansowania danego skryptu.
W przypadku const i let zasięg zmiennych jest blokowy, co oznacza "od klamry do klamry".
let x = "Jola";
{
let a = "Ala";
console.log(a); //Ala
console.log(x); //Jola
}
{
let a = "Ola"; //zmienna lokalna w tym bloku
console.log(a); //Ola
console.log(x); //Jola
}
console.log(a); //error - nie ma takiej zmiennej
console.log(x); //Jola
W przypadku zmiennych deklarowanych za pomocą var ich zasięg jest funkcyjny.
Zasada jest podobna do const i let, z tym, że tutaj zasięg definiuje ciało danej funkcji:
var x = "Jola";
function fn1() {
var a = "Ala";
console.log(a); //Ala
console.log(x); //Jola
}
function fn2() {
var a = "Ola";
console.log(a); //Ola
console.log(x); //Jola
}
console.log(a); //błąd bo zmienna a jest dostępna tylko w funkcji
console.log(x); //Jola
Takie zamknięte środowisko może mieć spokojnie swoje zmienne, które mogą nazywać się tak samo jak zmienne ze środowiska zewnętrznego:
var x = 1;
function show() {
//deklarujemy zmienną x dostępną tylko w tej funkcji
//zbieżność nazw z zewnętrzną zmienną x nie przeszkadza, bo to nie są te same zmienne
var x = 2;
console.log(x); //2
}
show();
console.log(x); //1 - jesteśmy poza funkcją
let x = 1;
{
let x = 2;
console.log(x); //2
}
console.log(x); //1
W przypadku funkcji zmiennymi lokalnymi stają się też parametry.
const a = 10;
const b = 20;
function myF(a, b) {
console.log(a, b); //"Ala", "Ola"
}
myF("Ala", "Ola");
console.log(a, b); //10, 20
Zawsze przy tworzeniu nowych zmiennych używaj var/let/const. W przeciwnym razie możemy nieumyślnie nadpisać już istniejącą zmienną:
var sum = 1;
function show(a, b) {
//funkcja show zaczyna definiować swoje zmienne.
//musi zadeklarować zmienne parametrów a i b
//czy poniżej musi zdefiniować nową zmienną sum?
//Nie, ponieważ 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
let sum = 1;
{
sum = 2 + 3;
console.log(sum); //5
}
console.log(sum); //5
Zarówno funkcje jak i bloki mogą być spokojnie zagnieżdżone w innych funkcjach i blokach.
Działać tu będzie ten sam mechanizm co powyżej.
Funkcje/bloki mają dostęp do swoich zmiennych i zmiennych z zewnątrz.
Zewnętrzne środowisko nie ma dostępu do zmiennych w zagnieżdżonych blokach i funkcjach.
Tworzy się więc pewna zagnieżdżona hierarchia.

var lv0 = 0;
function fn1() {
var lv1 = 1;
console.log(lv0); //0
console.log(lv2); //błąd - nie ma dostępu do zmiennych w wewnętrznej funkcji
function fn2() {
var lv2 = 2;
console.log(lv0, lv1, lv2); //0, 1, 2
}
}
console.log(lv1, lv2); //błąd - zewnętrzne środowisko nie ma dostępu
let lv0 = 0;
{
let lv1 = 1;
console.log(lv0); //0
console.log(lv2); //błąd - nie ma dostępu do zmiennych w wewnętrznym bloku
{
let lv2 = 2;
var lv2B = 2;
console.log(lv0, lv1, lv2); //0, 1, 2
}
}
console.log(lv1, lv2); //błąd - zewnętrzne środowisko nie ma dostępu
console.log(lv2B); //uwaga - lv2B to var. Mamy tutaj dostęp, bo zmienna ta nie jest wewnątrz funkcji
Domknięcia - closures
Gdy funkcja zaczyna działać, tworzy swoje środowisko ze swoimi zmiennymi. Tymi zmiennymi są zmienne lokalne (zadeklarowane za pomocą var/let/const) oraz parametry funkcji. 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() {
a++; //zmienna globalna
console.log(`a: ${a}`);
}
myF(); //a: 1
myF(); //a: 2
myF(); //a: 3
myF(); //a: 4
Za każdym razem gdy odpalamy nasza funkcję myF()
, zwiększamy zmienną braną z zewnątrz.
Dodajmy do naszego przykładu dodatkową zmienną lokalną:
let a = 0;
function myF() {
let b = 0;
a++;
b++;
console.log(`a: ${a}, b: ${b}`);
}
myF(); //a: 1, b: 1
myF(); //a: 2, b: 1
myF(); //a: 3, b: 1
myF(); //a: 4, b: 1
Za każdym wywołaniem naszej funkcji zmienna lokalna tworzona jest na nowo, natomiast zmienna globalna jest brana z zewnątrz, dzięki czemu między kolejnymi odpaleniami naszej funkcji trzyma swój stan. Zewnętrzne środowisko dla naszej funkcji staje się więc swoistym schowkiem.
Spróbujmy ten sam manewr co powyższy przeprowadzić na funkcji zagnieżdżonej o jeden poziom:
function firstFn() {
let a = 0;
function myF() {
let b = 0;
a++;
b++;
console.log(`a: ${a}, b: ${b}`);
}
myF(); //a: 1, b: 1
myF(); //a: 2, b: 1
myF(); //a: 3, b: 1
myF(); //a: 4, b: 1
}
firstFn();
Zasada jak widać nic się nie zmienia - zmienił się tylko zasięg do którego funkcja myF() ma dostęp.
Funkcje mogą zwracać dowolne rzeczy - w tym inne funkcje. Dla takich funkcji także możemy wykorzystać "zewnętrzne środowisko" do zapamiętywania stanu między kolejnymi wywołaniami:
function firstFn() {
let a = 0;
return function() {
a++;
console.log(a);
}
}
const c = firstFn(); //podstawiam wynik funkcji firstFn czyli funkcję z linii 4
c(); //1
c(); //2
c(); //3
c(); //4
Dzięki wykorzystaniu zewnętrznego środowiska (którym dla naszej zwróconej funkcji jest funkcja firstFn()
i zewnętrzne) nasza funkcja c()
może zapamiętać swój stan między kolejnymi odpaleniami.
Dodatkowo zwrócona funkcja ma dostęp do zasięgu funkcji firstFn()
i jej zewnętrznego zasięgu. Równocześnie globalny zasięg nie ma dostępu do ciała funkcji firstFn()
. Oznacza to, że zwróconej funkcji zasięg firstFn()
staje się swoistym zamkniętym schowkiem, do którego ma dostęp tylko zwrócona funkcja.
Zasada ta tyczy się nie tylko funkcji, ale także obiektów:
function mySafeArea() {
let x = 100;
const show = () => {
console.log(`x: ${x}`);
}
const setX = (nr) => {
x = nr
}
//poniżej zwracany obiekt, który ma dostęp do powyższych zmiennych i metod
//dzięki którym może te zmienne modyfikować
return {
setX,
show
}
}
const ob = mySafeArea();
ob.x = 150; //nie mogę tak ustawić zmiennej bo nie mam do niej dostępu z zewnątrz
ob.show(); //x: 100
ob.setX(150);
ob.show(); //x: 150
Technika ta ma zastosowanie w momencie, gdy chcemy coś chronić przed zewnętrznym środowiskiem.
IIFE
IIFE (Immediately-invoked function expression) - czyli samo wywołujące się wyrażenie funkcyjne to wzorzec funkcji, która sama się wywołuje.
(function() {
...
})();
Aby zrozumieć powyższy zapis, stwórzmy proste wyrażenie funkcyjne:
const fn1 = function() {...}
const fn2 = function(a) {
console.log(a);
}
Żeby teraz wywołać powyższe funkcje, musimy podać ich nazwy, za którymi wstawimy parę nawiasów:
fn1();
fn2("ala");
Częstokroć jednak wcale nie będziemy potrzebować nazwy funkcji, bo wewnętrzny kod chcielibyśmy wykonać tylko jeden raz i to od razu.
Czyli po definicji funkcji chcielibyśmy od razu ją wywołać:
function() { ... }(); //zwróci błąd składni
Powyższy kod zwróci błąd. Aby to naprawić, wystarczy skorzystać z zasad matematyki, gdzie nawiasami okrywamy część równania, która powinna się wykonać w pierwszej kolejności:
2 + 2.toFixed() //zwróci błąd
(2 + 2).toFixed() //na początku wykonaj równanie, potem toFixed()
Podobnie do powyższego równania wystarczy 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:
Alternatywnym zapisem dla powyższego jest wzór zalecany przez Douglasa Crockforda - jednego z guru JavaScript.
(function() {...}()); //nawiasy w środku
Czemu jest on zalecany? Rozchodzi się o psie jajka, ale to tylko sprawy estetyczne.
A do czego to może być wykorzystane?
Zakres let i const w przeciwieństwie do var mają zasięg blokowy:
{
let a = 20;
var b = 10;
}
console.log(a); //błąd - nie mamy dostępu
console.log(b); //10
Jeżeli chcemy ograniczyć zasięg var, musimy skorzystać z funkcji:
(function() {
let a = 20;
var b = 10;
})();
console.log(a); //błąd - nie mamy dostępu
console.log(b); //błąd - nie ma dostępu
W przeszłości - gdy używało się głównie var, stosowanie IIFE było dość powszechną metodą ograniczania zasięgu. W dzisiejszych czasach wielu programistów zabezpiecza tak całe swoje kody.
Gdy zaczniemy dzielić nasz kod na oddzielne pliki (moduły) nasz kod będzie automatycznie okrywany podobnym zapisem (1).
Problem ze zmiennymi
W rozdziale o zmiennych powiedziałem Tobie, żebyś zawsze tworzył zmienne używając słowa kluczowego.
for (i=0; i<10; i++) {
console.log(i);
}
Powyższy kod - mimo, że bardzo krótki - zawiera błąd. Stworzyliśmy zmienną i
, ale nie używając słowa kluczowego. Pętla się wykona, natomiast rezultat może być dla nas nieprzewidywalny. Nieumyślnie stworzyliśmy zmienną globalną, która z automatu przypisana jest do obiektu window
:
for (i=0; i<10; i++) {
console.log(i);
}
console.log(i); //10
console.log(window.i); //10
Spójrzmy na jeszcze jeden przykład:
<button>przycisk 1</button>
<button>przycisk 2</button>
<button>przycisk 3</button>
<button>przycisk 4</button>
//pobieram "tablicę" przycisków
const buttons = document.querySelectorAll("button");
for (el of buttons) {
//dodaję im kliknięcie, które pokaże ich wewnętrzny tekst
el.addEventListener("click", () => {
console.log(el.innerText);
el.style.background = "dodgerblue";
});
}
Klikając na dowolny z przycisków będziesz zmieniał tylko ostatni. Czemu tak się dzieje? Znowu zawinił ten sam błąd. Robiąc pętlę po przyciskach skorzystałem ze zmiennej el
. Tak samo jak w pierwszym przykładzie nie utworzyłem jej korzystając ze słowa kluczowego let/var
, więc automatycznie stała się globalną. Gdybyśmy wypisali ją po zakończeniu pętli, efekt byłby podobny jak w przypadku pierwszego przykładu - wskazywała by na ostatni element tablicy.
Po zakończeniu pętli my jako użytkownicy klikamy na przycisk, który uruchamia funkcję odnoszącą się do tej zmiennej globalnej.
Wracamy więc do kluczowej zasady: zawsze twórz zmienne używając słów kluczowych.
const buttons = document.querySelectorAll("button"); //pobieram "tablicę" przycisków
for (let el of buttons) {
//dodaję im kliknięcie, które pokaże ich wewnętrzny tekst
el.addEventListener("click", () => {
console.log(el.innerText);
el.style.background = "dodgerblue";
});
}
Ale uwaga. Jeżeli kiedykolwiek będziesz musiał napisać podobny kod z wykorzystaniem starej składni, wtedy nawet użycie słowa kluczowego nie rozwiąże problemu:
const buttons = document.querySelectorAll("button");
for (var el of buttons) {
el.addEventListener("click", function(e) {
console.log(el.innerText);
el.style.background = "dodgerblue";
});
}
console.log(el.innerText); //przycisk 4
Zasięg zmiennych var
określa ciało funkcji a nie blok, stąd zmienna el
"wypadła" poza pętlę stając się zmienną globalną. Żeby to naprawić moglibyśmy skorzystać z IIFE:
const buttons = document.querySelectorAll("button");
for (var el of buttons) {
(function(btn) { //działam na zmiennej lokalnej btn która wskazuje na zmienną el
btn.addEventListener("click", function(e) {
console.log(btn.innerText);
btn.style.background = "dodgerblue";
});
})(el);
}
console.log(el.innerText); //przycisk 4
W dzisiejszych czasach traktuj to raczej jako ciekawostkę, chociaż któż wie co tam spotakasz na swojej ścieżce kariery...
Funkcje zwrotne
Do parametrów funkcji możemy przekazywać dowolny typ danych:
function show(a) {
console.log(a)
}
show(1); //przekazaliśmy numer
show("Ala"); //przekazaliśmy tekst
show({a : 2}); //przekazaliśmy obiekt
show([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().
Dość często przy takich działaniach używamy tak zwanych funkcji anonimowych (funkcji które nie mają własnej nazwy):
[1,2,1,4].sort((a, b) => {
return a - b
})
[1,2,3,4].forEach(el => {
console.log(el);
});
element.addEventListener("click", () => {
alert("klik");
});
Ale nic nie stoi za przeszkodą by używać referencji:
function sortFn(a, b) {
return a - b;
}
function printText(el) {
console.log(el);
}
function klik() {
alert("klik");
}
[1,2,1,4].sort(sortFn);
[1,2,3,4].forEach(printText);
element.addEventListener("click", klik);
Mechanizm funkcji zwrotnych 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(() => {
console.log("...");
});
function randomBetween(min, max, fn) {
const nr = Math.floor(Math.random()*(max-min+1)+min);
fn(nr);
}
randomBetween(10, 20, res => {
console.log("Losowa 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], res => {
console.log("Suma liczb w tablicy to: " + res);
});
Jak wiemy, funkcja może 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((a) => {
console.log(a + " ma kota") //Ala ma kota
})
Do tematu funkcji zwrotnych jeszcze wrócimy w jednym z kolejnych rozdziałów.
Rekurencja
Funkcja rekurencyjna to taka funkcja, która wywołuje sama siebie.
function myFn() {
console.log("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;
}
}
console.log(fibonacci(10)); //[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
Naszym celem zazwyczaj nie jest rysowanie ani fraktali, ani wyliczanie ciągów matematycznych. Porozmawiajmy więc o bardziej realnych zastosowaniach.
Jedno z zastosowań rekurencji spotyka się w przypadku pracy z interwałami. Gdy odpalany 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++) {
console.log(i);
}
setTimeout(() => loop(), 2000); //po 2 sekundach ponownie odpalam loop
}
loop();
Dokładnie ten przykład opisałem w rozdziale o interwałach.
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] ] ] ];
Równocześnie w ramach treningu (albo i wstecznej kompatybilności) nie możesz użyć żadnej z dostępnych metod takich jak flat() czy reduce(). Pozostaje więc 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;
}
console.log( 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. Realny przykład opisałem na swoim blogu.
Trening czyni mistrza
Jeżeli chcesz sobie potrenować zdobytą wiedzę, zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania