Kiedy let, a kiedy var w JavaScript
Popularna rada brzmi: zawsze let, nigdy var. Ale każde z nich komunikuje co innego o intencji autora. Notatka o tym, kiedy var wciąż ma sens i jak wybrać między nimi.
Powtarzana rada: zawsze let, nigdy var. Najczęściej wystarcza - ale są miejsca, w których wybór między tymi słowami niesie informację dla czytelnika. O nich jest ta nota.
#Block vs function scope
let jest blokowy - żyje tylko między swoimi { }. var jest funkcyjny - sięga do najbliższej funkcji albo do globalnego scope:
function f() {
if (true) {
var a = 1;
let b = 2;
}
console.log(a); // 1 - var jest widoczny w całej funkcji
console.log(b); // ReferenceError - let zamknięte w bloku
}(Mechanikę scope chain pokazuję w nocie o lexical scope, TDZ przy let w poście o hoistingu.)
#Po co istnieje var, skoro mamy let
Bo var mówi czytelnikowi coś, czego let nie umie powiedzieć: „ta zmienna jest dla całej funkcji”. Tak argumentuje Kyle Simpson - autor serii You Don’t Know JS.
Wyobraź sobie funkcję, której pierwszą linią jest let total = 0. Mechanicznie total ma scope całej funkcji - bo na samej górze nie ma żadnego węższego bloku, który by go ograniczył. Ale jako czytelnik nie wiesz tego z samej linii deklaracji. Może gdzieś niżej znajdzie się blok, w którym total miało być zamknięte? Trzeba przejrzeć resztę. Z var total = 0 nie ma tej niepewności - var od razu mówi: „dla całej funkcji”.
To pozycja kontrowersyjna - większość styleguide’ów mówi po prostu „nigdy var”. Warto jednak wiedzieć, że istnieje druga strona: argument, że var i let nie są tym samym z perspektywy czytelnika kodu, nawet jeśli silnik traktuje je identycznie.
#Trzy reguły, które z tego wypadają
W pętli for - zawsze let. Wtedy każda iteracja dostaje własne i zamiast jednej wspólnej zmiennej na całą pętlę. Bez tego callbacki tworzone wewnątrz pętli (np. setTimeout) odczytują tę samą wartość - mechanika opisana w poście o closure.
Zmienna dla całej funkcji - var. Czytelnik widzi z miejsca: ta zmienna jest dostępna w całej funkcji, nigdzie nie zamknięta w wewnętrznym bloku. (Jeśli argument o var z poprzedniej sekcji ci nie odpowiada, używaj let konsekwentnie na samym początku funkcji.)
Zmienna potrzebna tylko dla kilku linii - let w samodzielnym bloku. Otaczasz fragment kodu w { } (blok bez if-a, bez pętli - po prostu nawiasy klamrowe sam dla siebie), deklarujesz w nim zmienną; po wyjściu z bloku zmienna przestaje istnieć:
function process(input) {
// ... reszta logiki
{
const cleaned = input.trim().toLowerCase();
log(cleaned);
}
// cleaned tu nie istnieje
}Sam tworzysz dla niej scope. Intencja jest widoczna: ta zmienna istnieje tylko po to, żeby kilka linii zadziałało.
#Słowo na koniec
let i var to nie dwa synonimy do wyboru - to dwa różne sygnały dla czytelnika:
let- „to żyje krótko, w tym bloku”var- „to należy do całej funkcji”
Domyślny wybór („nigdy var”) jest prosty i bezpieczny. Drugi (var tam, gdzie zmienna obejmuje całą funkcję, let tylko w blokach) wymaga więcej dyscypliny od autora, ale daje czytelnikowi więcej w jednej linii. To wybór, nie reguła z dokumentacji.
Sam piszę głównie const i let. Po var sięgnąłem raz - w skrypcie z <head> strony, który leci jeszcze przed startem React. To standardowa technika w aplikacjach z motywami: skrypt sprawdza cookie i ustawia kolor tła, zanim React zacznie renderować widok. Dzięki temu użytkownik z preferencją ciemnego motywu nie widzi krótkiego błysku jasnego po wejściu na stronę. Taki skrypt nie przechodzi przez żaden build - leci do przeglądarki tak, jak go napisałem. var działa w każdej, nawet bardzo starej, i dlatego po nim sięgnąłem.
Co ciekawe, część zmiennych była tam widoczna dla całego skryptu - i tam var znaczył dokładnie to, o czym pisał Simpson w poprzedniej sekcji: „to dla całego skryptu”.
(Co const chroni - i czego nie - opisałem osobno w nocie o const.)