Zasięg zmiennych w JavaScript

Zmienne to jeden z najważniejszych elementów każdego języka programowania. Ważne jest jednak dokładne zrozumienie zakresów, w jakim „obowiązują” dane zmienne, co w przypadku JavaScript jest nieco skomplikowane.

W wielu popularnych językach programowania występuje zasięg blokowy, dlatego część osób może być początkowo zaskoczonych faktem, że w JavaScript podstawowym zakresem zmiennych jest zakres funkcji. Takie informacje znajdziemy w większości książek i w wielu kursach dla początkujących programistów JavaScript.

Nie do końca jest to jednak prawdziwe stwierdzenie. W dobie coraz powszechniej stosowanego standardu ECMA Script 6 programista ma do dyspozycji zarówno zakres funkcyjny jak i zakres blokowy (deklaracje let, const). Jednakże już przed ES6 istniała pewna wyjątkowa sytuacja, gdy mieliśmy do czynienia z zakresem blokowym, a nie funkcyjnym, mimo braku deklaracji słowami let lub const – dotyczyło to tzw. przechwytywania wyjątków w konstrukcjach try – catch. Ponad to, jeśli ktoś zechce zastosować JavaScript również po stronie serwera, np. w nodeJS to spotka go jeszcze inny problem, związany z zakresem plikowym i koniecznością jawnego deklarowania zmiennych o zasięgu globalnym.

Na dokładkę, aby dobrze zrozumieć zmienne w JS należałoby również poświęcić nieco czasu na kwestie związane z domknięciami (clousures) – będzie to jednak poruszone w oddzielnym artykule, dlatego w tym tekście pominięto problematykę domknięć i przechodzenia przez hierarchię zakresów.

Jak widać kwestia zasięgu zmiennych w JavaScript jest nieco skomplikowana…

W niniejszym artykule postaram się w krótki, zwięzły sposób zobrazować problematykę zasięgu zmiennych w standardzie ECMA Script z analizą różnych przykładów, które łatwo można przetestować w konsoli JS przeglądarki internetowej (np. Chrome czy Firefox).

1. „Tradycyjny” zasięg zmiennych zamknięty w funkcji

var sum = 10;
function add (a,b) {
    var sum = a + b;
    return sum;
}

sum; //10
add(2,4); //6
sum; //10

Opiszmy po kolei co w zasadzie się tutaj wydarzyło i skąd takie, a nie inne wyniki:

  • Na początku deklarujemy zmienną sum, która stanowi zmienną o zasięgu globalnym, gdyż nie jest zadeklarowana w żadnej funkcji (dokładniej mówiąc zmienna ta staje się właściwością obiektu globalnego, którym w przypadku przeglądarki internetowej jest obiekt window, czyli dostęp do zmiennej sum tak na prawdę odbywa się poprzez odczytanie właściwości window.sum). Następnie zmiennej sum przypisywana jest wartość liczbowa: 10.
  • Kolejnym etapem jest zadeklarowanie funkcji o nazwie add, która pobiera dwa argumenty (a,b). Wewnątrz funkcji add deklarujemy nową zmienną sum i przypisujemy jej wartość matematycznej operacji dodawania argumentu „a” i „b”, a następnie zwracamy wartość zmiennej sum. Zmienna ta została jednak zadeklarowana nie jako zmienna globalna, lecz jako zmienna obowiązująca wyłącznie w zasięgu funkcji add, co obrazują dalsze testy.
  • Wpisanie w konsoli „sum;” wyświetli aktualną wartość zmiennej sum. Odnosi się to jednak do zmiennej globalnej (window.sum), dlatego w konsoli zobaczymy wartość „10”.
  • Wywołanie funkcji add z parametrami (2,4) spowoduje zwrócenie wartości zmiennej sum będącej zmienną lokalną funkcji add. W konsoli zobaczymy wartość „6”. Operacja ta nie zmodyfikowała jednak zmiennej globalnej o tej samej nazwie („sum”), dlatego gdy w następnym kroku ponownie wpiszemy w konsoli „sum;” to po raz kolejny odczytamy wartość zmiennej globalnej, czyli „10”.

2. Uważaj na przypadkowe deklarowanie zmiennych globalnych!

Zdarza się, szczególnie gdy mamy małe doświadczenie w pisaniu kodu, że łatwo możemy przypadkowo tworzyć zmienne o zakresie globalnym. Prześledźmy poniższy kod:

var sum = 10; //zmienna globalna
function add (a,b) {
    sum = a + b; //Ups... tutaj nie mamy "var"
    return sum;
}

sum; //10 czyli jest w porządku
add(2,4); //6 tak, takiego wyniku się spodziewamy
sum; //6 Ups... funkcja add zmodyfikowała zmienną globalną...
add(3,5) //8 Jest ok
sum; //8 Hmm, chyba ponownie zmodyfikowaliśmy zmienną globalną...

Hmm co się tutaj stało… otóż w czasie pisania kodu funkcji add zapomnieliśmy przy deklarowaniu zmiennej sum o słowie kluczowym var (są również inne, ale na razie pozostańmy jedynie przy var). W tym momencie JavaScript sprawdzi, czy w zasięgu wyższym (tutaj globalnym) znajduje się taka zmienna („sum”) i jeśli tak to zmodyfikuje jej wartość. Gdyby nie było zmiennej globalnej „sum” to pierwsze wywołanie funkcji add stworzy taką zmienną w zasięgu globalnym. Na razie przyjmijmy założenie, że wszystkie tworzone w tym artykule funkcje mają dostęp do wszystkich zmiennych globalnych (zjawisko to zostanie opisane w osobnym artykule i wtedy wyjaśni się dokładnie jak to wszystko działa i dlaczego). Z tego powodu funkcja add może zmodyfikować zmienną globalną sum.

3. Wyjątek od reguły zasięgu funkcyjnego:

Aż do „zadomowienia” się na dobre standardu EcmaScript 6 i tym samym wprowadzenia słów kluczowych „let” i „const” przypadek wystąpienia zakresu blokowego w JavaScript występował jedynie w konstrukcji przechwytywania wyjątków:

function myFunc(){
    try {
    //kod wykonujący jakieś zadanie, które może zakończyć się niepowodzeniem
    } catch (err) {
       console.log(err.message); //zmienna "err" dostępna tylko  blogu catch
    }
    return err.message; //ReferenceError: err is not defined
}

W powyższym kodzie, w bloku try { … } wykonywane są instrukcje, które z mniejszym lub większym prawdopodobieństwem mogą zakończyć się błędem, i chcąc go bezpiecznie obsłużyć stosujemy konstrukcję catch { … }, która zostanie wykonana w razie wystąpienia błędu (tzw. „wyjątku”). Wewnątrz bloku catch dysponujemy zmienną (w tym wypadku nazwaliśmy ją „err”),  która przechowuje nazwę błędu i jego treść (właściwość err.name i err.message, choć obiekt ten może również posiadać inne właściwości). Zmienna err nie jest jednak dostępna poza blokiem catch mimo, że blok ten nie jest przecież definicją funkcji.

4. Zasięg blokowy w EcmaScript 6 (i nowszych)

Począwszy od wejścia w życie standardu EcmaScript 6 programiści JavaScript zyskali nowe narzędzie – możliwość jawnego deklarowania zmiennych o zasięgu blokowym, wyznaczanym parą nawiasów klamrowych „{ … }”. Deklaracje takie wykonuje się przy użyciu słów kluczowych let lub const. Najogólniej mówiąc słowo let można porównać do funkcyjnego zasięgu zmiennych deklarowanych słowem var, natomiast const odnosi się do deklarowania tzw. stałych (nie są to jednak do końca takie same stałe jak w innych językach programowania, gdyż w pewnym stopniu mogą być one modyfikowane, ale o tym w innym artykule). Na razie pozostańmy przy tym, że zarówno let jak i const deklarują zmienne blokowe bez analizy dalszego zachowania się takich zmiennych w czasie prób ich zmieniania, jak również na razie (na tym etapie) pomijamy kwestię hoistingu zmiennych var, let i const.

var x = 10, y = 20;
if (x > 0 && y > 0) {
    let sum = x + y;
    console.log(sum); //30, więc jest w porządku
}
console.log(sum); //ReferenceError... zmienna sum nie istnieje

Gdy powyższy kod wkleimy w konsolę to uzyskamy wyniki pokazane jako komentarze, czyli odpowiednio „30” oraz „ReferenceError”. Z czego to wynika, przecież w kodzie nie ma żadnej deklaracji funkcji, więc zmienne powinny być zmiennymi globalnymi…

Otóż pojawiło się tutaj słowo kluczowe let, które deklaruje zmienną sum wyłącznie w obrębie bloku, czyli między klamrami: { … }. W tym wypadku zasięg zmiennej sum dotyczy zakresu blokowego instrukcji „if”. Poza klamrą zamykającą blok deklaracji let zmienna nie jest już dostępna, a więc ostatnie wywołanie console.log(sum) próbuje odnieść się do nieistniejącej zmiennej globalnej.

Zmienne o zasięgu blokowym to bardzo przydatne i od dawna wyczekiwane narzędzie w JavaScript, które pozwala w prosty sposób uporządkować kod. Często wykorzystuje się to w instrukcjach warunkowych czy pętlach. Przed pojawieniem się let i const programiści JavaScript uciekali się do „sztucznego” tworzenia zasięgu blokowego poprzez tworzenie dodatkowych funkcji natychmiastowych. W oddzielnym wpisie omówię dokładniej zagadnienie stosowania zarówno deklaracji let jak i const, wraz z omówieniem kilku specyficznych sytuacji, jak chociażby deklaracja let wewnątrz pętli for (co często rodziło problemy przy modyfikacji DOM z użyciem pętli).

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *