Darmowy fragment publikacji:
Tytuł oryginału: Practical Modern JavaScript: Dive into ES6 and the Future of JavaScript
Tłumaczenie: Inez Okulska-Stanisławska
ISBN: 978-83-283-4229-3
© 2018 Helion S.A.
Authorized Polish translation of the English edition of Practical Modern JavaScript, ISBN 9781491943533 ©
2017 Nicolás Bevacqua
This translation is published and sold by permission of O’Reilly Media, Inc.,
which owns or controls all rights to publish and sell the same.
All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means,
electronic or mechanical, including photocopying, recording or by any information storage retrieval system,
without permission from the Publisher.
Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej
publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną,
fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje
naruszenie praw autorskich niniejszej publikacji.
Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich
właścicieli.
Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte w tej książce informacje były
kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane
z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz Wydawnictwo HELION nie
ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji
zawartych w książce.
Wydawnictwo HELION
ul. Kościuszki 1c, 44-100 GLIWICE
tel. 32 231 22 19, 32 230 98 63
e-mail: helion@helion.pl
WWW: http://helion.pl (księgarnia internetowa, katalog książek)
Drogi Czytelniku!
Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres
http://helion.pl/user/opinie/nojspo
Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.
Printed in Poland.
• Kup książkę
• Poleć książkę
• Oceń książkę
• Księgarnia internetowa
• Lubię to! » Nasza społeczność
Spis treści
Przedmowa ................................................................................................................ 7
Wstęp ......................................................................................................................... 9
1. ECMAScript i przyszłość JavaScriptu ............................................................................ 13
13
15
17
24
25
1.1. Krótka historia standardów języka JavaScript
1.2. ECMAScript jako żywy standard
1.3. Obsługa przeglądarek i dodatkowe narzędzia
1.4. Kategorie nowych możliwości ES6
1.5. Przyszłość JavaScriptu
2. Najistotniejsze elementy ES6 ..................................................................................... 27
27
31
35
41
44
49
2.1. Literały obiektu
2.2. Funkcje strzałki
2.3. Destrukturyzacja przypisania
2.4. Parametr resztowy i operator rozłożenia
2.5. Literały szablonu
2.6. Instrukcje let oraz const
3. Klasy, symbole, obiekty i dekoratory .......................................................................... 57
57
64
71
76
3.1. Klasy
3.2. Symbole
3.3. Ulepszenia obiektu wbudowanego Object
3.4. Dekoratory
4.
Iterowanie i sterowanie przepływem ......................................................................... 81
81
4.1. Obietnice
4.2. Protokół iteratorów oraz protokół obiektów iterowalnych
96
106
4.3. Funkcje i obiekty generatora
122
4.4. Funkcje asynchroniczne
4.5. Asynchroniczna iteracja
131
5
Poleć książkęKup książkę5. Wykorzystanie kolekcji ECMAScript .......................................................................... 135
137
142
144
146
5.1. Użycie map ES6
5.2. Zrozumienie i wykorzystanie WeakMap
5.3. Zbiory w ES6
5.4. Słabe zbiory WeakSets
6. Zarządzanie dostępem do właściwości obiektu za pomocą obiektu Proxy .................. 149
149
155
156
163
6.1. Pierwsze kroki z proxy
6.2. Unieważniające obiekty proxy
6.3. Pułapki proxy
6.4. Zaawansowane pułapki obiektu Proxy
7. Ulepszenia obiektów wbudowanych w ES6 .............................................................. 175
175
184
188
199
208
7.1. Liczby
7.2. Math
7.3. Ciągi tekstowe oraz Unicode
7.4. Wyrażenia regularne
7.5. Tablice
8. Moduły JavaScript ................................................................................................... 217
217
221
229
8.1. CommonJS
8.2. Moduły JavaScript
8.3. Praktyczne rozważania na temat modułów ES
9. Rozważania praktyczne ........................................................................................... 233
233
237
241
243
246
249
252
256
9.1. Deklaracje zmiennych
9.2. Literały szablonu
9.3. Zwięzły zapis i destrukturyzacja obiektu
9.4. Parametr resztowy i operator rozłożenia
9.5. Odmiany funkcji
9.6. Klasy i proxy
9.7. Asynchroniczny przepływ programu
9.8. Dziwactwa złożoności, abstrakcje i konwencje
Skorowidz ............................................................................................................... 257
6
Spis treści
Poleć książkęKup książkęROZDZIAŁ 2.
Najistotniejsze elementy ES6
Szósta edycja języka oferuje szeroki wachlarz ulepszeń składni, które nie wprowadzają zmian ła-
miących kompatybilność wsteczną i które omówię w tym rozdziale. Wiele z nich to po prostu lukier
składniowy, czyli możliwości, które mogły pojawić się już w ES5, ale przy użyciu bardziej skom-
plikowanych fragmentów kodu. Są też jednak zmiany, które nie są jedynie ozdobnikami, lecz ofe-
rują całkowicie nowy sposób deklaracji zmiennych przy użyciu let oraz const, o czym przeko-
nasz się pod koniec tego rozdziału.
W literałach obiektu w ES6 również wprowadzono parę zmian i od nich zacznę.
2.1. Literały obiektu
Literałem obiektu jest każda deklaracja obiektu używająca zapisu klamrowego {}, tak jak w poniż-
szym przykładzie.
var book = {
title: Modular ES6 ,
author: Nicolas ,
publisher: O´Reilly
}
ES6 wnosi do składni literałów obiektu kilka ulepszeń, takich jak skrócone definiowanie właści-
wości, generowane nazwy właściwości i definicje metod. Przyjrzyjmy się bliżej tym możliwościom
i potencjalnym przykładom ich zastosowania.
2.1.1. Zwięzła deklaracja właściwości
Czasem deklarujemy obiekty z jedną właściwością lub wieloma właściwościami; ich wartościami są
referencje do zmiennych o tej samej nazwie. Możemy np. mieć kolekcję listeners i w celu przy-
pisania do właściwości nazwanej listeners literału obiektu musimy powtórzyć tę nazwę. Poniższy
fragment kodu pokazuje typowy przykład, gdzie mamy deklarację literału obiektu z kilkoma ta-
kimi powtarzającymi się właściwościami.
var listeners = []
function listen() {}
var events = {
listeners: listeners,
listen: listen
}
27
Poleć książkęKup książkęKiedykolwiek znajdziesz się w takiej sytuacji, możesz ominąć przypisanie wartości oraz średnik i sko-
rzystać z nowego zapisu z ES6. Jak pokazuje poniższy przykład, nowa zwięzła składnia ES6 impli-
kuje przypisanie:
var listeners = []
function listen() {}
var events = { listeners, listen }
Później, w drugiej części książki dowiesz się, że zapis klamrowy przy przypisaniu wartości właściwo-
ści pomaga oczyścić kod z niepotrzebnych powtórzeń, bez ingerencji w jego działanie. W poniż-
szym przykładzie ponownie zaimplementowano część localStorage, API przeglądarki do trwałe-
go przechowywania danych jako wewnątrzpamięciowy ponyfill1. Gdyby nie skrócona składnia,
obiekt storage byłby o wiele bardziej rozwlekły w zapisie.
var store = {}
var storage = { getItem, setItem, clear }
function getItem(key) {
return key in store ? store[key] : null
}
function setItem(key, value) {
store[key] = value
}
function clear() {
store = {}
}
To pierwsza z wielu możliwości ES6, które mają na celu uproszczenie zawiłości w kodzie, dzięki
czemu łatwiej nad nim zapanować. Kiedy już przyzwyczaisz się do tej składni, zauważysz, że po-
prawią się zarówno czytelność, jak i produktywność programowania.
2.1.2. Generowane nazwy właściwości
Czasami musisz zadeklarować obiekt, który zawiera właściwości o nazwach bazujących na zmien-
nych czy innych wyrażeniach JavaScriptu, co pokazuje poniższy fragment napisany w ES5. Załóżmy
np., że expertise otrzymujesz jako parametr funkcji i nie jest to wartość, którą znałeś już wcześniej.
var expertise = journalism
var person = {
name: Sharon ,
age: 27
}
person[expertise] = {
years: 5,
interests: [ international , politics , internet ]
}
1 Podobnie jak polyfille, ponyfille (https://mjavascript.com/out/ponyfills) to implementacje po stronie użytkownika
funkcji, które nie są dostępne w każdym środowisku uruchomieniowym JavaScriptu. O ile polyfille próbują wy-
pełniać lukę w danym środowisku, żeby mogło działać, tak jakby rzeczywiście miało dostępną daną funkcję, o tyle po-
nyfille implementują brakujące funkcje jako osobne moduły, które nie zaśmiecą środowiska uruchomieniowego.
Ma to taką zaletę, że nie koliduje z oczekiwaniami zewnętrznych, niezależnych od naszego kodu bibliotek (które
nie wiedzą o polyfillu), które mogą być używane w danym środowisku.
28
Rozdział 2. Najistotniejsze elementy ES6
Poleć książkęKup książkęLiterały obiektu w ES6 nie są ograniczone do deklaracji za pomocą statycznych obiektów. Za pomo-
cą generowanych nazw właściwości możesz dowolne wyrażenie zapisać w nawiasie kwadratowym
i użyć go jako nazwy właściwości. I kiedy parser dojdzie do miejsca deklaracji, Twoje wyrażenie
zostanie wykonane i użyte jako nazwa właściwości. Poniższy przykład pokazuje, jak te części ko-
du, które już widzieliśmy, za jednym zamachem deklarują obiekt person, bez konieczności użycia
drugiej instrukcji dodającej właściwość expertise do obiektu person.
var expertise = journalism
var person = {
name: Sharon ,
age: 27,
[expertise]: {
years: 5,
interests: [ international , politics , internet ]
}
}
Nie możesz łączyć zwięzłej składni z generowanymi nazwami właściwości. Zwięzły zapis to po
prostu lukier składniowy, który jest wykorzystywany w czasie kompilacji kodu JavaScript i poma-
ga uniknąć powtórzeń, podczas gdy generowane nazwy właściwości są wykonywane w czasie rze-
czywistym. Biorąc pod uwagę fakt, że chcemy połączyć te dwie niekompatybilne ze sobą funkcje,
poniższy przykład wyrzuciłby błąd. W większości przypadków taka kombinacja prowadziłaby do
powstania kodu, który byłby trudny do zinterpretowania dla innych programistów, więc chyba lepiej
dla Ciebie, że nie da się tego w ten sposób użyć.
var expertise = journalism
var journalism = {
years: 5,
interests: [ international , politics , internet ]
}
var person = {
name: Sharon ,
age: 27,
[expertise]
// błąd składni!
}
Typowym scenariuszem użycia generowanej nazwy właściwości jest chęć dodania encji do obiektu
mapującego, który używa pola entity.id jako klucza, co widać w poniższym przykładzie. Zamiast
używać trzeciej instrukcji, gdzie dodalibyśmy grocery do mapy groceries, możemy wpisać tę de-
klarację do literału obiektu groceries.
var grocery = {
id: bananas ,
name: Bananas ,
units: 6,
price: 10,
currency: USD
}
var groceries = {
[grocery.id]: grocery
}
Może też się okazać, że za każdym razem, gdy funkcja otrzymuje parametr, powinniśmy użyć go
do zbudowania obiektu. W kodzie ES5 trzeba by alokować zmienną deklarującą literał obiektu,
2.1. Literały obiektu
29
Poleć książkęKup książkędodać dynamiczną właściwość, a na końcu zwrócić obiekt. Poniższy przykład pokazuje dokładnie taki
przypadek podczas tworzenia koperty, która mogłaby zostać później użyta do wiadomości ajak-
sowych, zgodnych z pewną konwencją — posiadają one właściwość error z opisem, jeśli coś się
nie udaje, i właściwość success, gdy wszystko się uda.
function getEnvelope(type, description) {
var envelope = {
data: {}
}
envelope[type] = description
return envelope
}
Generowane nazwy właściwości pomagają zapisać tę samą funkcję w sposób bardziej skondenso-
wany, przy użyciu jednej instrukcji.
function getEnvelope(type, description) {
return {
data: {},
[type]: description
}
}
Ostatnie ulepszenie dotyczące literałów obiektu odnosi się do funkcji.
2.1.3. Definiowanie metody
Zazwyczaj możesz deklarować metodę obiektu jako właściwość obiektu. W następnym przykła-
dzie utworzymy mały emiter zdarzeń, który obsługuje różne rodzaje zdarzeń. Robi to za pomocą
metody emitter#on, której można użyć do rejestracji funkcji obsługi zdarzenia. Natomiast metoda
emitter#emit służy do wywołania konkretnego zdarzenia.
var emitter = {
events: {},
on: function (type, fn) {
if (this.events[type] === undefined) {
this.events[type] = []
}
this.events[type].push(fn)
},
emit: function (type, event) {
if (this.events[type] === undefined) {
return
}
this.events[type].forEach(function (fn) {
fn(event)
})
}
}
Od wersji ES6 możesz deklarować metody w literale obiektu, używając nowej składni definiowania
metod. W tym przypadku możemy zrezygnować ze średnika i słowa kluczowego function. To nowa
zwięzła alternatywa dla tradycyjnej deklaracji metody, w której trzeba było właśnie wstawiać sło-
wo function. Poniższy przykład pokazuje, jak będzie wyglądał nasz obiekt emitter, jeśli skorzy-
stamy z definicji metody.
30
Rozdział 2. Najistotniejsze elementy ES6
Poleć książkęKup książkęvar emitter = {
events: {},
on(type, fn) {
if (this.events[type] === undefined) {
this.events[type] = []
}
this.events[type].push(fn)
},
emit(type, event) {
if (this.events[type] === undefined) {
return
}
this.events[type].forEach(function (fn) {
fn(event)
})
}
}
Jeszcze innym sposobem deklaracji funkcji w ES6 są funkcje strzałki; mamy do dyspozycji całą ich
paletę. Przyjrzyjmy się bliżej, czym są te funkcje, jak się je deklaruje i jak się zachowują pod kątem
semantyki.
2.2. Funkcje strzałki
W JavaScripcie zazwyczaj deklaruje się funkcje przy użyciu kodu, gdzie — tak jak w poniższym
przykładzie — podajemy nazwę, listę parametrów oraz ciało funkcji.
function name(parameters) {
// ciało funkcji
}
Możesz też utworzyć funkcję anonimową, omijając jej nazwę podczas przypisania zmiennej lub
właściwości do funkcji oraz podczas jej wywołania.
var example = function (parameters) {
// ciało funkcji
}
Od wersji ES6 możesz już korzystać z funkcji strzałki jako innego sposobu zapisu anonimowej
funkcji. Pamiętaj, że jest wiele nieco różniących się od siebie wersji tego zapisu. Poniższy fragment
kodu pokazuje funkcję strzałki, która jest bardzo podobna do funkcji anonimowej, zademon-
strowanej przed chwilą. Jedyną różnicą zdaje się być brakujące słowo klucz function oraz obec-
ność strzałki = na prawo od listy parametrów.
var example = (parameters) = {
// ciało funkcji
}
Chociaż funkcje strzałki wyglądają bardzo podobnie do typowych anonimowych funkcji, w rzeczy-
wistości są od nich diametralnie różne — funkcji strzałki nie można bezpośrednio nazwać, chociaż
nowoczesne środowiska uruchomieniowe mogą wywnioskować jej nazwę ze zmiennej, do której
taka funkcja jest przypisana; nie mogą być też użyte jako konstruktory, ani nie posiadają właści-
wości prototype, co oznacza, że nie możesz użyć słowa kluczowego new w połączeniu z funkcją strzał-
ki. Funkcje te są też ograniczone zasięgiem leksykalnym i dlatego nie modyfikują znaczenia this.
2.2. Funkcje strzałki
31
Poleć książkęKup książkęZagłębmy się zatem w semantycznych różnicach względem funkcji tradycyjnych, dostępnych sposo-
bach deklaracji funkcji strzałki oraz praktycznych przykładach ich zastosowania.
2.2.1. Zasięg leksykalny
W ciele funkcji strzałki this, arguments oraz super odnoszą się do już istniejącego zasięgu, po-
nieważ funkcja strzałki nie tworzy nowego zasięgu. Rozważ poniższy przykład — mamy tu obiekt
timer z licznikiem seconds oraz metodą start, zdefiniowaną przy użyciu składni, o której była mowa
wcześniej. Uruchamiamy timer, czekamy parę sekund i zgłaszamy aktualną ilość czasu (seconds),
która upłynęła.
var timer = {
seconds: 0,
start() {
setInterval(() = {
this.seconds++
}, 1000)
}
}
timer.start()
setTimeout(function () {
console.log(timer.seconds)
}, 3500)
// - 3
Gdybyśmy zdefiniowali funkcję przekazywaną do setInterval jako zwykłą funkcję anonimową
zamiast używać funkcji strzałki, wówczas this odnosiłoby się do kontekstu tej anonimowej funkcji,
a nie — tak jak tu — do kontekstu metody start. Moglibyśmy zaimplementować timer, używając
deklaracji typu var self = this na początku metody start, i potem referencji do self zamiast
this. W przypadku funkcji strzałki zanika dodatkowa komplikacja związana z utrzymaniem refe-
rencji kontekstowych, dzięki czemu możemy się skupić na funkcjonalności kodu.
Analogicznie leksykalne powiązania w funkcjach strzałki w ES6 oznaczają również, że wywołania
funkcji nie mogą modyfikować kontekstu this poprzez użycie .call, .apply, .bind itp. Ogranicze-
nie to zazwyczaj jest przydatne, bo gwarantuje, że kontekst zawsze będzie zachowany i niezmienny.
Przejdźmy teraz do poniższego przykładu. Jak myślisz, co wypisze console.log?
function puzzle() {
return function () {
console.log(arguments)
}
}
puzzle( a , b , c )(1, 2, 3)
Odpowiedź brzmi następująco: arguments odnoszą się tutaj do kontekstu funkcji anonimowej
i dlatego wypisane zostaną argumenty przekazane do tej funkcji. A w tym przypadku są to 1, 2, 3.
A co z kolei się stanie, gdy w poprzednim przykładzie użyjemy funkcji strzałki zamiast funkcji ano-
nimowej?
function puzzle() {
return () = console.log(arguments)
}
puzzle( a , b , c )(1, 2, 3)
32
Rozdział 2. Najistotniejsze elementy ES6
Poleć książkęKup książkęW tym przypadku obiekt arguments odnosi się do kontekstu funkcji puzzle, ponieważ funkcje
strzałki nie tworzą domknięcia. Dlatego też wypisane zostaną argumenty a , b , c .
Jak wspominałem, istnieje sporo odmian funkcji strzałki, ale dotąd przyjrzeliśmy się tylko ich
pełnej wersji. Jakie są inne sposoby reprezentacji funkcji strzałki?
2.2.2. Odmiany funkcji strzałki
Spójrzmy raz jeszcze na tę składnię funkcji strzałki, którą zdążyliśmy poznać do tej pory.
var example = (parameters) = {
// ciało funkcji
}
Jeśli funkcja strzałki posiada dokładnie jeden parametr, wówczas można pominąć opcjonalny nawias.
Jest to przydatne, jeśli przekazujemy funkcję strzałki do innej metody, bo wtedy redukujemy licz-
bę nawiasów, co sprawia, że kod staje się bardziej czytelny dla ludzkiego oka.
var double = value = {
return value * 2
}
Funkcje strzałki są często używane do prostych funkcji, takich jak funkcja double, którą już omawia-
łem. Poniższa odmiana funkcji strzałki pozbywa się ciała funkcji. Zamiast niego wystarczy wyra-
żenie, takie jak value * 2. Kiedy funkcja zostanie wywołana, wykonywane jest wyrażenie i zwra-
cany wynik. Instrukcja return jest wywoływana domyślnie — nie trzeba też używać nawiasów
oznaczających ciało funkcji, dlatego wystarczy pojedyncze wyrażenie.
var double = (value) = value * 2
Zauważ, że możesz połączyć ukryty nawias i domyślny return, tworząc jeszcze bardziej zwięzłą
funkcję strzałkową.
var double = value = value * 2
Domyślne zwracanie literałów obiektu
Jeśli chcesz zastosować domyślną instrukcję return dla literałów obiektu, musisz umieścić ją w na-
wiasie. W przeciwnym razie kompilator zinterpretuje Twoje nawiasy klamrowe jako początek i ko-
niec bloku funkcji.
var objectFactory = () = ({ modular: es6 })
W poniższym przykładzie JavaScript interpretuje nawiasy klamrowe jako ciało naszej funkcji strzałki.
Co więcej, number jest interpretowany jako etykieta2, co sprawia, że mamy wyrażenie value, które ni-
czego nie robi. A ponieważ znajdujemy się w bloku i nie zwracamy niczego, zmapowane wartości bę-
dą undefined.
[1, 2, 3].map(value = { number: value })
// - [undefined, undefined, undefined]
2 Etykiety (https://mjavascript.com/out/label) są stosowane w celu identyfikacji instrukcji. Etykiet można używać
wraz z instrukcją break, żeby wskazać, z której sekwencji chcemy wyjść, oraz z instrukcją continue, żeby było
wiadomo, którą sekwencję chcemy kontynuować.
2.2. Funkcje strzałki
33
Poleć książkęKup książkęJeśli nasza próba domyślnego zwracania literału obiektu zakładałaby więcej niż jedną właściwość,
wówczas kompilator nie byłby w stanie poprawnie zinterpretować drugiej właściwości, co z kolei po-
skutkowałoby wyrzuceniem SyntaxError.
[1, 2, 3].map(value = { number: value, verified: true })
// - SyntaxError
Wstawienie wyrażenia w nawias rozwiązuje ten problem, ponieważ kompilator przestanie interpre-
tować je jako blok funkcji. Zamiast tego deklaracja obiektu stanie się wyrażeniem odnoszącym się do
literału obiektu, który chcemy domyślnie zwrócić.
[1, 2, 3].map(value = ({ number: value, verified: true }))
/* - [
{ number: 1, verified: true },
{ number: 2, verified: true },
{ number: 3, verified: true }]
*/
Teraz, kiedy już rozumiesz funkcje strzałki, możemy pomyśleć o ich zaletach i sytuacjach, w któ-
rych mogą się przydać.
2.2.3. Zalety i przykłady zastosowania
Z zasady nie warto bezrefleksyjnie wykorzystywać możliwości ES6, kiedy tylko się da. Lepiej po-
myśleć o każdym przypadku indywidualnie i zastanowić się, czy wdrożenie nowej cechy rzeczy-
wiście polepszy czytelność i stabilność kodu. Możliwości z ES6 nie zawsze są lepsze od tego, czym
dysponowaliśmy do tej pory, i traktowanie ich w ten sposób nie jest najlepszym pomysłem.
Jest kilka przypadków, w których użycie funkcji strzałki może nie mieć większego sensu. Jeśli np.
masz pokaźną funkcję składającą się z wielu linii kodu, zamiana function na = raczej niespecjal-
nie polepszy Twój kod. Funkcje strzałki są najskuteczniejsze dla krótkich instrukcji, gdzie słowo
kluczowe function i idąca za nim składnia stanowią istotną część wyrażenia funkcji.
Odpowiednie nazwanie funkcji dodaje kontekst, co sprawia, że człowiek może łatwiej zinterpre-
tować taki kod. Funkcje strzałki nie mogą być bezpośrednio nazywane, ale można je nazwać po-
średnio poprzez przypisanie ich do zmiennej. W poniższym przykładzie przypisujemy funkcję
strzałki do zmiennej throwError. Kiedy wywołanie tej funkcji poskutkuje błędem, zrzut stosu po-
prawnie zidentyfikuje ją jako throwError.
var throwError = message = {
throw new Error(message)
}
throwError( this is a warning )
- Uncaught Error: this is a warning at throwError
Funkcje strzałki są zgrabne, jeśli trzeba definiować anonimowe funkcje, które prawdopodobnie
tak czy inaczej powinny być leksykalnie związane. Funkcje te w niektórych sytuacjach sprawiają też, że
Twój kod staje się bardziej zwięzły. Są szczególnie przydatne w większości przypadków progra-
mowania funkcjonalnego, np. przy korzystaniu z .map, .filter czy .reduce w kolekcjach, co po-
kazuje poniższy przykład.
[1, 2, 3, 4]
.map(value = value * 2)
.filter(value = value 2)
34
Rozdział 2. Najistotniejsze elementy ES6
Poleć książkęKup książkę .forEach(value = console.log(value))
// - 4
// - 6
// - 8
2.3. Destrukturyzacja przypisania
To jedna z najbardziej elastycznych i wyrazistych możliwości w ES6. A także jedna z najprost-
szych. Wiąże właściwości z tyloma zmiennymi, ilu tylko sobie zażyczysz. Działa dla obiektów, tablic,
a nawet list parametrów function. Przyjrzyjmy się jej dokładnie, zaczynając od obiektów.
2.3.1. Destrukturyzacja obiektów
Wyobraź sobie, że masz program, w którym występują postaci z komiksów, a wśród nich Bruce
Wayne, i chcesz zrobić odniesienie do właściwości obiektu, który go opisuje. Oto przykład obiektu,
którego użyjemy dla Batmana.
var character = {
name: Bruce ,
pseudonym: Batman ,
metadata: {
age: 34,
gender: male
},
batarang: [ gas pellet , bat-mobile control , bat-cuffs ]
}
Gdybyś chciał, żeby zmienna pseudonym odnosiła się do character.pseudonym, mógłbyś tak to za-
pisać w kodzie ES5. To typowe rozwiązanie, jeśli chcesz np. odnosić się do pseudonym w różnych
miejscach swojego kodu i wolałbyś uniknąć wpisywania za każdym razem character.pseudonym.
var pseudonym = character.pseudonym
Z wykorzystaniem destrukturyzacji w przypisaniu składnia staje się nieco bardziej przejrzysta. Jak wi-
dać w poniższym przykładzie, nie musisz dwukrotnie pisać pseudonym, a i tak intencja zostanie po-
prawnie przekazana. Poniższa instrukcja jest równoważna z poprzednią napisaną w kodzie ES5.
var { pseudonym } = character
Tak jak możesz zadeklarować wiele zmiennych po przecinku, używając prostej instrukcji var, tak mo-
żesz deklarować wiele zmiennych wewnątrz nawiasu klamrowego wyrażenia destrukturyzowanego.
var { pseudonym, name } = character
W podobnym stylu możesz mieszać destrukturyzację z normalnym zapisem deklaracji zmiennych
wewnątrz tej samej instrukcji var. Chociaż na pierwszy rzut oka może to nieco mylić, jednak od wy-
branego przez Ciebie stylu kodowania będzie zależało, czy należy deklarować różne zmienne za pomo-
cą jednej instrukcji. W każdym razie pokazuje to elastyczność, jaką oferuje składnia destrukturyzacji.
var { pseudonym } = character, two = 2
Jeśli chcesz wyodrębnić właściwość nazwaną pseudonym, ale jednocześnie zadeklarować ją jako zmien-
ną pod nazwą alias, możesz w tym celu użyć następującej składni destrukturyzującej, zwanej aliaso-
waniem. Zauważ, że możesz wykorzystać alias albo jakąkolwiek inną dostępną nazwę zmiennej.
2.3. Destrukturyzacja przypisania
35
Poleć książkęKup książkęvar { pseudonym: alias } = character
console.log(alias)
// - Batman
Chociaż aliasy nie wyglądają prościej od zapisu z wersji ES5, alias = character.pseudonym, zaczy-
nają mieć sens wtedy, gdy weźmiesz pod uwagę fakt, że destrukturyzacja służy głębokim struktu-
rom, tak jak w poniższym przykładzie.
var { metadata: { gender } } = character
W takich przypadkach jak poprzedni, gdzie mieliśmy głęboko zagnieżdżoną właściwość, która
była poddawana destrukturyzacji, mógłbyś za pomocą aliasów pobrać właściwość w jeszcze bar-
dziej przejrzysty sposób. Przyjrzyj się poniższemu fragmentowi, gdzie właściwość nazwana code
nie wskazywałaby na swoją zawartość tak jasno jak właściwość colorCode.
var { metadata: { gender: characterGender } } = character
Scenariusz, który przed chwilą przedstawiłem, występuje bardzo często, ponieważ właściwości są
często nazywane w kontekście swoich nadrzędnych obiektów. Chociaż palette.color.code jest
idealnie opisowa, code sama w sobie mogłaby już mieć wiele znaczeń, ale — na szczęście — aliasy,
takie jak colorCode, pomagają ponownie przywrócić brakujący kontekst do nazwy zmiennej, bez
konieczności rezygnowania z destrukturyzacji.
Za każdym razem, gdy w notacji ES5 próbujesz uzyskać dostęp do nieistniejącej właściwości,
otrzymujesz wartość undefined.
console.log(character.boots)
// - undefined
console.log(character[ boots ])
// - undefined
Przy użyciu destrukturyzacji zachowanie to nie ulega zmianie. Deklarując destrukturyzowaną
zmienną dla właściwości, której brakuje, również otrzymasz wartość undefined.
var { boots } = character
console.log(boots)
// - undefined
Destrukturyzowana deklaracja sięgająca do zagnieżdżonej właściwości obiektu nadrzędnego, któ-
ry ma wartość null lub undefined, wyrzuci wyjątek, tak jak przy zwykłej próbie dostępu do wła-
ściwości null czy undefined.
var { boots: { size } } = character
// - wyjątek
var { missing } = null
// - wyjątek
Jeśli pomyślisz o tym fragmencie kodu jak o odpowiedniku kodu napisanego w ES5, który pokażę
za chwilę, stanie się jasne, że wyrażenie musi wyrzucić wyjątek, zwłaszcza wtedy, gdy weźmiemy
pod uwagę fakt, że destrukturyzacja jest w zasadzie lukrem składniowym.
var nothing = null
var missing = nothing.missing
// - wyjątek
36
Rozdział 2. Najistotniejsze elementy ES6
Poleć książkęKup książkęW ramach częściowej destrukturyzacji możesz ustawić domyślne wartości za każdym razem, kie-
dy wartość byłaby undefined. Wartość domyślna może być czymkolwiek, co przyjdzie Ci do gło-
wy — mogą to być liczby, ciągi tekstowe, funkcje, obiekty, referencja do innej zmiennej itp.
var { boots = { size: 10 } } = character
console.log(boots)
// - { size: 10 }
Domyślne wartości można też ustawić dla destrukturyzacji zagnieżdżonych właściwości.
var { metadata: { enemy = Satan } } = character
console.log(enemy)
// - Satan
Jeśli chcesz połączyć je z aliasem, najpierw powinieneś wpisać alias, a potem wartość domyślną,
tak jak to pokazałem niżej.
var { boots: footwear = { size: 10 } } = character
W składni destrukturyzacji można używać też składni generowanych nazw właściwości. W takiej
sytuacji będziesz zmuszony dodać alias, który będzie mógł zostać użyty jako nazwa zmiennej. A to
dlatego, że generowane nazwy właściwości dopuszczają arbitralne wyrażenia, przez co kompilator
nie byłby w stanie wywnioskować, co jest nazwą zmiennej. W poniższym przykładzie używamy
aliasu value oraz generowanych nazw właściwości w celu wyodrębnienia właściwości boots z obiektu
character.
var { [ boo + ts ]: characterBoots } = character
console.log(characterBoots)
// - true
Ta odmiana destrukturyzacji jest prawdopodobnie najmniej przydatna, ponieważ characterBoots
= character[type] jest zazwyczaj prostsze niż { [type]: characterBoots } = character, ponieważ
jest instrukcją bardziej sekwencyjną. A to znaczy, że możliwość ta jest bardziej przydatna, kiedy
masz właściwości, które chcesz zadeklarować w literale obiektu, a nie korzystać z późniejszej in-
strukcji ich przypisania.
To tyle, jeśli chodzi o obiekty w kwestii destrukturyzacji. A co z tablicami?
2.3.2. Destrukturyzacja tablic
Składnia destrukturyzacji tablic jest podobna do składni dotyczącej obiektów. Poniższy przykład
pokazuje obiekt coordinates, który został rozłożony na dwie zmienne — x oraz y. Zauważ, że w nota-
cji użyty został nawias kwadratowy zamiast klamrowego — to oznacza, że używamy destruktury-
zacji tablicy, a nie obiektu. Zamiast szpikować swój kod szczegółami implementacji, takimi jak
x = coordinates[0], przy użyciu destrukturyzacji możesz przekazać odpowiedni sens w sposób
jasny i bez konieczności wyraźnego odnoszenia się do indeksów, po prostu nazywając wartości.
var coordinates = [12, -7]
var [x, y] = coordinates
console.log(x)
// - 12
Podczas destrukturyzacji tablic możesz ominąć właściwości, które Cię nie interesują, lub te, do
których nie potrzebujesz referencji.
2.3. Destrukturyzacja przypisania
37
Poleć książkęKup książkęvar names = [ James , L. , Howlett ]
var [ firstName, , lastName ] = names
console.log(lastName)
// - Howlett
Destrukturyzacja tablic pozwala na ustawienie domyślnych wartości, tak jak w przypadku de-
strukturyzacji obiektu.
var names = [ James , L. ]
var [ firstName = John , , lastName = Doe ] = names
console.log(lastName)
// - Doe
Kiedy w ES5 musisz zamienić miejscami wartości dwóch zmiennych, zazwyczaj odwołujesz się do
trzeciej, tymczasowej zmiennej, tak jak w poniższym kodzie.
var left = 5
var right = 7
var aux = left
left = right
right = aux
Destrukturyzacja pomoże uniknąć deklaracji zmiennej aux i skupić się na Twojej intencji. Po-
nownie w tego typu zastosowaniach destrukturyzacja pomaga przekazać właściwą intencję w spo-
sób bardziej zwięzły i efektywny.
var left = 5
var right = 7
[left, right] = [right, left]
Ostatnim obszarem destrukturyzacji, który omówimy, są parametry funkcji.
2.3.3. Domyślne ustawienia parametrów funkcji
Parametry funkcji w ES6 również oferują możliwości określenia wartości domyślnych. Poniższy przy-
kład definiuje domyślną wartość parametru exponent, przypisując mu najczęściej używaną wartość.
function powerOf(base, exponent = 2) {
return Math.pow(base, exponent)
}
Ustawienia domyślne można również zaaplikować do parametrów funkcji strzałki. Jeśli mamy
wartości domyślne w funkcjach strzałki, musimy te listy parametrów wstawić w nawiasy, nawet
jeśli znajduje się tam tylko jeden parametr.
var double = (input = 0) = input * 2
Wartości domyślne nie są ograniczone do parametru funkcji znajdującego się najbardziej po prawej,
tak jak w niektórych językach programowania. Możesz ustawić wartość domyślną dla każdego
parametru, znajdującego się na każdej pozycji.
function sumOf(a = 1, b = 2, c = 3) {
return a + b + c
}
console.log(sumOf(undefined, undefined, 4))
// - 1 + 2 + 4 = 7
38
Rozdział 2. Najistotniejsze elementy ES6
Poleć książkęKup książkęW JavaScripcie nierzadko pisze się funkcję z obiektem options, która zawiera różne właściwości.
Możesz określić domyślny obiekt options, jeśli żaden nie zostanie wprowadzony, tak jak pokaza-
łem w poniższym kodzie.
var defaultOptions = { brand: Volkswagen , make: 1999 }
function carFactory(options = defaultOptions) {
console.log(options.brand)
console.log(options.make)
}
carFactory()
// - Volkswagen
// - 1999
Problem z takim podejściem polega na tym, że w momencie, w którym użytkownik carFactory
dostarczy obiekt options, stracisz wszystkie swoje domyślne wartości.
carFactory({ make: 2000 })
// - undefined
// - 2000
Możemy połączyć domyślne wartości funkcji z destrukturyzacją i otrzymać to, co najlepsze w obu
tych podejściach.
2.3.4. Destrukturyzacja parametrów funkcji
Lepszym podejściem niż tylko określenie wartości domyślnej byłaby całkowita destrukturyzacja
obiektu options, ustawiająca domyślne wartości dla każdej z właściwości oddzielnie, wewnątrz wspól-
nego wzorca destrukturyzacji. Takie podejście pozwala również odnosić się do każdej z opcji bez
konieczności przechodzenia przez obiekt options. W ten sposób straciłbyś jednak możliwość
bezpośredniej referencji do tego obiektu, co w niektórych sytuacjach mogłoby przysporzyć kłopotów.
function carFactory({ brand = Volkswagen , make = 1999 }) {
console.log(brand)
console.log(make)
}
carFactory({ make: 2000 })
// - Volkswagen
// - 2000
Aczkolwiek w tej sytuacji znów stracilibyśmy wartość domyślną, jeśli użytkownik nie dostarczy
obiektu options. Co oznacza, że carFactory() zgłosi wyjątek, jeśli obiekt options nie będzie istniał.
Można tego uniknąć, używając składni pokazanej w poniższym fragmencie kodu, gdzie dodany
został pusty obiekt jako domyślna wartość parametru options. Pusty obiekt zostanie wówczas za-
pełniony wartościami domyślnymi ze wzorca destrukturyzacji.
function carFactory({
brand = Volkswagen ,
make = 1999
} = {}) {
console.log(brand)
console.log(make)
}
carFactory()
// - Volkswagen
// - 1999
2.3. Destrukturyzacja przypisania
39
Poleć książkęKup książkęPoza wartościami domyślnymi, możesz użyć destrukturyzacji dla parametrów funkcji, żeby okre-
ślić rodzaj obiektów, jakie może obsłużyć Twoja funkcja. Przyjrzyj się poniższemu przykładowi,
gdzie mamy obiekt car o wielu różnych właściwościach. Obiekt car opisuje właściciela, rodzaj
auta, producenta, datę produkcji oraz preferencje właściciela w momencie zakupu auta.
var car = {
owner: {
id: e2c3503a4181968c ,
name: Donald Draper
},
brand: Peugeot ,
make: 2015,
model: 208 ,
preferences: {
airbags: true,
airconditioning: false,
color: red
}
}
Gdybyśmy chcieli zaimplementować funkcję, która bierze pod uwagę tylko wybrane właściwości
parametru, dobrym pomysłem mogłaby być bezpośrednia referencja do tych właściwości za po-
mocą jawnej destrukturyzacji. Zaletą takiego rozwiązania jest fakt, że wówczas, czytając sygnaturę
funkcji, dowiemy się o każdej wymaganej właściwości.
Jeśli destrukturyzujemy wszystko z góry, łatwo zauważyć, kiedy dane wejściowe nie są zgodne z kon-
traktem funkcji. Poniższy przykład pokazuje, że można każdą potrzebną nam właściwość zdefi-
niować w liście parametrów, jasno określając kształt obiektów, które może obsłużyć nasze API
getCarProductModel.
var getCarProductModel = ({ brand, make, model }) = ({
sku: brand + : + make + : + model,
brand,
make,
model
})
getCarProductModel(car)
Zobaczmy, w czym jeszcze, oprócz domyślnych wartości i wypełniania obiektu options, przydat-
na jest destrukturyzacja.
2.3.5. Przykłady zastosowania destrukturyzacji
Za każdym razem, gdy funkcja zwraca obiekt lub tablicę, destrukturyzacja sprawia, że interakcja
staje się o wiele bardziej zwięzła. Poniższy przykład pokazuje funkcję, która zwraca obiekt z kil-
koma koordynatami, z których bierzemy tylko te, które nas interesują, czyli x oraz y. Unikamy
deklaracji pośredniej wartości point, która raczej przeszkadza, jednocześnie wcale nie poprawia-
jąc czytelności kodu.
function getCoordinates() {
return { x: 10, y: 22, z: -1, type: 3d }
}
var { x, y } = getCoordinates()
40
Rozdział 2. Najistotniejsze elementy ES6
Poleć książkęKup książkęPrzypadek użycia domyślnych wartości danych opcji prosi się o powtórzenie. Wyobraź sobie, że masz
funkcję random, która wytwarza losowe wartości liczbowe w zakresie od min do max, domyślnie
wynoszące od 0 do 10. To wyjątkowo ciekawa alternatywa dla nazwanych parametrów w językach
z silnym typowaniem, takich jak Python czy C#. Wzorzec, w którym jesteś w stanie zdefiniować war-
tości domyślne dla opcji, a potem pozwolić je pojedynczo nadpisywać, daje ogromne pole manewru.
function random({ min = 1, max = 10 } = {}) {
return Math.floor(Math.random() * (max - min)) + min
}
console.log(random())
// - 7
console.log(random({ max: 24 }))
// - 18
Wyrażenia regularne to kolejna kwestia idealnie nadająca się do destrukturyzacji — pozwala ona
na nazwanie grup zwróconych przez funkcję match bez konieczności sięgania po indeksy liczbo-
we. Mamy tu przykład RegExp, którego można by użyć do parsowania prostych dat, oraz przykład
destrukturyzacji tych dat na poszczególne ich komponenty. Pierwszy wpis w tak otrzymanej ta-
blicy jest zarezerwowany dla surowych danych wejściowych w postaci ciągu tekstowego i możemy
go odrzucić.
function splitDate(date) {
var rdate = /(\d+).(\d+).(\d+)/
return rdate.exec(date)
}
var [ , year, month, day] = splitDate( 2015-11-06 )
Jednak musisz uważać, bo jeśli wyrażenie regularne nie znajdzie dopasowania, funkcja zwróci null.
Być może więc lepszym podejściem byłoby sprawdzenie całości pod kątem zgodności, zanim
przeprowadzimy destrukturyzację, tak jak to pokazuje poniższy kod.
var matches = splitDate( 2015-11-06 )
if (matches === null) {
return
}
var [, year, month, day] = matches
Przejdźmy teraz do operatorów resztowych i rozłożenia.
2.4. Parametr resztowy i operator rozłożenia
Zanim pojawił się ES6, interakcja z arbitralną liczbą parametrów funkcji była raczej skomplikowana.
Trzeba było użyć obiektu arguments, który nie jest tablicą, ale ma właściwość length. Zazwyczaj
kończyło się to zamianą obiektu arguments na właściwą tablicę poprzez użycie Array#slice.call,
tak jak w poniższym fragmencie.
function join() {
var list = Array.prototype.slice.call(arguments)
return list.join( , )
}
join( first , second , third )
// - first, second, third
ES6 oferuje lepsze rozwiązanie tego problemu, a mianowicie parametry resztowe.
2.4. Parametr resztowy i operator rozłożenia
41
Poleć książkęKup książkę2.4.1. Parametry resztowe
Teraz możesz ostatni parametr w każdej funkcji JavaScriptu poprzedzić trzema kropkami, zmie-
niając go w specjalny „parametr resztowy”. Jeśli jest on jedynym parametrem funkcji, wówczas
otrzyma wszystkie argumenty przekazywane do funkcji — co działa podobnie jak rozwiązanie
.slice, które widzieliśmy przed chwilą, ale pozwala uniknąć skomplikowanego konstruktu typu
arguments oraz jest określone w liście parametrów.
function join(...list) {
return list.join( , )
}
join( first , second , third )
// - first, second, third
Parametry nazwane, znajdujące się przed parametrem resztowym nie wejdą do obiektu list.
function join(separator, ...list) {
return list.join(separator)
}
join( ; , first , second , third )
// - first; second; third
Zauważ, że funkcja strzałki z parametrem resztowym musi zawierać nawiasy nawet wtedy, kiedy
ma tylko jeden parametr. W innym przypadku otrzymalibyśmy błąd SyntaxError. W poniższym ko-
dzie mamy piękny przykład na to, że kombinacja funkcji strzałki z parametrem resztowym może
dać zwięzłe wyrażenie funkcji.
var sumAll = (...numbers) = numbers.reduce(
(total, next) = total + next
)
console.log(sumAll(1, 2, 5))
// - 8
Porównaj to z wersją ES5 tej samej funkcji — różnica tkwi w złożoności. Chociaż bywa zwięzła,
funkcja sumAll potrafi zdezorientować czytelnika kodu, jeśli nie jest przyzwyczajony do metody
.reduce, ale też z powodu użycia aż dwóch funkcji strzałki. To jeden z kompromisów w kwestii
skomplikowania kodu, o których opowiem dalej w tej książce.
function sumAll() {
var numbers = Array.prototype.slice.call(arguments)
return numbers.reduce(function (total, next) {
return total + next
})
}
console.log(sumAll(1, 2, 5))
// - 8
Następny w kolejce jest operator rozłożenia (ang. spread operator). Również zapisuje się go z uży-
ciem trzech kropek, ale ma nieco inne zastosowanie.
2.4.2. Operator rozłożenia
Operator rozłożenia może być użyty do zamiany każdego iterowalnego obiektu w tablicę. Rozło-
żenie wyrażenia może być wykorzystane do różnych celów, m.in. związanych z literałem tablicy
czy wywołaniem funkcji. Poniższy przykład wykorzystuje …arguments w celu zmiany parametrów
funkcji w literał tablicowy.
42
Rozdział 2. Najistotniejsze elementy ES6
Poleć książkęKup książkęfunction cast() {
return [...arguments]
}
cast( a , b , c )
// - [ a , b , c ]
Możemy użyć operatora rozłożenia, żeby rozłożyć ciąg tekstowy na tablicę, zawierającą każdy
pojedynczy znak tego ciągu.
[... show me ]
// - [ s , h , o , w , , m , e ]
Możesz umieścić dodatkowe elementy po lewej lub prawej stronie operatora i wciąż otrzymasz
efekt, który Cię interesuje.
function cast() {
return [ left , ...arguments, right ]
}
cast( a , b , c )
// - [ left , a , b , c , right ]
Rozłożenie jest skutecznym sposobem na łączenie wielu tablic. Poniższy przykład pokazuje, że
można rozłożyć tablicę na literał tablicowy, rozszerzając jej elementy w miejscu.
var all = [1, ...[2, 3], 4, ...[5], 6, 7]
console.log(all)
// - [1, 2, 3, 4, 5, 6, 7]
Zauważ, że operator rozłożenia nie jest ograniczony do tablicy i obiektu arguments. Można go za-
stosować do każdego iterowalnego obiektu. Iterowalność to protokół w ES6, który pozwala Ci na
zmianę każdego obiektu w coś, co będzie się poddawało iteracji. Przyjrzymy się temu w rozdziale 4.
Przesuwanie i rozłożenie
Jeśli chcesz wyodrębnić element czy dwa z początku tablicy, najczęstszym podejściem będzie użycie
.shift. I chociaż jest ono całkiem funkcjonalne, to jednak poniższy fragment kodu pokazuje, że trudno je
zrozumieć na pierwszy rzut oka, ponieważ w celu wyciągnięcia każdorazowo innego elementu z początku
obiektu list aż dwukrotnie używa metody .shift. Punkt ciężkości kładzie się więc tu, tak jak w wielu in-
nych przypadkach sprzed ery ES6, na zmuszanie języka do tego, żeby działał po naszej myśli.
var list = [ a , b , c , d , e ]
var first = list.shift()
var second = list.shift()
console.log(first)
// - a
W ES6 możesz połączyć operator rozłożenia z destrukturyzacją tablicy. Poniższy fragment kodu jest
podobny do poprzedniego, z tą różnicą, że używamy jednej linijki i ta jedna linijka lepiej opisuje to,
co robimy, zamiast powtarzać wywołanie list.shift(), tak jak wcześniej.
var [first, second, ...other] = [ a , b , c , d , e ]
console.log(other)
// - [ c , d , e ]
Stosowanie operatora rozłożenia sprawia, że możesz skoncentrować się na implementacji potrzeb-
nych Ci opcji, nie zaprzątając sobie głowy samym językiem. Większa efektywność wyrażeń i krótsza
walka z ograniczeniami języka to wspólne cechy nowych możliwości wprowadzonych przez ES6.
2.4. Parametr resztowy i operator rozłożenia
43
Poleć książkęKup książkęWcześniej, przed ES6, za każdym razem, gdy miałeś dynamiczną listę argumentów, którą trzeba
było zastosować do wywołania funkcji, musiałeś użyć .apply. Było to mało eleganckie, bo .apply
ustala kontekst this, a w takiej sytuacji nie chcesz sobie przecież dodatkowo zawracać głowy.
fn.apply(null, [ a , b , c ])
Oprócz rozłożenia na tablice, możesz również rozłożyć elementy tablicy na poszczególne para-
metry w wywołaniu funkcji. Poniższy przykład pokazuje, że można używać operatora rozłożenia,
by przekazać dowolną liczbę argumentów do funkcji multiply.
function multiply(left, right) {
return left * right
}
var result = multiply(...[2, 3])
console.log(result)
// - 6
Rozłożenie argumentów na poszczególne parametry w wywołaniu funkcji można dowolnie połą-
czyć z argumentami regularnymi, podobnie jak z literałami tablicy. Następny przykład wywołuje
funkcję print z kilkoma parametrami regularnymi i kilkoma tablicami rozłożonymi do listy pa-
rametrów. Zauważ, że parametr resztowy list bez problemu obsługuje wszystkie parametry.
Rozłożenie i parametr resztowy sprawiają, że intencje kodu stają się bardziej przejrzyste, a jedno-
cześnie nie umniejszają funkcjonalności.
function print(...list) {
console.log(list)
}
print(1, ...[2, 3], 4, ...[5])
// - [1, 2, 3, 4, 5]
Innym ograniczeniem .apply jest fakt, że połączenie go ze słowem kluczowym new podczas two-
rzenia egzemplarza obiektu rozwleka całe wyrażenie. Mamy tutaj przykład takiego połączenia,
które ma na celu utworzenie obiektu Date. Zapomnij na chwilę o tym, że miesiące w dacie Java-
Scriptu liczą się od zera, co sprawia, że 11 oznacza grudzień, i pomyśl, ile z poniższych linii kodu
trzeba poświęcić na to, żeby język zaczął łaskawie robić to, czego potrzebujemy. A chodzi przecież
tylko o utworzenie instancji obiektu Date.
new (Date.bind.apply(Date, [null, 2015, 11, 31]))
// - Thu Dec 31 2015
Jak pokazuje następny fragment, operator rozłożenia usuwa tę niepotrzebną zawiłość, dzięki
czemu zostajemy tylko z tym, co naprawdę istotne. Mamy słowo kluczowe new, które używa ope-
ratora ..., żeby rozłożyć listę argumentów w wywołaniu funkcji, i mamy Date. Tyle.
new Date(...[2015, 11, 31])
// - Thu Dec 31 2015
Tabela na następnej stronie podsumowuje przykłady użycia, o których wspominaliśmy, omawia-
jąc operator rozłożenia.
2.5. Literały szablonu
Literały szablonu to ogromne ulepszenie regularnych ciągów tekstowych w języku JavaScript.
Zamiast używać pojedynczych czy podwójnych cudzysłowów, szablony literału deklaruje się po-
przez użycie lewego apostrofu (ang. backtick), tak jak to pokazuję poniżej.
var text = `To mój pierwszy literał szablonu`
44
Rozdział 2. Najistotniejsze elementy ES6
Poleć książkęKup książkęPrzykład użycia
Konkatenacja
Dodanie elementów tablicy do listy
Destrukturyzacja
Połączenie słowa kluczowego new
i metody apply
ES5
ES6
[1, 2].concat(more)
[1, 2, ...more]
list.push.apply(list,
items)
a = list[0], other =
list.slice(1)
new (Date.bind.apply(Date,
[null,2015,31,8]))
list.push(...items)
[a, ...other] = list
new Date(...
[2015,31,8])
Biorąc pod uwagę fakt, że szablony literału są ograniczone lewymi apostrofami, możesz teraz za-
deklarować ciąg tekstowy zawierający zarówno pojedynczy, jak i podwójny cudzysłów, bez ko-
nieczności otaczania ich znakami ucieczki, tak jak to było dotychczas.
var text = `Znalazłem w Google’u informację o tym „smaczku” z ES6 i jestem pod wrażeniem!`
Jedną z najatrakcyjniejszych możliwości szablonów literału jest ich zdolność do interpolacji wyra-
żeń JavaScriptu.
2.5.1. Interpolacja ciągów tekstowych
Z wykorzystaniem szablonów literału jesteś w stanie wewnątrz swojego szablonu interpolować
każde wyrażenie JavaScriptu. Kiedy program dochodzi do takiego ciągu tekstowego, wykonuje go,
a my otrzymujemy z powrotem już skompilowany wynik. Poniższy przykład interpoluje zmienną
name do literału szablonu.
var name = Shannon
var text = `Witaj, ${ name }!`
console.log(text)
// - Witaj, Shannon!
Ustaliliśmy już, że możesz użyć każdego wyrażenia JavaScriptu, nie tylko zmiennych. Możesz
pomyśleć o każdym wyrażeniu wewnątrz literału szablonu jako definiowaniu zmiennej przed
uruchomieniem szablonu i potem łączeniu tej zmiennej z pozostałym ciągiem tekstowym. Jednak
to sprawia, że kod staje się łatwiejszy do opanowania, bo nie zawiera ręcznie łączonych ciągów
tekstowych i wyrażeń JavaScriptu. Zmienne, których używasz w wyrażeniu, funkcje, które wy-
wołujesz itp., powinny być dostępne w tym zasięgu, w którym zdefiniowano szablon.
W zależności od stylu kodowania, który stosujesz, możesz zdecydować, ile treści chcesz zmieścić
w takich interpolowanych wyrażeniach. Poniższy fragment kodu pokazuje np. utworzenie instan-
cji obiektu Date i sformatowanie go do zapisu czytelnego dla człowieka — i wszystko to dzieje się
wewnątrz literału szablonu.
`Obecna data i godzina to ${ new Date().toLocaleString() }.`
// - obecna data i godzina to 26.08.2015, 15:15:20.
Możesz interpolować również operacje matematyczne.
`2+3 równa się ${ 2 + 3 }`
// - 2+3 równa się 5
Możesz nawet zagnieżdżać szablony literału, ponieważ są one poprawnymi wyrażeniami Java-
Scriptu.
2.5. Literały szablonu
45
Poleć książkęKup książkę`Ten szablon literału ${ `jest ${ zagnieżdżony }` }!`
// - Ten szablon literału jest zagnieżdżony!
Inną zaletą literału szablonu jest obsługa wielowierszowych ciągów tekstowych.
2.5.2. Wielowierszowe literały szablonu
Jeśli chciałeś w JavaScripcie utworzyć wielowierszowy ciąg tekstowy, zanim pojawiły się literały
szablonu, musiałeś stosować znaki końca linii, konkatenację, tablice, a nawet wykorzystywać tric-
ki z użyciem komentarzy. Poniższy fragment podsumowuje jedną z najbardziej typowych repre-
zentacji wielowierszowego literału szablonu zgodnego ze standardem ES5.
var escaped =
Pierwszy wiersz\n\
Drugi wiersz\n\
A potem trzeci
var concatenated =
Pierwszy wiersz \n +
Drugi wiersz\n +
A potem trzeci
var joined = [
Pierwszy wiersz ,
Drugi wiersz ,
A potem trzeci
].join( \n )
W ES6 możesz zamiast tego użyć lewego apostrofu. Szablon literału domyślnie obsługuje wielowier-
szowe ciągi tekstowe. Zauważ, że nie ma znaków końca linii, nie ma też konkatenacji ani tablic.
var multiline =
`Pierwszy wiersz
Drugi wiersz
A potem trzeci`
Wielowierszowe ciągi tekstowe naprawdę cieszą, kiedy masz np. kawał kodu w HTML, do którego
chcesz interpolować jakieś zmienne. Jeśli potrzebujesz wyświetlić listę wewnątrz szablonu, możesz ite-
rować listę, mapując jej elementy na odpowiednie znaczniki, i zwrócić taki połączony wynik in-
terpolowanego wyrażenia. Dzięki temu deklaracja komponentów wewnątrz szablonu staje się nie-
zwykle prosta, co widać w kodzie poniżej.
var book = {
title: Modułowy ES6 ,
excerpt: Tutaj pojawia się jakiś dobrze przefiltrowany HTML ,
tags: [ es6 , template-literals , es6-in-depth ]
}
var html = ` article
header
h1 ${ book.title } /h1
/header
section ${ book.excerpt } /section
footer
ul
${
book.tags
.map(tag = ` li ${ tag } /li `)
.join( \n )
}
46
Rozdział 2. Najistotniejsze elementy ES6
Poleć książkęKup książkę /ul
/footer
/article `
Szablon, który właśnie przygotowaliśmy, zwróciłby w danych wyjściowych to, co widzimy w po-
niższym kodzie. Zauważ, że zachowane3 zostały odstępy, a tagi li poprawnie zinterpretowane dzię-
ki temu, że połączyliśmy je, używając kilku spacji.
article
header
h1 Modułowy ES6 /h1
/header
section Tutaj pojawia się jakiś dobrze przefiltrowany HTML /section
footer
ul
li es6 /li
li template-literals /li
li es6-in-depth /li
/ul
/footer
/article
Wadą wielowierszowych ciągów tekstowych jest właśnie formatowanie wcięć. Poniższy przykład
pokazuje typowe wcięcia w kodzie we wnętrzu literału szablonu zawartego wewnątrz funkcji.
Chociaż moglibyśmy nie oczekiwać żadnych wcięć, nasz ciąg tekstowy będzie przesunięty o sześć
spacji w prawo.
function getParagraph() {
return `
Drogi Rodzie,
To jest literał szablonu, który ma wcięcie równe sześciu spacjom. A Ty,
zdaje się, nie spodziewałeś się w ogóle żadnego wcięcia.
Nico
`
}
Chociaż nie jest to rozwiązanie idealne, możesz uniknąć tych wcięć za pomocą funkcji narzę-
dziowej i usunąć je z każdego wiersza wynikowego ciągu tekstowego.
function unindent(text) {
return text
.split( \n )
.map(line = line.slice(4))
.join( \n )
.trim()
}
Czasem może lepiej sprawdzić, co będzie wynikiem interpolowanego wyrażenia, zanim wkleimy
je do naszego szablonu. Dla przypadków bardziej zaawansowanych można wykorzystać inną możli-
wość szablonów literału, zwaną szablonami z tagami.
3 Użycie literału szablonu nie zachowuje automatycznie odstępów. W wielu przypadkach wystarczy wprowadzić
odpowiednie wcięcia, by zadziałało, ale bądź ostrożny w stosowaniu bloków kodu z wcięciami, bo może to spo-
wodować niechciane formatowanie z powodu bloków zagnieżdżonych.
2.5. Literały szablonu
47
Poleć książkęKup książkę2.5.3. Szablony z tagami
Domyślnie JavaScript interpretuje ukośnik \ jako znak kodowania o specjalnym znaczeniu. Przy-
kładowo \n oznacza nową linię, \u00f1 to ñ itd. Możesz ominąć te zasady, używając szablonu z tagiem
String.raw. Następny fragment pokazuje szablon literału wykorzystujący String.raw, który
sprawia, że \n nie będzie już interpretowane jako nowa linia.
text = String.raw` \n jest wzięte dosłownie.
Czyli nie zostało zinterpretowane jako znak nowej linii.`
console.log(text)
// \n jest wzięte dosłownie.
// Czyli nie zostało zinterpretowane jako znak nowej linii.
Prefiks String.raw, który dodaliśmy do naszego literału szablonu, to szablon z tagiem, używany do
parsowania danego szablonu. Szablony z tagiem otrzymują parametr z tablicą zawierającą statyczne
części szablonu oraz wyniki wykonanych wyrażeń, każdy w zależności od własnego parametru.
W ramach przykładu rozważ szablon literału z tagiem z poniższego fragmentu.
tag`Witaj, ${ name }. Tak bardzo ${ emotion } , że Cię widzę!`
Wyrażenie w szablonie z tagiem będzie w praktyce przetłumaczone na poniższe wywołanie funkcji.
tag(
[ Witaj, , . Tak bardzo , , że Cię widzę! ],
Maurice ,
cieszę się
)
Wynikiem będzie ciąg tekstowy zbudowany w następujący sposób: do każdego elementu szablo-
nu dołączane jest jedno wyrażenie tak długo, aż użyte zostaną wszystkie elementy szablonu.
Trudno interpretować listę argumentów bez rzucenia okiem na potencjalną implementację do-
myślnego literału szablonu tag, dlatego teraz właśnie tym się zajmiemy.
Poniższy fragment kodu pokazuje możliwą implementację domyślnego szablonu tag. Oferuje on
tę samą funkcjonalność, co literał szablonu, gdy szablon z tagiem nie jest bezpośrednio użyty. Re-
dukuje tablicę parts do pojedynczej wartości, a mianowicie wyniku literału szablonu. Wynik ten
jest zainicjalizowany pierwszym elementem part i potem każdy następny element (part) szablo-
nu jest poprzedzany jedną z wartości values.
Użyliśmy składni parametru resztowego ...values w celu łatwiejszego wychwycenia wyniku ob-
liczenia każdego z wyrażeń w szablonie. Korzystamy też z funkcji strzałki z domyślną instrukcją
return, zakładając, że jej wyrażenie jest relatywnie proste.
function tag(parts, ...values) {
return parts.reduce(
(all, part, index) = all + values[index - 1] + part
)
}
Możesz wypróbować szablon tag korzystający z kodu, takiego jak ten w poniższym przykładzie.
Zobaczysz, że otrzymasz takie same dane wyjściowe, jeśli pominiesz tag, ponieważ kopiujemy tu
zachowanie domyślne.
48
Rozdział 2. Najistotniejsze elementy ES6
Poleć książkęKup książkęvar name = Maurice
var emotion = cieszę się
var text = tag`Witaj, ${ name }. Tak bardzo ${ emotion } ,że Cię widzę!`
console.log(text)
// - Witaj, Maurice, tak bardzo cieszę się, że Cię widzę!
Szablonu z tagiem można użyć w wielu sytuacjach. Jedną z nich może być zamiana danych wej-
ściowych od użytkownika na wersaliki, które sprawią, że treść ciągu tekstowego nabierze charak-
teru bardziej ironicznego. Tak właśnie zadziała kod w poniższym przykładzie. Zmodyfikowaliśmy
nieco tag, żeby zamienił zinterpolowane fragmenty ciągu tekstowego na wersaliki.
function upper(parts, ...values) {
return parts.reduce((all, part, index) =
all + values[index - 1].toUpperCase() + part
)
}
var name = Maurice
var emotion = cieszę się
upper`Witaj, ${ name }. Tak bardzo ${ emotion }, żę Cię widzę!`
// - Witaj MAURICE, tak bardzo CIESZĘ SIĘ, że Cię widzę
Zdecydowanie bardziej przydatnym przykładem jest natomiast automatyczna filtracja wyrażeń
interpolowanych w naszym szablonie, możliwa dzięki użyciu szablonu z tagiem. Mając szablon,
w którym wszystkie wyrażenia miałyby pochodzić z danych wejściowych od użytkownika, mogli-
byśmy hipotetycznie wykorzystać bibliotekę sanitize („cenzuruj”) do usunięcia tagów HTML czy
innych niepożądanych elementów. W ten sposób moglibyśmy zapobiegać atakom XSS, uniemoż-
liwiając wstawienie podejrzanych fragmentów kodu w pola tekstowe na naszej stronie.
function sanitized(parts, ...values) {
return parts.reduce((all, part, index) =
all + sanitize(values[index - 1]) + part
)
}
var comment = Evil comment iframe src= http://evil.corp
/iframe
var html = sanitized` div ${ comment } /div `
console.log(html)
// - div Evil comment /div
Uff, niemal dopadłoby nas złośliwe iframe .
Zestaw składniowych nowości w ES6 zamykają instrukcje let oraz const.
2.6. Instrukcje let oraz const
Instrukcja let jest jedną z najbardziej znanych nowych możliwości ES6. Działa jak instrukcja var,
ale różni się od niej zasięgiem.
JavaScript ma od zawsze skomplikowany zestaw reguł dotyczących ustalania zasięgu, co dopro-
wadza programistów do szaleństwa, kiedy próbują zrozumieć, jak w tym języku działają zmienne.
Aż w końcu odkrywasz hoisting i JavaScript zaczyna nabierać nieco więcej sensu. Hoisting ozna-
cza, że zmienna jest brana z miejsca, w którym została zadeklarowana, i przenoszona na początek
swojego zasięgu. Przykładowo może wyglądać to tak, jak w poniższym kodzie.
2.6. Instrukcje let oraz const
49
Poleć książkęKup książkęfunction isItTwo(value) {
if (value === 2) {
var two = true
}
return two
}
isItTwo(2)
// - true
isItTwo( two )
// - undefined
Tego typu JavaScript działa, mimo że zmienna two została zadeklarowana w odgałęzieniu kodu i uzy-
skujemy do niej dostęp z zewnątrz, spoza tej gałęzi. Takie zachowanie wynika z faktu, że zmienne
deklarowane za pomocą instrukcji var przyjmują domyślnie zasięg otoczenia, np. funkcji, czy za-
sięg globalny. A to, w połączeniu z hoistingiem, oznacza, że kod, który napisaliśmy wcześniej, zo-
stanie zinterpretowany tak samo, jakbyśmy napisali coś w poniższym stylu.
function isItTwo(value) {
var two
if (value === 2) {
two = true
}
return two
}
Czy nam się to podoba, czy nie, hoisting bywa bardziej mylący niż użycie zmiennych o zasięgu
blokowym. Jednak zasięg blokowy działa raczej na poziomie nawiasów klamrowych, a nie na po-
ziomie całej funkcji.
2.6.1. Zasięg blokowy i deklaracje let
Jeśli potrzebujemy głębszego poziomu zasięgu, to zamiast deklarować nowy obiekt function, mo-
żemy skorzystać z zasięgu bloku, który pozwala na wykorzystanie istniejących odgałęzień kodu,
takich jak utworzone przez instrukcje if, for czy while. Możesz też budować dowolnie nowe blo-
ki z użyciem nawiasów klamrowych {}. Może o tym nie wiesz, ale JavaScript pozwala na tworze-
nie nieograniczonej liczby bloków, jeśli tylko tak trzeba.
{{{{{ var deep = Ten fragment jest dostępny w zewnętrznym zasięgu. ; }}}}}
console.log(deep)
// - Ten fragment jest dostępny w zewnętrznym zasięgu.
Używając instrukcji var, która korzysta z zasięgu leksykalnego, można uzyskać dostęp do zmien-
nej deep również spoza bloku, w którym została zadeklarowana, i nie dostać ostrzeżenia o błędzie.
Czasami jednak informacja o błędzie może być przydatna; dzieje się tak szczególnie wtedy, kiedy
znajdujesz się w sytuacji, którą opisuje któreś z poniższych stwierdzeń.
Dotarcie do wewnętrznej zmiennej łamie pewien rodzaj domknięcia w naszym kodzie.
Wewnętrzna zmienna w ogóle nie powinna znaleźć się w zewnętrznym zasięgu.
Blok, o który chodzi, ma wiele bratnich bloków, które chcą korzystać z tej samej nazwy
zmiennej.
Jeden z bloków nadrzędnych już posiada zmienną o tej samej nazwie, ale wciąż można by jej
użyć w bloku wewnętrznym.
50
Rozdział 2. Najistotniejsze elementy ES6
Poleć książkęKup książkęInstrukcja let jest więc alternatywą dla instrukcji var. Działa zgodnie z zasadami zasięgu bloko-
wego, a nie domyślnego zasięgu leksykalnego. Kiedy stosujemy var, jedynym sposobem na uzy-
skanie głębszego zasięgu jest utworzenie funkcji zagnieżdżonej, ale przy użyciu let można po prostu
otworzyć kolejny nawias klamrowy. A to oznacza, że nie potrzebujesz całkowicie nowej funkcji,
żeby utworzyć nowy zasięg — wystarczy tylko {}.
let topmost = {}
{
let inner = {}
{
let innermost = {}
}
// próby uzyskania stąd dostępu do innermost wyrzuciłyby błąd
}
// próby uzyskania stąd dostępu do inner wyrzuciłyby błąd
// próby uzyskania stąd dostępu do innermost wyrzuciłyby błąd
Przydatnym aspektem instrukcji let jest to, że możesz użyć ich podczas deklaracji pętli for i wte-
dy jej zmienne będą ograniczone do zasięgu wewnątrz pętli, tak jak pokazuje poniższy przykład.
for (let i = 0; i 2; i++) {
console.log(i)
// - 0
// - 1
}
console.log(i)
// - i i is not defined
Jeśli weźmiemy pod uwagę fakt, że zmienne zdeklarowane wewnątrz pętli za pomocą instrukcji
let będą związane z zasięgiem każdego kroku w tej pętli, to ich wiązania będą działały poprawnie
w połączeniu z wywołaniem funkcji asynchronicznej, w przeciwieństwie do dotychczasowej
Pobierz darmowy fragment (pdf)