Azjatycka programistka czuje się szalona i przytłoczona pracą w biurze.

Dlaczego var w JavaScript bywa problemem? var, let i const

10 min. czytania

var w nowoczesnym JavaScript jest źródłem wielu trudnych do wykrycia błędów: ma zasięg funkcyjny zamiast blokowego, podlega hoistingowi (jest „wynoszony” na górę zakresu i inicjalizowany undefined), może być wielokrotnie redeklarowany i w zasięgu globalnym staje się własnością obiektu window.

Dlatego dziś zaleca się traktować var jako konstrukcję historyczną i używać przede wszystkim const, a tam gdzie wartości się zmieniają – let.

1. Skąd problem z var? Krótki kontekst historyczny

Przez wiele lat JavaScript oferował tylko jeden sposób deklarowania zmiennych: var. Wraz ze standardem ES6 (ECMAScript 2015) dodano let i const, aby rozwiązać właśnie problemy, które generował var – przede wszystkim związane z zakresem (scope) i hoistingiem.

Dziś niemal wszędzie zaleca się: unikać var, preferować let i const.

2. var, let, const – podstawowe różnice

Najważniejsze różnice można streścić w kilku punktach:

  • zakres (scope)var ma zasięg funkcyjny, a let/const zasięg blokowy (między { i });
  • hoistingvar jest hoistowany i od razu inicjalizowany undefined, podczas gdy let/const są hoistowane bez inicjalizacji (tzw. temporal dead zone);
  • redeklaracjavar pozwala na wielokrotną deklarację w tym samym zakresie, a let/const rzucają błąd przy próbie ponownej deklaracji;
  • globalny obiekt (window) – globalne var tworzy właściwość na window, podczas gdy globalne let/const tego nie robią;
  • zmiennośćvar i let pozwalają zmieniać wartość, a const wymaga inicjalizacji i nie pozwala na ponowne przypisanie.

Te „drobne” różnice mocno wpływają na czytelność, przewidywalność i bezpieczeństwo kodu – a w konsekwencji na jakość interfejsu i dostępność aplikacji.

3. Zasięg funkcyjny var – źródło nieintuicyjnych błędów

Największym problemem var jest to, że przestrzega on zasięgu funkcji, a nie zasięgu bloku. To oznacza, że zmienna zadeklarowana wewnątrz np. if czy pętli for za pomocą var jest dostępna w całej funkcji, a nie tylko wewnątrz bloku, w którym została zadeklarowana.

Przykład:

function init() {
  if (true) {
    var label = 'Przycisk wyślij';
  }
  console.log(label); // działa! (choć większość osób się tego nie spodziewa)
}

Tutaj label jest dostępne poza blokiem if, bo var ma zasięg funkcji, a nie bloku.

Dla porównania:

function init() {
  if (true) {
    let label = 'Przycisk wyślij';
  }
  console.log(label); // ReferenceError – i to jest zachowanie, którego się spodziewamy
}

Skutki w realnym kodzie (również dla dostępności)

Zasięg funkcyjny prowadzi do sytuacji, gdzie:

  • nazwy zmiennych przypadkowo na siebie nachodzą, bo „wypływają” z bloku do reszty funkcji,
  • trudno zrozumieć, gdzie zmienna została zdefiniowana i zmieniona, co utrudnia debugging,
  • w logice interfejsu łatwo nadpisać wcześniejszy stan,
  • w logice interfejsu łatwo użyć „starej” zmiennej w nowym kontekście.

Błąd w logice sterującej widocznością, fokusem czy stanami ARIA może wprost obniżyć dostępność aplikacji (np. niewidoczny, ale semantycznie „aktywny” element, zły focus po przeładowaniu sekcji formularza).

4. Hoisting: var + undefined + brak błędu

Drugim kluczowym problemem var jest hoisting. Silnik JavaScript podczas fazy kompilacji „wynosi” deklaracje var na górę zakresu i od razu inicjalizuje je wartością undefined.

W efekcie można odwołać się do zmiennej zadeklarowanej var przed jej deklaracją:

console.log(label); // undefined – brak błędu!
var label = 'Wyślij formularz';

To zachowanie jest trudne do przewidzenia i prowadzi do subtelnych bugów, bo program nie sygnalizuje błędu, tylko działa na undefined.

Dla let i const, próba użycia zmiennej przed deklaracją kończy się błędem (tzw. temporal dead zone):

console.log(label); // ReferenceError
let label = 'Wyślij formularz';

Dlaczego to jest istotne dla jakości i dostępności?

Kiedy logika UI polega na:

  • aktualizowaniu tekstów przycisków,
  • pokazywaniu/ukrywaniu komunikatów o błędach,
  • sterowaniu atrybutami ARIA (np. aria-live, aria-invalid, aria-describedby).

działanie na undefined zamiast na spodziewanej wartości może prowadzić do:

  • wyświetlenia błędnych tekstów,
  • braku komunikatu o błędzie dla użytkownika,
  • niepoprawnego ogłoszenia stanu przez czytnik ekranu.

To ciche naruszenia dostępności, które trudno wykryć bez testów – hoisting var zwiększa ryzyko takich sytuacji.

5. Redeklaracja i nadpisywanie zmiennych

var pozwala na ponowną deklarację tej samej zmiennej w tym samym zakresie, bez zgłaszania błędu:

var state = 'loading';
// ... dużo kodu
var state = 'ready'; // OK dla var – ciche nadpisanie

W większych kodach (zwłaszcza przy pracy zespołowej) to prosta droga do przypadkowego nadpisania zmiennej i trudnych do znalezienia błędów – wszystko formalnie „działa”, tylko wartości są inne, niż zakładaliśmy.

Dla let / const ten sam kod wygenerowałby błąd – i dobrze:

let state = 'loading';
// ... dużo kodu
let state = 'ready'; // SyntaxError – wiadomo, że coś jest nie tak

Dzięki temu redukujemy liczbę cichych nadpisań i szybciej znajdujemy problem podczas developmentu.

Z perspektywy dostępności to oznacza mniej ukrytych błędów stanu interfejsu, więc mniejsze ryzyko, że UI będzie zachowywał się inaczej, niż wskazują etykiety, komunikaty czy atrybuty ARIA.

6. Zanieczyszczanie globalnego obiektu (window)

Kolejna wada var: zmienne zadeklarowane z użyciem var poza funkcją stają się własnościami obiektu globalnego (window):

var appName = 'MyApp';
console.log(window.appName); // 'MyApp'

Może to prowadzić do:

  • konfliktów nazw z innymi skryptami,
  • przypadkowego nadpisywania istniejących właściwości globalnych,
  • trudności w testowaniu i utrzymaniu kodu.

let i const zadeklarowane globalnie nie stają się własnościami window, co zmniejsza ryzyko takich konfliktów.

W serwisach z wieloma skryptami (np. widgety dostępności, analityka, reklamy) globalne zmienne var łatwo zaczynają na siebie „wchodzić”. To może wpływać również na:

  • skrypty odpowiedzialne za obsługę klawiatury,
  • fokusowanie elementów,
  • dynamikę komunikatów dostępności (np. aria-live).

Efekt uderza w użytkowników korzystających z czytników ekranu, klawiatury czy switchów.

7. Pętle, domknięcia i klasyczny bug z var

Klasyczny przykład, który latami męczył programistów: pętla for z var i funkcje wywoływane później.

for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 100);
}
// wypisze: 3, 3, 3

Dzieje się tak dlatego, że var i ma zasięg funkcji (nie pętli) i wszystkie funkcje używają tej samej zmiennej i, która po zakończeniu pętli ma wartość 3.

Z let wszystko jest dużo bardziej intuicyjne:

for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 100);
}
// wypisze: 0, 1, 2

let ma zasięg blokowy, więc w każdej iteracji mamy osobną zmienną i.

W kontekście dostępności podobny błąd może wystąpić np. w:

  • generowaniu listy przycisków „Pomiń do sekcji…”,
  • przypisywaniu handlerów do elementów nawigacji,
  • zarządzaniu tab orderem i aria-controls.

BUG z „współdzielonym” indeksem potrafi sprawić, że wszystkie przyciski odsyłają do tej samej sekcji – problem jednocześnie UX i a11y.

8. Jak let i const rozwiązują problemy var

ES6 wprowadził let i const właśnie po to, aby uporać się z problemami var. Najważniejsze cechy:

  • zasięg blokowy – zmienne żyją tylko wewnątrz { ... }, co jest spójne z intuicją i praktyką w innych językach,
  • brak cichej redeklaracji – ponowna deklaracja w tym samym zakresie kończy się błędem,
  • bezpieczniejszy hoisting – do momentu deklaracji zmienne są w temporal dead zone, więc próba użycia zbyt wcześnie rzuca błąd zamiast podstawiać undefined,
  • const wyraża zamiar – jasno komunikuje, że referencja nie będzie zmieniana.

9. Dobre praktyki: const domyślnie, let gdy trzeba, var wyjątkowo

Współczesne rekomendacje pokrywają się w wielu źródłach:

  • używaj const jako domyślnego wyboru, gdy wartość nie będzie ponownie przypisana,
  • używaj let, gdy rzeczywiście potrzebujesz zmiennej, której wartość się zmienia (liczniki, aktualny stan itp.),
  • unikaj var, chyba że masz bardzo konkretny powód (np. świadome odwołanie do window lub utrzymywanie legacy).

W nowym kodzie w praktyce nie używaj var.

10. Przykład refaktoryzacji: od var do let/const w komponentach UI

Załóżmy prosty skrypt do obsługi komunikatów błędów w formularzu (wersja z var):

// Wersja z var
var errorVisible = false;
var errorElement = document.getElementById('error-message');

function showError(message) {
  errorElement.textContent = message;
  errorElement.removeAttribute('hidden');
  errorElement.setAttribute('aria-hidden', 'false');
  errorVisible = true;
}

function hideError() {
  errorElement.setAttribute('hidden', '');
  errorElement.setAttribute('aria-hidden', 'true');
  errorVisible = false;
}

Główne problemy: errorVisible da się przypadkowo ponownie zadeklarować w innym miejscu, co zmieni logikę błędów, a globalne var zanieczyszcza obiekt window i zwiększa ryzyko konfliktów.

Refaktoryzacja:

const errorElement = document.getElementById('error-message');
let errorVisible = false;

function showError(message) {
  errorElement.textContent = message;
  errorElement.removeAttribute('hidden');
  errorElement.setAttribute('aria-hidden', 'false');
  errorVisible = true;
}

function hideError() {
  errorElement.setAttribute('hidden', '');
  errorElement.setAttribute('aria-hidden', 'true');
  errorVisible = false;
}

Korzyści:

  • errorElement jest stałą referencjąconst dobrze oddaje zamiar,
  • errorVisible zmienia się, więc używamy let i zabezpieczamy się przed ponowną deklaracją w tym samym zakresie,
  • brak var ogranicza ryzyko zanieczyszczenia globalnego obiektu.

Prosta zmiana deklaracji zmniejsza liczbę potencjalnych punktów awarii i poprawia niezawodność zachowań dostępnościowych (np. pokazywanie/ukrywanie komunikatów).

11. Czy są jeszcze sytuacje, w których var ma sens?

Spotyka się dwa główne powody: stary kod (legacy), w którym pełna refaktoryzacja jest kosztowna, oraz celowe odwołanie do obiektu globalnego, gdy świadomie chcemy utworzyć właściwość na window.

Nawet wtedy jednak warto rozważyć inne wzorce (np. jawne window.foo = ...) i ograniczać użycie var do absolutnego minimum.

W wielu poradnikach pojawia się dość radykalna, ale praktyczna zasada:

„z var nie korzystamy nigdy”.

12. Checklista dla projektów webowych i dostępności

Przy implementacji logiki interfejsu (szczególnie tej wpływającej na dostępność) stosuj poniższe zasady:

  • deklaruj zmienne domyślnie przez const – np. referencje do elementów DOM, konfiguracje, stałe teksty komunikatów,
  • używaj let dla zmiennych stanu, które rzeczywiście muszą się zmieniać (np. isOpen, currentIndex, errorVisible),
  • unikaj var, aby:
    • nie zanieczyszczać globalnego obiektu,
    • nie generować nieoczekiwanego zasięgu funkcyjnego,
    • nie polegać na hoistingu z undefined.
  • pilnuj zasięgów w logice dostępności – trzymaj zmienne sterujące widocznością i atrybutami ARIA jak najbliżej miejsca użycia (zasięg blokowy) i unikaj dzielenia „magicznych” flag po całej funkcji,
  • traktuj błędy jak sprzymierzeńcówlet/const częściej „krzyczą” (ReferenceError, SyntaxError), gdy coś jest nie tak, dzięki czemu łatwiej wyłapujesz problemy, zanim trafią do użytkowników.

Takie podejście znacząco zmniejsza ryzyko subtelnych, trudnych do wykrycia błędów logiki, które psują UX i obniżają dostępność Twojej strony lub aplikacji.