ES6 bez tajemnic. Słowa kluczowe let i const

09 lutego 2016
1 gwiadka2 gwiazdki3 gwiazdki4 gwiazdki5 gwiazdek

ES6 bez tajemnic to cykl artykułów poświęconych nowym składnikom języka JavaScript, które pojawiły się w 6. edycji standardu ECMAScript, w skrócie ES6.

Składnik ES6, który chcę dziś zaprezentować jest skromny, a jednocześnie niesamowicie ambitny.

Kiedy Brendan Eich projektował pierwszą wersję JavaScriptu w 1995 r. wiele rzeczy mu nie wyszło, łącznie z tymi elementami, którą są częścią języka do dziś. Przykładem jest obiekt Date czy automatyczna zamiana obiektów na wartość NaN, jeśli zostaną przypadkowo pomnożone. Patrząc z perspektywy czasu, jednak wiele kluczowych składników JS mu się udało: obiekty, prototypy, funkcje pierwszoklasowe z leksykalnym określaniem zakresu, domyślna modyfikowalność. Szkielet języka był dobry – lepszy niż się z początku wydawało.

Mimo to Eich podjął pewną decyzję dotyczącą kształtu JS, której konsekwencje mają wpływ na ten artykuł i którą, moim zdaniem, można uznać za pomyłkę. Chodzi o drobnostkę. Może jej nie zauważyć nawet programista używający JS od wielu lat. Pomyłka ta jest jednak istotna, ponieważ dotyczy „dobrej części” języka.

Pomyłka ta związana jest ze zmiennymi.

Problem nr 1 — bloki nie stanowią zakresów

Zakres zadeklarowanej w funkcji JS zmiennej var stanowi cała główna treść tejże funkcji. Choć zasada ta brzmi niewinnie, z dwóch względów może mieć ona bolesne konsekwencje.

Po pierwsze, zakres zmiennych zadeklarowanych w bloku nie ogranicza się tylko do tego bloku. Zakresem jest cała funkcja.

Mogłeś na to nigdy wcześniej nie zwrócić uwagi. Obawiam się jednak, że jest to jedna z tych rzeczy, które raz dostrzeżone nie przestają rzucać się w oczy. Przeanalizujmy sytuację, w której powyższa zasada prowadzi do trudnego błędu.

Powiedzmy, że mamy kod wykorzystujący zmienną o nazwie t:

function runTowerExperiment(tower, startTime) {
  var t = startTime;

  tower.on("tick", function () {
    ... kod wykorzystujący t ...
  });
  ... więcej kodu ...
}

Póki co wszystko działa jak należy. Chcielibyśmy jeszcze obliczać prędkość osiąganą przez kulę do kręgli – dodamy więc niepozorną instrukcję if do wewnętrznej funkcji zwrotnej.

function runTowerExperiment(tower, startTime) {
  var t = startTime;

  tower.on("tick", function () {
    ... kod wykorzystujący t ...
    if (bowlingBall.altitude() <= 0) {
      var t = readTachymeter();
      ...
    }
  });
  ... więcej kodu ...
}

Ojej. Nieumyślnie dodaliśmy drugą zmienną t. W dotychczas poprawnie dziającym „kodzie wykorzystującym t” nazwa t odnosi się teraz do nowej zmiennej wewnętrznej, a nie do istniejącej zmiennej zewnętrznej.

Zasięg deklaracji var w JavaScripcie przypomina działanie narzędzia Wiadro z farbą w Photoshopie. Rozciąga się on w obu kierunkach – w tył i wprzód deklaracji – aż po granicę funkcji. Ponieważ zakres zmiennej t sięga tak daleko wstecz, to musi zostać utworzony na samym początku funkcji. To tak zwane windowanie (ang. hoisting). Lubię sobie wyobrażać, że silnik JS przenosi każde wyrażenie var i function na sam początek zawierającej je funkcji przy pomocy maleńkiego kodowego żurawia.

Windowanie ma swoje zalety. Gdyby nie ten mechanizm, wiele zupełnie poprawnych technik, funkcjonujących bez zarzutu w zakresie globalnym, nie działałoby wewnątrz samowykonującej się funkcji. W tym przypadku windowanie powoduje jednak nieprzyjemny błąd: wszystkie obliczenia z wykorzystaniem zmiennej t zaczynają zwracać wynik NaN. Trudno jest go namierzyć, zwłaszcza, jeśli mamy do czynienia z większym objętościowo kodem niż zaprezentowany króciutki przykład.

Dodanie nowego bloku kodu powoduje tajemniczy błąd w kodzie znadującym się przed dodanym blokiem. Czy to ze mną jest coś nie tak, czy mamy tu do czynienia z czymś naprawdę dziwnym? Skutek nie powinien poprzedzać przyczyny.

To jednak małe piwo w porównaniu z drugim problemem z deklaracją var.

Problem nr 2 — współdzielenie zmiennych w pętlach

Możesz się domyślić, co się stanie po wykonaniu poniższego kodu. Nie ma w nim nic zaskakującego.

var messages = ["Cześć!", "Jestem stroną internetową!", "alert() to świetna zabawa!"];

for (var i = 0; i < messages.length; i++) {
  alert(messages[i]);
}

Jeśli przeczytałeś poprzednie artykuły z tej serii, to wiesz, że lubię umieszczać funkcję alert() w przykładowym kodzie. Być może wiesz również, że alert() to koszmarny interfejs API. Jest synchroniczny. Oznacza to, że podczas wyświetlania okna alertu zdarzenia wejściowe nie są obsługiwane. Kod JS — a tak naprawdę cały interfejs — jest wstrzymany dopóki użytkownik nie kliknie przycisku OK.

W związku z powyższym alert() to zwykle niewłaściwy wybór jeśli chodzi o tworzenie stron internetowych. Ja jednak korzystam z niego, ponieważ te same powody czynią go świetnym narzędziem edukacyjnym.

Dałbym się jednak przekonać i zrezygnowałbym z tej niewygody i niepożądanego działania interfejsu… jeśli mógłbym stworzyć gadającego kota.

var messages = ["Miau!", "Jestem gadającym kotem!", "Wywołania zwrotne to świetna zabawa!"];

for (var i = 0; i < messages.length; i++) {
  setTimeout(function () {
    cat.say(messages[i]);
  }, i * 1500);
}

Zobacz, jak powyższy kod nie działa!

Coś jest nie tak. Zamiast powiedzieć po kolei wszystkie trzy komunikaty, kot trzykrotnie zwraca wartość undefined.

Czy potrafisz znaleźć błąd?

Sęk w tym, że mamy tylko jedną zmienną i. Jest ona współdzielona przez pętlę i wszystkie trzy wywołania zwrotne dla przekroczenia limitu czasu. Po zakończeniu wykonywania pętli wartość i wynosi 3 (ponieważ jest to wartość messages.length) i żadne z wywołań zwrotnych nie zostało jeszcze wywołane.

Gdy zatem rozpoczyna się pierwsze z nich i wywołuje funkcję cat.say(messages[i]) to używa elementu messages[3] – jego wartość to oczywiście undefined.

Można to naprawić na wiele sposobów (oto jeden z nich), lecz nie jest to główny problem z określaniem zakresu przez deklarację var. Najlepiej byłoby w ogóle nie mieć z tą zagwozdką do czynienia.

let to nowe var

Większości błędów projektowych JavaScriptu (a także innych języków, lecz zwłaszcza JS) nie da się naprawić. Konieczność zapewnienia zgodności wstecznej uniemożliwia zmianę sposobu działania istniejącego już w sieci kodu JS. Nawet komisja standaryzacyjna nie może np. naprawić dziwnych błędów w automatycznym wstawianiu średnika w JavaScripcie. Twórcy przeglądarek po prostu nie zgodzą się na zaimplementowanie przełomowych zmian, ponieważ ich konsekwencje byłyby wymierzone w użytkowników.

Gdy zatem jakieś dziesięć lat temu Brendan Eich postanowił naprawić problem ze słowem var, miał tylko jedno wyjście.

Dodał nowe słowo kluczowe, let, za pomocą którego można było deklarować zmienne podobnie jak przy pomocy var, tyle że z lepszym określaniem zakresu zmiennej.

Może być ono użyte tak:

let t = readTachymeter();
Albo tak:
for (let i = 0; i < messages.length; i++) {
  ...
}

Słowa kluczowe let i var różnią się od siebie, zatem jeśli chciałbyś globalnie je wyszukać i zamienić, to w konsekwencji kod zależny, prawdopodobnie wbrew intencjom programisty, od dziwnego funkcjonowania słowa var przestałby działać. W większości przypadków w nowym kodzie ES6 powinno się jednak odchodzić od słowa var i korzystać w jego miejsce ze słowa kluczowego let. Stąd też hasło „let to nowe var”.

Czym dokładnie różnią się deklaracje let i var? Cieszę się, że pytasz!

  • Zmienne let mają zasięg blokowy. Zasięg zmiennej zadeklarowanej przy pomocy słowa kluczowego let stanowi tylko zawierający je blok, a nie cała funkcja.

    Deklaracja let również jest windowana, lecz w bardziej przewidywalny sposób. By naprawić przykład z runTowerExperiment wystarczy zmienić var na let. Jeśli będziesz stosował let, to nigdy nie będziesz musiał borykać się z tego typu błędem.

  • Zmienne globalne let nie są własnościami obiektu globalnego. Innymi słowy nie można się do nich odwołać poprzez window.nazwaZmiennej. Są one natiomast aktywne w zakresie niewidzialnego bloku, który hipotetycznie obejmuje cały kod JS uruchamiany na stronie internetowej.
  • Pętle typu for (let x...) tworzą nowe wiązanie dla każdego x w każdej iteracji.

    Mamy tu do czynienia z bardzo subtelną różnicą. Powyższe stwierdzenie oznacza, że jeśli pętla for (let...) jest wykonywana kilkukrotnie i, tak jak w przykładzie z gadającym kotem, zawiera domknięcia, to każde z nich będzie domykać inną kopię zmiennej pętli, a nie wszystkie domknięcia obejmujące tę samą zmienną.

    A zatem także przykład z gadającym kotem można naprawić po prostu poprzez zamianę słowa var na let.

    Odnosi się to do wszystkich trzech typów pętli for: for–of, for–in oraz tradycyjnej pętli ze średnikami znanej z języka C.

  • Błędem jest użycie zmiennej let przed jej deklaracją. Dopóki przepływ sterowania nie napotka deklaracji zmiennej, zmienna ta pozostaje niezainicjalizowana. Przykład:
    function update() {
      console.log("aktualny czas:", t);  // ReferenceError
      ...
      let t = readTachymeter();
    }
    

    Zasada ta ma pomóc ci wychwycić błędy. Zamiast wyniku NaN otrzymasz wyjątek dotyczący linijki kodu, w której pojawił się problem.

    Okres, w którym znajdująca się w zakresie zmienna pozostaje niezainicjalizowana nazywamy czasowo martwą strefą. Nie mogę się doczekać, aż to natchnione żargonowe wyrażenie podchwycą twórcy science fiction. Jeszcze tego nie zrobili.

    (Ciekawostki dotyczące wydajności: aby określić czy deklaracja została wykonana, w większości przypadków wystarczy spojrzeć na kod – silnik JavaScriptu nie musi zatem faktycznie wykonywać dodatkowego sprawdzenia po każdym odwołaniu do zmiennej, aby upewnić się, że jest ona zainicjowana. Kod znajdujący się wewnątrz domknięcia może być jednak niejasny. W takich przypadkach silnik JavaScriptu dokona sprawdzenia zmiennej w czasie wykonywania kodu. Oznacza to, że let może być nieco wolniejsze od var.)

    (Ciekawostki dotyczące alternatywnego określania zakresu: w niektórych językach programowania zasięg zmiennej zaczyna się od deklaracji, a nie sięga wstecz, by objąć cały zawierający ją blok. Komisja standaryzacyjna rozważała zastosowanie takiego określania zakresu dla deklaracji let. Wówczas odwołanie do zmiennej t, które w naszym przykładzie spowodowało ReferenceError znajdowałoby się po prostu poza zasięgiem późniejszej zmiennej let t i co za tym idzie, w ogóle by jej nie dotyczyło. Mogłoby natomiast dotyczyć zmiennej t wewnątrz wyższego zakresu. Takie podejście nie sprawdziło się jednak w połączeniu z domknięciami i windowaniem funkcji, dlatego ostatecznie z niego zrezygnowano.)

  • Ponowna deklaracja zmiennej za pomocą słowa kluczowego let powoduje błąd składni.

    Również i ta zasada ma pomóc w wykrywaniu banalnych błędów. Niemniej jednak różnica ta najprawdopodobniej przysporzy ci kłopotów, jeśli spróbujesz globalnie zamienić deklaracje let na var, gdyż dotyczy ona nawet globalnych zmiennych let.

    Jeśli masz więc kilka skryptów, w których deklarowana jest ta sama zmienna globalna, lepiej w takim przypadku pozostać przy słowie var. Zamiana go na let spowoduje, że próba wczytania drugiego w kolejności skryptu zakończy się błędem.

    Możesz także skorzystać z modułów ES6, ale teraz nie pora o tym mówić.

(Ciekawostki składniowe: w trybie ścisłym let jest słowem zarezerwowanym. W trybie zwykłym, aby zachować zgodność wsteczną, możesz zaś używać let do deklarowania zmiennych, funkcji czy argumentów – możesz nawet napisać var let = 'q'; ! Nie żebyś miał to robić. Deklaracja let let; jest natomiast całkowicie zabroniona.)

Pomijając opisane różnice, słowa kluczowe let i var działają praktycznie tak samo. Oba można wykorzystać do zadeklarowania kilku zmiennych oddzielonych przecinkami czy w destrukturyzacji.

Warto zauważyć, że deklaracje przy użyciu słowa class funkcjonują podobnie jak deklaracje let, nie var. Jeśli spróbujesz kilkukrotnie wczytać skrypt zawierający deklarację class, druga próba wczytania skryptu zakończy się błędem z powodu ponownej deklaracji klasy.

const

Jeszcze jedno!

Oprócz let w ES6 pojawia się też trzecie słowo kluczowe: const.

Zmienne zadeklarowane przy pomocy słowa const działają tak samo jak zmienne let, lecz ich wartość można przypisać tylko w momencie deklaracji. Przypisanie nowej wartości powoduje błąd składni – SyntaxError.

const MAX_CAT_SIZE_KG = 3000; // 🙀
MAX_CAT_SIZE_KG = 5000; // SyntaxError
MAX_CAT_SIZE_KG++; // dobrze kombinujesz, ale to wciąż błąd składni

Co zrozmiałe, nie można zadeklarować zmiennej const bez przypisania jej wartości.

const theFairest;  // SyntaxError, ty rozrabiako

Przestrzeń nazw do zadań specjalnych

„Przestrzenie nazw są genialne – miejmy ich więcej!” — Tim Peters, Zen Pythona

Choć tego nie widać, zagnieżdżone zakresy stanowią jedną z fundamentalnych koncepcji, na których oparte są jezyki programowania. Jest już tak od czasów… Hm… ALGOL-a? Czyli od jakichś 57 lat. Dziś ich znaczenie jest większe niż kiedykolwiek.

Przed pojawieniem się standardu ES3 w JavaScripcie istniały jedynie zakres globalny i zakres funkcji (pomińmy instrukcje with). W ES3 wprowadzono instrukcję try–catch, co wiązało się z dodaniem nowego typu zakresu, używannego tylko dla zmiennych wyjątków w bloku catch. W ES5 pojawił się zakres wykorzystywany przez funkcję eval() w trybie ścisłym. ES6 przyniósł natomiast zakres blokowy, zakres pętli for, nowy globalny zakres deklaracji let, zakres modułowy, a także dodatkowe zakresy używane przy ocenianiu domyślnych wartości argumentów.

Wszystkie te dodatkowe zakresy wprowadzane od standardu ES3 są niezbędne, by proceduralne i obiektowe składniki JavaScriptu działały równie płynnie, precyzyjnie i intuicyjnie co domknięcia – a także po to, by bezawaryjnie współpracowały z domknięciami. Być może do tej pory nigdy nie zwróciłeś uwagi na te różne reguły określania zakresu. Oznacza to, że język dobrze spełnia swoje zadanie.

Czy mogę już dziś korzystać z deklaracji let i const

Owszem. Aby używać ich w kodzie sieciowym, musisz skorzystać z kompilatora ES6, np. Babel, Traceur lub TypeScript (Babel i Traceur nie obsługują jeszcze czasowo martwej strefy).

Środowisko io.js obsługuje deklaracje let i const jedynie w trybie ścisłym. To samo tyczy się Node.js, przy czym wymagana jest także opcja – -harmony.

Pierwsza wersja słowa kluczowego let została zaimplementowana w Firefoksie przez Brendana Eicha dziewięć lat temu. Składnik ten został jednak gruntownie przeprojektowany w trakcie standaryzacji języka. Shu-yu Guo pracuje obecnie nad aktualizacją naszej implementacji do nowego standardu. Za przegląd kodu odpowiedzialny jest m.in. Jeff Walden.

Jesteśmy już na ostatniej prostej. Nasza wielka podróż szlakiem ES6 pomału dobiega końca. Jeden z ostatnich artykułów poświęcimy prawdopodobnie najbardziej wyczekiwanemu elementowi, który pojawił się w nowym standardzie JS. W najbliższym wpisie zaś poszerzymy wiedzę na temat słowa kluczowego new, które jest po prostu super. Odwiedzaj nas zatem często, by nie przegapić kolejnego artykułu Erica Fausta wyczerpująco omawiającego podklasy w ES6.

Autor: Jason Orendorff

Źródło: https://hacks.mozilla.org/2015/07/es6-in-depth-let-and-const/

Tłumaczenie: Joanna Liana

Treść tej strony dostępna jest na zasadach licencji CC BY-SA 3.0

Zobacz również:

Dyskusja

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