ES6#2 Var, Let, Const - zasięg zmiennych w JS

W poprzednim poście opisałem jak wygląda obiektowość, jakie są jej problemy oraz przedstawiłem jak jest to rozwiązane w ES6. Tym razem pokażę jakie utrudnienia możemy napotkać w JavaScript zajmując się zasięgiem zmiennych. Wrócimy krótko do zagadnienia jakim jest strict mode oraz IIFE, zainteresujemy się zmiennymi globalnymi oraz dowiemy się jak let oraz const zastępuje var. Zaczynajmy!

# Zasięg zmiennych i funkcji w JavaScript

Na początku powinniśmy wyjaśnić sobie jak działa zasięg funkcji oraz zmiennych. Czy zmienna zadeklarowana poprzez var jest dostępna poza tą funkcją?

 
function drive(a){
  var b = 2;
  
  function start(){
    //...
  }
  var c = 3;
}
start(); // błąd
console.log(a); //błąd
console.log(b); //błąd
console.log(c); //błąd

W powyższym przykładzie stworzyliśmy funkcję drive i zagnieżdżoną funkcję start. W funkcjach zasięg zmiennych i funkcji zagnieżdżonych zamyka się na najbliższej zewnętrznej funkcji. To właśnie dlatego wywołując wewnętrzną funkcję poza funkcją drive otrzymaliśmy błąd, który mówi że taka funkcja nie istnieje. To samo tyczy się zmiennych, które są zadeklarowane wewnątrz tej funkcji.

Spróbujmy teraz troszkę zmodyfikować nasz kod. Sprawdzimy teraz jak to jest ze zmiennymi globalnymi.

 
var z = 100;
function drive(){
  
  function start(){
    
    function engineStart(){
      console.log(z);
    }
    engineStart();
  }
  start();
}
drive();//100

W powyższym kodzie mamy podwójnie zagnieżdżoną funkcję. Wewnątrz drive, funkcja engineStart poszukuje zmiennej z, której nie znajduje w swoim zasięgu zmiennych, dlatego szuka jej o jeden stopień wyżej, czyli w funkcji start. Nie ma tam tej zmiennej, więc przechodzi później do funkcji drive. Tam też jej nie znajduje, więc udaje się do zasięgu globalnego i wypisuje tą zmienną. W przypadku, którym mielibyśmy tą zmienną zadeklarowaną jako globalną i drugą, o takiej samej nazwie wewnątrz funkcji, engineStart wypisałaby pierwszą na którą natrafi.

Czy jest to pożądane zachowanie, czy raczej niebezpieczny efekt uboczny? Bez takiego mechanizmu programowanie w JavaScript byłoby niesamowicie utrudnione, jednak musimy bardzo uważać, żeby nie sprawiło to nieoczekiwanych kłopotów. Spójrzmy na przykład:

 
for (var i = 0; i < 5; i++){
    console.log(i);
}
console.log('na koniec pętli:', i);

Deklarujemy zmienną i w bloku pętli for, więc zakładamy, że na zewnątrz tego bloku nie będzie dostępna. Niestety wynik tego kodu jest dosyć nieoczekiwany:

 
0
1
2
3
4
"na koniec pętli:5"

Dlaczego jest to złe? Dobrą praktyką pisania kodu jest ukrywanie kodu w funkcjach i tworzenie minimalnej ilości zmiennych. W takim przypadku musielibyśmy tworzyć inną zmienną do każdej pętli. Co spowodowało taki wynik? JavaScript ma tylko jeden mechanizm zasięgu i są to funkcje. A więc jak możemy naprawić nasz kod?

# IIFE, strict mode i hoisting

Na temat IIFE oraz strict mode pisałem już w innym poście, przy okazji opisywania dobrych praktyk w AngularJS
http://www.idaszak.com/article/2017/03/23/daj-sie-poznac-2-projekt-konkursowy-mistrzmakro dlatego pominę wstępne tłumaczenie.

Użyjmy IIFE do stworzenia zasięgu funkcji, który nie pozwoli naszej zmiennej i stać się zmienną globalną:

 
(function(){
  for (var i = 0; i < 5; i++){
      console.log(i);
  }
})();
console.log('na koniec pętli:', i); //błąd

Dzięki temu, console.log(i); wyrzuca błąd, że nie ma takiej zmiennej.

A co jeśli w przypadku IIFE zapomnimy zadeklarować zmiennej słowem kluczowym var?

 
(function(){
  for (i = 0; i < 5; i++){
      console.log(i);
  }
})();
console.log('na koniec pętli:', i); //"na koniec pętli:5"

Zmienna została wyciągnięta do zasięgu globalnego, przez co jest znowu dostępna poza blokiem funkcji!

Dlaczego tak się wydarzyło? Odpowiedzialny jest za to hoisting, czyli wyszukiwanie i wyciąganie deklaracji zmiennych do góry przed kompilacją kodu. Żeby to zobrazować, spójrzmy na fragment kodu:

 
function drive(){
  x=7;
  console.log(x);//7
  var x;
}
drive();
console.log(x);//błąd

Zmienna została normalnie zadeklarowana i nie jest dostępna poza zasięgiem tej funkcji.

Jak uniknąć przypadkowego tworzenia zmiennych globalnych? Tutaj przychodzi z pomocą tryb strict mode:

 
"use strict";
(function(){
  for (i = 0; i < 5; i++){
      console.log(i);
  }
})();
console.log('na koniec pętli:', i); //błąd

Nie pozwoli nam on stworzyć przypadkowej zmiennej globalnej.

Jednak wadą tych rozwiązań jest komplikowanie kodu. Czy nie da się tego napisać prościej? Z pomocą nadchodzi ES6.

# ES6 let i const

W ES6 poza słowem kluczowym var do deklarowania zmiennych służy także let, które pozwala na deklarowanie zmiennej dostępnej w obecnym zasięgu i zagnieżdżonych funkcjach. Nasz kod wygląda w taki sposób przy użyciu let:

 
for (let i = 0; i < 5; i++){
    console.log(i);
}
console.log('na koniec pętli:', i);//błąd

Kolejną rzeczą, która została dodana, jest znane już z innych języków programowania const. Zmienna zadeklarowana w taki sposób nie może zmienić swojej wartości później w programie.

 
const a = 1;
a = 2; //błąd

Używanie const zapobiega przypadkowej zmianie wartości.

Let umożliwia stworzenie innego zasięgu zmiennych, niż tylko zasięgu funkcji, a const pozwala stworzyć stałą zamiast zmiennej. To bardzo przydatne mechanizmy, dlatego powinniśmy z nich korzystać na codzień.

Published: April 05 2017

blog comments powered by Disqus