JavaScript ES6 – najważniejsze funkcjonalności

Wstęp

Wraz z doprowadzeniem JavaScriptu do standardu ECMAScript 2015 wprowadzono wiele narzędzi rozszerzających możliwości JS-a, poprawiających czytelność kodu i ułatwiających programistom pracę. Całkowita lista zmian zawiera ponad 100 pozycji, jednakże najważniejsze nowości można policzyć na placach dwóch rąk.

1. let i const

Let to nowy typ zmiennej, który zachowuje się bardzo podobnie do var, jednakże zamiast zasięgu funkcyjnego, ma zasięg blokowy. Const także ma zasięg blokowy, jednakże jest to stała, a nie zmienna, więc jej wartości nie można zmienić po zadeklarowaniu.


var name = 'John';
let age = 40;
const array = [1, 5, 2];
const pi = 3.14;

function changeVariables() {
	name = 'Johnny';
	age = 35;	
	array[1] = 123;
	
	console.log(array);
	// wyświetli [1, 123, 2]
	
	pi = 4;
	// próba przypisania nowej wartości do stałej
	// zakończy się błędem
}

changeVariables();

Pewnie się teraz zastanawiasz, dlaczego zmiana wartości komórki tablicy jest możliwa, a zmiana wartości naszej stałej pi już nie. Odpowiedzią na pytanie jest typ zmiennej. Typy number, string, boolean, undefined i null to typy proste, więc zmienna przechowuje bezpośrednio ich wartość. Tablica (a właściwie pochodna obiektu) to typ złożony, więc zmienna przechowuje referencję (odniesienie, adres) do komórki pamięci, w której tablica (obiekt) jest przechowywana. Kiedy nadpisujemy jedną z komórek tablicy, to w żaden sposób nie wpływamy na adres tejże tablicy, co pozwala nam na edycję wartości poszczególnych szufladek. Tak samo ma się to w kwestii obiektów i edycji ich własności (properties).

A co by się stało, gdybyśmy spróbowali nadpisać stałą array inną tablicą, o tych samych wartościach?


const array = [1, 5, 2];

function changeVariables() {
	array[1] = 123;
	
	console.log(array);
	// wyświetli [1, 123, 2]
	
	const array = [1, 123, 2];
	// zakończy się błedem
}

changeVariables();

Powyższe przypisanie zakończy się błedem, ponieważ próbowaliśmy nadpisać adres istniejącej tablicy adresem naszej nowej tablicy (to, że wartości były takie same nie oznacza, że obie tablice były przechowywane w tym samym miejscu w pamięci), co, jak już pisałem wcześniej, nie jest możliwe.

Przejdźmy teraz do zasięgu

Jeśli porównamy let, const i var w zasięgu globalnym, to zobaczymy, że zachowują się one praktycznie identycznie:


var varVariable = 'var';
let letVariable = 'let';
const constVariable = 'const';

function printVariables() {
	console.log(varVariable, letVariable, constVariable);
	// wyświetla: "var" "let" "const"
}
printVariables();

Jednakże, zarówno let jak i const nie są dostępne z poziomu obiektu window:


var varVariable = 'var';
let letVariable = 'let';
const constVariable = 'const';

console.log(
	window.varVariable,
	window.letVariable,
	window.constVariable
);
// wyświetla: "var" undefined undefined

Tyle czytania, a Ty dalej nie wiesz, co to oznacza, że let i const mają zasięg blokowy. Oznacza to, że zmienna nie jest dostępna w całej funkcji (nie ma zasięgu funkcyjnego, jak var), jeśli zostanie zadeklarowana w bloku, którym może być warunek lub pętla. Najlepszym przykładem, by pokazać różnice między tymi zasięgami, jest zadeklarowanie zmiennej jako licznik pętli for:


function calculate() {
	var sumOfOperations = 0;

	for (let i = 0; i < 10; i++) {
		const textToLog = 'I am a block-scoped variable';
		
		for (var j = 0; j < 15; j++) {
			console.log(textToLog);
			// wyświetla 'I am a block-scoped variable'
			
			sumOfOperations++;
		}
	}

	console.log(j);
	// wyświetla 15
	console.log(i);
	// kończy się błedem
}

calculate();

Przykład ten świetnie pokazuje różnicę między oboma typami zasięgu. Obie zmienne zostają zadeklarowane jako licznik pętli. Co więcej, zmienna typu var zostaje zadeklarowana w pętli zagnieżdżonej. Oznacza to, że jest ona o jeden blok głębiej, niż let. Mimo to, z sukcesem udaje nam się podejrzeć w konsoli jej wartość, co niestety kończy się błedem dla zmiennej typu let, właśnie ze względu na jej blokowy charakter. Zmienne blokowe są dostępne w każdym zagnieżdżonym bloku w stosunku do tego, w którym zostały zadeklarowane, jednakże nie są dostępne poza nim.

2. Domyślne wartości argumentów

Wraz z ES6, dostaliśmy możliwość definiowana domyślnych wartości argumentów funkcji. Pozwala to, na przykład, na tworzenie kodu, który będzie się zachowywać w określony sposób, chyba że programista zadecyduje inaczej, podając kolejny argument funkcji.


function calculateDistance(time, velocity, metricUnits = true) {
	// czas w godzinach,
	// prędkość w km/h
	const kmToMileRatio = 1.6;

	if (metricUnits) {
		return 'Odległość wynosi: ' + time * velocity + ' kilometrów';
	} else {
		return 'Odległość wynosi: ' + time * velocity * kmToMileRatio + ' mil';
	}
}

console.log(calculateDistance(2, 25));
// wyświetla Odległość wynosi: 50kilometrów
console.log(calculateDistance(2, 25, false));
// wyświetla Odległość wynosi: 80mil

3. Łańcuchy wieloliniowe

Łańcuchy wieloliniowe rozpoczynają się i kończą znakami `, zwanymi backtickami. Dzięki nim możemy zapisywać ciągi znakowe rozciągające się na dowolną liczbę linii, co jest szczególnie przydatne w przypadku zapisywania kodu HTML.


const emailBody = `
	<h1>
		Cześć
	</h1>

	<p>
		Dawno nie rozmawialiśmy, co u Ciebie słychać?
	</p>

	<p>
		Pozdrawiam,<br>
		Stefan
	</p>
`;

Jak widać na powyższym przykładzie, wszystko, co znajduje się wewnątrz backticków, jest traktowane jako jeden spójny tekst.

4. Interpolacja zmiennych

Backticki pozwalają także na wykorzystanie kolejnej nowej funkcjonalności, którą jest interpolacja zmiennych. Pozwala ona na tworzenie łańcuchów znakowych bez potrzeby rozdzielania ich w momencie, w którym chcielibyśmy dołączyć do łańcucha zmienną. Interpolację wykonujemy stawiając znak dolara, po którym następuje nazwa zmiennej zamknięta w nawiasach klamrowych, np. ${name}


const name = 'John';

console.log(`My name is ${name}`);
// wyświetla "My name is John"

console.log("My name is ${name}");
// wyświetla "My name is ${name}"

5. Funkcje strzałkowe

Funkcje strzałkowe są bardzo podobne do funkcji anonimowych, jednakże znacznie ułatwiają pracę z kodem. Zapisy funkcyjne są dzięki nim krótsze, wszelkie callbacki są dużo bardziej przejrzyste, a this w końcu zachowuje się tak, jakbyśmy się tego spodziewali, bez konieczności używania bind(). Co więcej, jeśli pominiemy ciało funkcji strzałkowej, będziemy mogli w jednej linii zwracać to, co jest zapisane za strzałką, ze względu na tzw. implicit return.


// porównanie zapisu funkcji anonimowych i strzałkowych

const sayHi = function(name) {
	return `Hi, ${name}!`;
}

const sayHello = (name) => `Hello, ${name}!`;

const sayHey = name => `Hey, ${name}, out there in the cold!`;

console.log(sayHi('Mark'));
// wyświetla "Hi, Mark!"

console.log(sayHello('World'));
// wyświetla "Hello, World!"

console.log(sayHey('you'));
// wyświetla "Hey, you, out there in the cold!"

Jak widzisz, nawiasy wokół argumentów funkcji strzałkowej są opcjonalne, ale tylko, jeśli argument jest jeden. W każdym innym przypadku nawiasy muszą się pojawić. Co więcej, jak wcześniej wspomniałem, return jest zbędne, jeśli pominiemy ciało funkcji (czyli nawiasy klamrowe), gdyż jednoliniowe funkcje strzałkowe domyślnie zwracająca w takim przypadku zapisane wyrażenie.

6. Spread operator

Słowo spread w języku angielskim to czasownik, który oznacza „rozłożyć”, „rozkładać”, co idealnie obrazuje działanie tego operatora. Zapisujemy go, używając trzech kropek przed nazwą iterowalnej zmiennej (...array). Stosuje się go do tablic, by rozłożyć je na czynniki pierwsze i otrzymać zestaw ich pierwotnych komórek. Przydaje się to do aplikowania parametrów do wywołań funkcji oraz kopiowania tablic (stworzenie nowej tablicy z użyciem spread operator gubi referencję do oryginalnej tablicy).


// operacje z użyciem spread operator

const numbers = [ 21, 37 ];
const add = (a, b) => a + b;

console.log(add(...numbers));
// zwraca 58

W tym momencie mamy tablicę numbers z dwoma liczbami. Jeśli chcielibyśmy ją skopiować i na jej kopii wykonywać jakieś działania, to nie osiągnęlibyśmy tego zwyczajnie przypisując zmienną numbers do innej zmiennej, gdyż przypisalibyśmy nie wartości, a referencję do tablicy numbers.


// zakładamy, że w tym momencie mamy już zdefiniowaną tablicę numbers

const straightCopy = numbers;
const spreadCopy = [...numbers];

straightCopy.pop();
spreadCopy.push(2004);
spreadCopy.push('jestem innym bytem');

console.log(straightCopy);
// zwraca [ 21 ]

console.log(numbers);
// zwraca [ 21 ]
// mimo, że nie działaliśmy na tej zmiennej

console.log(spreadCopy);
// zwraca [ 21, 37, 2004, "jestem innym bytem" ]

Mimo, że nie wykonywaliśmy żadnych działań na zmiennej numbers, to ze względu na przypisanie referencji do niej do zmiennej straightCopy, z „obu” tablic zostały usunięte ostatnie elementy (z obu w cudzysłowie, ponieważ tylko zmienna numbers przechowuję tablicę, a straightCopy przechowuje referencję do zmiennej numbers, a nie tablicę).
Co więcej, dzięki wykonaniu kopii tablicy z użyciem operatora rozłożenia, zmienna spreadCopy posiada dwa nowe elementy, oraz liczbę 21, która została usunięta ze zmiennych numbers i straightCopy. Ta metoda świetnie sprawdza się, gdy chcemy uniknąć zmian wartości oryginalnej tablicy, a potrzebujemy wykonać jakąś inwazyjną operację na jej elementach.

7. Destrukturyzacja obiektów

Destrukturyzacja, czyli rozbijanie większego bytu na mniejsze składowe, pozwala na wygodne tworzenie zmiennych, które zawierają wartości poszczególnych właściwości obiektów. Dzięki temu możemy w jednej linii stworzyć kilka zmiennych, które będą zawierały to, co kryje się pod danym kluczem.


// prosta destrukturyzacja obiektu

const obj = {
  name: 'Patryk',
  age: 25,
  lastName: 'Kiedrowski'
}

const { name, age } = obj;

console.log(name, age);
// zwraca "Patryk", 25

Destrukturyzacja jest szczególnie przydatna, gdy odbieramy w argumencie funkcji jakiś obiekt, ale nie potrzebujemy wszystkich jego właściwości. Posługując się powyższym obiektem, możemy w prosty sposób stworzyć funkcję, która przedstawi nam osobę podaną jako obiekt. Co więcej, by jeszcze uprościć sprawę, możemy skorzystać z interpolacji zmiennych. Definiujemy zmienną(e) z użyciem słowa kluczowego (var, let, const) i w nawiasach klamrowych podajemy właściwości obiektu, z których chcemy stworzyć zmiennę, po czym stawiamy znak równości i podajemy obiekt, który chcemy rozłożyć.


// destrukturyzacja argumentu

const obj = {
  name: 'Patryk',
  age: 25,
  lastName: 'Kiedrowski'
}

function introduction({ name, age, lastName }) {
	return `My name is ${name} ${lastName}, I am ${age} years old`;
}

console.log(introduction(obj));
// zwraca "My nam is Patryk Kiedrowski, I am 25 years old"


// destrukturyzacja zagnieżdżonego obiektu

const person = {
  name: {
    firstName: 'Patryk',
    lastName: 'Kiedrowski'
  },
  age: 25,
  occupation: 'front-end developer'
}

function introduction({ name: { firstName, lastName }, occupation }) {
	return `My name is ${firstName} ${lastName}, I am a ${occupation}`;
}

console.log(introduction(person));
// zwraca "My name is Patryk Kiedrowski, I am a front-end developer"

Możemy także destrukturyzować tablice, przypisując kolejne komórki do dowolnie nazwanych zmiennych. Niestety, nie mamy tutaj zbyt dużej dowolności, gdyż komórki tablic są destrukturyzowane po kolei, więc nie możemy wydobyć trzeciej komórki, jeśli nie wydobędziemy też pierwszej i drugiej.


// destrukturyzacja tablic

const numbers = [ 1, 2, 3, 4 ];

const [ first, second ] = numbers;
const [ one ] = numbers;

console.log(first, second);
// zwraca 1, 2

console.log(one);
// zwraca 1, a nie [ 1,2,3,4 ]

8. Moduły

Wprowadzone w ES6 moduły pozwalają na wygodne rozdzielanie naszego kodu JS na mniejsze bloczki i odnoszenie się do nich poprzez importowanie. W ten sposób, zamiast ładować kilka różnych plików JS jeden pod drugim wewnątrz tagów <script>, możemy pobierać konkretne elementy kodu tam, gdzie tego potrzebujemy.

Eksportować możemy zmienną, funkcję, czy klasę. Czynimy to pisząc przed danym elementem słowo kluczowe export. Możemy także definiować domyślny element do eksportu korzystając z export default. W ten sposób, osoba, która nie będzie wiedziała, który element zaimportować, dostanie od nas domyślny kod, który zostanie zwrócony.


// importowanie i eksportowanie

// ---------------
// plik assets.js
// ---------------
export const pi = Math.PI;

export function calculateCircleArea(r) {
	return pi * r * r;
}

export class Animal {
	constructor(name, species) {
		this.name = name;
		this.species = species;
	}
	
	introduction() {
		return `Hi, my name is ${this.name}, I am a ${this.species}`;
	}
}

export default Animal;

// ---------------
// plik index.js
// ---------------
import { default } from 'assets.js';
// importuje domyślny eksport z pliku assets.js, czyli klasę Animal

import { calculateCircleArea as area } from 'assets.js';
// importuje funkcję calculateCircleArea i podmienia jej nazwę na area

import * from 'assets.js';
// importuje wszystko z pliku assets.js

console.log(pi);
// zwraca 3.14...

console.log(area(2));
// zwraca ~12,57

const myDog = new Animal('Azor (ahai)', 'dog');

console.log(myDog.introduction);
// zwraca "Hi, my name is Azor (ahai), I am a dog";

Podsumowanie

To już praktycznie wszystkie najważniejsze zagadnienia dotyczące ES6, które można zmieścić w jednym artykule. Warto się z nimi zapoznać i się ich nauczyć, gdyż, w obecnych czasach, są powszechnie używane w pracy z kodem. Pozostają jeszcze dwie funkcjonalności, które powinny zostać opisane – klasy i obietnice (Promises). Są to jednak tak obszerne tematy, że zasługują na osobne artykuły. Pojawią się one w najbliższej przyszłości, I promise :)

Komentarze

Dodaj komentarz