07-04-2020 | Programming
Deklaracja zmiennych w JavaScript

Z racji #zostańwdomu postanowiłem odświeżyć pewne zagadnienia związane z programowaniem. Jest to pierwszy wpis z serii na tematy związane z JavaScriptem, Reactem, html oraz css. 

Na początek, w formie rozgrzewki, na warsztat wziąłem temat deklaracji zmiennych w JavaScripcie. Więc … zaczynamy …

Co to są zmienne

Zmienne są to opakowania wewnątrz których przechowujemy dane takie jak liczby, tekst oraz bardziej złożone obiekty. Dokładniej to rozpiszę innym razem 🙂

Po co nam zmienne? Pokaże na przykładzie prostego zadania.

Zadanie: Policz pola trzech kół o promieniach 3, 5 oraz 7.
//Math.pow() -> funkcja licząca potęgę liczby
console.log(3.1415926535897 * Math.pow(3,2)) //28.2743338823073
console.log(3.1415926535897 * Math.pow(5,2)) //78.5398163397425
console.log(3.1415926535897 * Math.pow(7,2)) //153.9380400258953

Za każdym razem liczba Pi wpisana jest ręcznie (na potrzeby przykładu zapominam, że Pi jest dostępne w bibliotece Math ).  Ponadto czytelność powyższego kodu nie jest najlepsza.

Wprowadzę teraz zmienne:

const pi = 3.1415926535897 //deklaracja zmiennej pi
const r1 = 3 // promień pierwszego koła
const r2 = 5 // promień drugiego koła
const r3 = 7// promień trzeciego koła
 
console.log(pi * Math.pow(r1,2)) //28.2743338823073
console.log(pi * Math.pow(r2,2)) //78.5398163397425
console.log(pi * Math.pow(r3,2)) //153.9380400258953

Po pierwsze kod jest bardziej czytelny, wiadomo skąd się biorą poszczególne wartości, po drugie o wiele łatwiej manipulować zmiennymi, np zmieniając dokładność Pi. Oczywiście, mógłbym wprowadzić funkcję aby kod był jeszcze lepszy.

W JavaScript deklaracja zmiennych odbywała się za pomocą słowa var. Naturalnie w kodzie zdarza się sytuacja, że chcemy aby jakaś zmienna była stała, niezmienna. W przypadku korzystania z deklaracji var nie było wiadomo, co jest zmienną a co powinno być stałą, bo de facto wszystko było zmienną. Programiści wypracowali metodę rozróżniania zmiennych od stałych. Otóż stałe zapisywano dużymi literami, dzięki temu w dalszej części kodu wiadomo było czy daną wartość można nadpisać. Lecz była to tylko konwencja nazewnictwa i nie niosła za sobą żadnych realnych ograniczeń w kodzie. Inni programiści widząc taką zmienną zapisaną dużymi literami wiedzieli, że dobrze byłoby nie nadpisywać tej zmiennej.  Wraz z pojawieniem się standardu ES6 wprowadzono dwa nowe słowa kluczowe służące do deklaracji zmiennych: let oraz const. Już samo nazewnictwo rozróżnia ich przeznaczenie. let / const wnoszą pewne zmiany w stosunku do var.

Deklaracja zmiennej

Czyli po kolei:

  • var – deklaracja zmiennej 
  • let – deklaracja zmiennej (ES6)
  • const – deklaracja stałej. Przypisana wartość nie może być nadpisana
var name = 'John' //deklaracja zmiennej
var SURNAME = 'Doe' // deklaracja "stałej", ale tak naprawde zniennej
let c = 0 //deklaracja zmiennej
const b = 0 //deklaracja stałej

Spróbuję zmienić wartości,

var name = 'John' //deklaracja zmiennej
name = 'Alfred';
console.log(name); // name zwraca Alfred
 
var SURNAME = 'Doe' // deklaracja "stałej"
SURNAME = 'Smith'
console.log(SURNAME) // SURNAME zwraca Smith.
 
let a = 0 //deklaracja zmiennej
a = 10;
console.log(a) // a zwraca 10
 
const b = 0 //deklaracja stałej
b=5 // Error. Nie można nadpisać b

jak widać w powyższym przykładzie “SURNAME” mogłem spokojnie nadpisać. Tak jak pisałem wcześniej, zadeklarowanie “stałej” jako var pisane dużymi literami, była to tylko konwencja nazewnictwa, w praktyce była to zwykła zmienna którą można było nadpisać. Za to “b” zadeklarowana jak const nie może być nadpisana.

Idźmy dalej. Jak widać zmiennej zadeklarowanej jako const nie mogę nadpisać, ale co jeżeli za pomocą const zadeklarujemy typ złożony taki jak tablice lub obiekt. W takim przypadku mogę zmieniać składowe elementu lub jego wartości. Niedozwolone jest tylko przypisanie na nowo do const.

Tabela:

const names = [] // deklaracja tabeli
names.push('Alan','Brian') // dodanie elementów do tabeli
console.log(names) // ['Alan', 'Brian']
names[0] = 'Jurgen' // zmiana elementu w tabeli
console.log(names) // ['Jurgen']
names.length = 0 // zmiana właściwości tabeli
console.log(names) // []
names = ['Alan', 'Jurgen'] // przypisanie całej tabeli. Error!

Obiekt:

const user = { name: 'Jurgen' } //deklaracja obiektu
user.name = 'Alan' //zmiana wartości składowej obiektu
console.log(user) //{ name: "Alan" }
user.surname = 'Doe' // dodanie składowej obiektu
console.log(user) //{ name: "Alan", surname:"Doe" }
user = {name: 'Brian'} // przypisanie całego obiektu. Error!

Warto zwrócić uwagę na kwestię ponownej deklaracji zmiennej. Otóż var pozwalał na taki zabieg, natomiast let już nie dopuszcza takiej sytuacji

var city = "Rome";
var city = "London";
console.log(city) // London
 
let country = "Poland";
let country = "Italia"; // Error. Identifier 'country' has already been declared

Hoisting

Kolejną, bardzo ważną różnicą pomiędzy var a let / const jest hoisting zmiennych. W skrócie Hoisting jest to cecha JavaScriptu która wynosi deklaracje zmiennej na początek bloku kodu. Istotne jest, że wynoszona była tylko deklaracja a nie wartość.

Deklaracja zmiennej bez przypisania wartości:

const name //deklaracja zmiennej, zmienna nie posiada wartości
console.log(name) //undefined

Deklaracja zmiennej z przypisaniem wartości:

const name = 'Jurgen' //deklaracja zmiennej i przypisanie wartości 
console.log(name) //Jurgen

Hoisting wynosi na początek tylko deklaracje zmiennej:

console.log(name) // undefined. 
 var name = 'Jurgen';

poprzez działanie hoistingu ten fragment kodu był interpretowany w następujący sposób:

var name; // Hoisting wyniósł deklaracje zmiennej name na sam początek kodu
console.log(a) // undefined
name = 'Jurgen';

Dla let taki zapis nie przejdzie. JavaScript wymaga od programisty myślenia i wymusza deklaracje zmiennej przed jej użyciem. Co w sumie jest sensowne 🙂

Czyli poniższy zapis jest błędny:

console.log(city) // Error!. Cannot access 'city' before initialization
let city = "Rome"

Wymagane jest przynajmniej zadeklarowanie zmiennej przed jej użyciem:

let country;
console.log(country) // undefined, country zadeklarowane, ale nie ma przypisanej wartości
country = "Italy";
console.log(country) // Italy

Ciekawym aspektem jest też kwestia kiedy zmienne są parametrami funkcji. Gdy zmienne są zadeklarowane jako var to hoisting wyciąga ich deklaracje na początek i do funkcji trafiają jako undefined. Funkcja się wywołuje bez problemu, po prostu ma puste parametry:

function showPlace(a,b){
   console.log(a,b)//undefined undefined
}
showPlace(city,country)
var city = "Rome";
var country = "Italy"

Natomiast gdy zmienne są zadeklarowane jako let, a wiemy już że hoisting tutaj nie zadziała w ten sposób, więc …

function showPlace(a,b){
   console.log(a,b)
}
showPlace(city,country) // Error! Cannot access 'city' before initialization
let city = "Rome";
let country = "Italy"

Wystąpił błąd na poziomie wywołania funkcji. Funkcja w ogóle się nie odpala. Jak widać jest zasadnicza różnica w działaniu.

Zasięg zmiennej

Wprowadzenie let / const zmieniło trochę zasięg zmiennych. Var miało zasięg funkcyjny, czyli ich zasięg określało ciało funkcji. Natomiast let / const ma zasięg blokowy, tj od klamry do klamry. To znaczy że mamy lepsza kontrolę nad zmiennymi lokalnymi.

var city = "Rome" //zmianna globalna
{
	var city = "London" // zmienna lokalna
	console.log(city) // London
}
console.log(city) // London

Natomiast jeżeli wprowadzę funkcję:

var city = "Rome" //zmianna globalna
function showCity(){
	var city = "London" // zmienna lokalna
	console.log(city) // London
}
showCity()
console.log(city) // Rome

Wrócę do pierwszego przykładu, tym razem z użyciem let zamiast var:

let city = "Rome" //zmianna globalna
{
	let city = "London" // zmienna lokalna
	console.log(city) // London
}
console.log(city) // Rome

let city = “London” jest widoczna tylko w obszarze klamer.

Inny przykład. Dla var:

var condition = true
if (condition) {
   var city = "Rome";
}
console.log(city); // Rome, zmienna city widoczna jest poza if'em

Natomiast dla let:

let condition = true
if (condition) {
  let city = "Rome";
}
console.log(city) //Error! city is not defined

I ostatni przykład, w nim widać różnicę w zasięgu blokowym a funkcyjnym.

{
   let city = 'Rome';
   var country = 'Italia';
}
console.log(city); // Error! city is not defined
console.log(country); // Italia

Let jest niewidoczna poza klamrami, natomiast same klamry nie ograniczają widoczności var. Dopiero zamknięcie var w funkcji ogranicza jego widoczność.

(function() {
	let city = 'Rome';
	var country = 'Italia';
})();
console.log(city); // Error! city is not defined
console.log(country); // Error! country is not defined

Jeszcze jeden przykład pokazujący ciekawą rzecz:

function counter(){
	for (var i=0; i<10; i++) {
   		console.log(i);
 	}
 	console.log(i); //10
}
counter()

Zmienna var i jest widoczna po skończonej pętli for, gdyż jest w obrębie ciała funkcji. Natomiast jeżeli w tym fragmencie kodu zmienną zadeklaruje jak let:

function counter(){
	for (let i=0; i<10; i++) {
   		console.log(i);
 	}
 	console.log(i); //Error!. i is not defined
}
counter()

Zmienna let i jest widoczna tylko w obrębie pętli for.

Closures

Domknięciem tego wpisu będzie Closures, czyli … domknięcia 🙂  Domknięcie jest to obszar stworzony przez funkcje które “zamyka” w nim zmienne i funkcje oraz odgradza je od reszty kodu. Czyli inaczej closures to taki płot na łące który odgradza znajdujące się wewnątrz zwierzęta od reszty łąki (taką alegorie znalazłem w necie i bardzo mi się spodobała 🙂 ), logiczne i proste. To teraz jak się ma to do programowania i zmiennych. De facto domknięcie były już zastosowane w przykładach powyżej. Działa to tak samo dla var oraz dla let.

let city = "Rome" //zmianna globalna
function showCity(){
 	//funkcja tworzy swoje domknięcie
	let city = "London" // zmienna lokalna
 	function printCity(){
   		console.log(city) // London
 	}
	return printCity()
}
// poza funkcją jej zmienne oraz funkcje nie są dostępne
showCity()
console.log(city) // Rome

Funkcja może także mieć dostęp do zmiennej wyższego poziomu:

let city = "Rome" //zmianna globalna
function showCity(){
	//funkcja tworzy swoje domknięcie
	city = "London" // zmiana globalnej zmiennej
 	function printCity(){
   		console.log(city) // London
 	}
 	return printCity()
}
 // poza funkcją jej zmienne oraz funkcje nie są dostępne
showCity()
console.log(city) // London

Oczywiście może wystąpić kombinacja zmiennych lokalnych oraz globalnych:

let city = "Rome" //zmianna globalna
function showCity(){
 	//funkcja tworzy swoje domknięcie
	city = "London" // zmiana globalnej zmiennej która jest dostępna wewnątrz dokmknięcia
	let country = "Italy" // zmienna lokalna
		function printCity(){
   			console.log(city,country) // London, Italy
 		}
	return printCity()
}
 // poza funkcją jej zmienne oraz funkcje nie są dostępne
showCity()
console.log(city) // London
console.log(country) // Error! country is not defined (jest to zmienna lokalna widoczna tylko dla funkcji showCity)

Zakończenie

Zmienne pozwalają przechowywać nam dane. Wprowadzenie let i const w znaczący sposób usystemizowało JS’a. Mam jawnie rozdzielone na zmienne oraz stałe. W obrębie bloku nie możemy deklarować zmiennych o tej samej nazwie. Zmiana zasięgu zmiennych pozwala na przyjemniejsze (IMO) pisanie kodu. W pewien sposób pozytywnym jest także ukrócenie hoistingu dla zmiennych i wymaganie od programisty pisania kodu który jest czytelniejszy oraz ma dobrą strukturę (plus to, że zniknie jedno standardowe pytanie rekrutacyjne 😉 ). Zalecam stosowanie const i let zamiast var

I to tyle w dzisiejszym wpisie, zachęcam do zostawienia / przesłania feedbacku. Dziękuję za uwagę.

Łukasz