Scott Tiger Tech Blog

Blog technologiczny firmy Scott Tiger S.A.

JavaScript

W sieci istnieje wiele tutoriali do języka JavaScript. Większość z nich jest przeznaczona dla początkujących i poprzestaje na podstawach. Tutaj będzie o JavaScripcie dla średniozaawansowanych, czyli dla tych co już programują w tym języku a chcieliby się dowiedzieć więcej.

Spis treści
1. Zakresy i domknięcia
  1.1. Zakres leksykalny
      1.1.1. Eval
      1.1.2. With
  1.2. Zakres funkcji kontra zakres bloku
      1.2.1. Funkcja jako zakres
      1.2.2. Blok jako zakres
            1.2.2.1. Try-catch
            1.2.2.2. Let i const
  1.3. Hoisting
2. Wskaźnik this i prototypy obiektów
  2.1. this
      2.1.1. Wiązanie domyślne
      2.1.2. Wiązanie niejawne
      2.1.3. Wiązanie jawne
      2.1.4. Wiązanie new
      2.1.5. Kolejność stosowania reguł
      2.1.6. Leksykalne this
  2.2. Obiekty
      2.2.1. Właściwości obiektu
      2.2.2. Deskryptory właściwości
            2.2.2.1. Niemodyfikowalność
            2.2.2.2. Gettery i settery
      2.2.3. Widoczność właściwości
  2.3. Prototypy
  2.4. Tryb ścisły (strict mode)
3. ECMAScript 6
  3.1. Polyfill i transpiling
  3.2. Składnia
      3.2.1. Rozproszenie (reszta)
      3.2.2. Domyślne wartości parametrów
      3.2.3. Destrukturyzacja
      3.2.4. Rozszerzenia literałów obiektowych
            3.2.4.1. Zwięzłe właściwości
            3.2.4.2. Zwięzłe metody
            3.2.4.3. Obliczane nazwy właściwości
            3.2.4.4. Ustawienie [[Prototype]]
      3.2.5. Literały szablonów
            3.2.5.1. Literały szablonów ze znacznikiem
      3.2.6. Funkcje typu arrow function
      3.2.7. Pętla for..of
      3.2.8. Wyrażenia regularne
      3.2.9. Rozszerzenia literałów liczbowych
      3.2.10. Unicode
      3.2.11. Symbole
  3.3. Organizacja
      3.3.1. Iteratory
      3.3.2. Moduły
            3.3.2.1. Eksport
            3.3.2.2. Import
      3.3.3. Klasy
  3.4. Kolekcje
      3.4.1. Map
      3.4.2. WeakMap
      3.4.3. Set
      3.4.4. WeakSet
  3.5. Modyfikacje API
      3.5.1. Array
      3.5.2. Object
      3.5.3. Math
      3.5.4. Number
      3.5.5. String
  3.6. Metaprogramowanie
      3.6.1. Obiekty pośredniczące – Proxy
      3.6.2. Interfejs API obiektu Reflect
  3.7. Dalszy rozwój języka po ES6
      3.7.1. SIMD
      3.7.2. WebAssembly

1. Zakresy i domknięcia

1.1. Zakres leksykalny

Użycie eval lub with spowalnia działanie kodu, ponieważ wyłącza optymalizacje silnika języka.

1.1.1. Eval

Kiedy funkcja eval jest używana w programie działającym w trybie ścisłym, to operuje na własnym zakresie leksykalnym. Oznacza to, że deklaracje wewnątrz eval w rzeczywistości nie będą modyfikowały jej zakresu nadrzędnego.

function foo(str)
  "use strict";
  eval( str );
  console.log( a ); // Reference error
}
foo( "var a = 2" );

1.1.2. With

Polecenie with (niedozwolone w trybie ścisłym) pobiera obiekt mający zero lub więcej właściwości, a następnie traktuje ten obiekt tak, jakby był całkowicie oddzielnym zakresem leksykalnym. Dlatego też właściwości obiektu są traktowane jako leksykalnie zdefiniowane identyfikatory w tym zakresie.

function foo(obj) {
  with(obj) { a = 2; }
}
var o1 = { a: 3 };
var o2 = { b: 3 };

foo( o1 );
console.log( o1.a ); // 2

foo( o2 );
console.log( o2.a ); // Niezdefiniowane
console.log( a ); // 2 (!)

1.2. Zakres funkcji kontra zakres bloku

1.2.1. Funkcja jako zakres

Ponieważ zakres zmiennej wyznacza funkcja w której zmienna została zadeklarowana, często spotyka się konstrukcję IIFE (ang. Immediately Invoked Function Expression):

(function IIFE() {
  var a = 3;
  ...
})();

1.2.2. Blok jako zakres

1.2.2.1. Try-catch

Począwszy od specyfikacji ES3 języka JavaScript, deklaracja zmiennej w klauzuli catch konstrukcji try-catch ma zasięg bloku catch.

try {
  ...
} catch( err ) {
  console.log( err );
}
console.log( err ); // Reference error: nie znaleziono err

1.2.2.2. Let i const

W ES6 wprowadzono nowe słowo kluczowe let, stanowiące obok polecenia var inny sposób deklarowania zmiennych – zakresem zmiennej jest blok. Deklaracje let nie podlegają hoistingowi do całego zakresu bloku:

{
  console.log( bar ); // ReferenceError!
  let bar = 2;
}

Przypadkiem szczególnym pokazującym niezwykłą wręcz użyteczność polecenia let jest pętla for:

for (let i = 0; i < 10; i++ ) {
  console.log( i );
}
console.log( i ); // ReferenceError

W rzeczywistości zmienna i jest tu deklarowana nie jeden raz dla całej pętli, lecz na nowo przy każdym obrocie pętli, co ma kluczowe znaczenie w połączeniu z domknięciami:

for( let i = 1; i <= 5; i++ ) {
  setTimeout( function timer() {
    console.log( i );
  }, i * 1000 );
}

Powyższy kawałek kodu wyświetli na ekranie liczby 1..5, natomiast jeśl zamiast let użyjesz var, wyświetlone zostaną wartości 6,6,6,6,6.

Obok let pojawiło się również const, które deklaruje stałą w tym sensie, że nie pozwala potem przypisać tak zadeklarowanej zmiennej nowej wartości.

1.3. Hoisting

Kuszące może być potraktowanie polecenia var a = 2 jako pojedynczego, ale silnik JavaScript nie postrzega go w taki sposób. Zamiast tego widzi dwa oddzielne polecenia, var a i a = 2. Pierwsze jest wykonywane w trakcie fazy kompilacji, a drugie w fazie właściwego wykonywania programu.

Prowadzi to do następującego wniosku: wszystkie deklaracje w zakresie, niezależnie od miejsca ich występowania, są przetwarzane przed rozpoczęciem wykonywania kodu. Można powiedzieć, że deklaracje (funkcji i zmiennych, w tej kolejności) są „przenoszone” na początek ich zakresów, a ten proces jest określany mianem hoistingu. Hoistingowi podlegają same deklaracje, natomiast operacje przypisania już nie.

console.log( a ); // undefined
var a = 2;
console.log( b ); // Reference Error: b is not defined

2. Wskaźnik this i prototypy obiektów

2.1. this

Słowo kluczowe this nie ma nic wspólnego z deklaracją funkcji. Jest to wiązanie powstające w trakcie wywoływania funkcji. To, do czego odwołuje się this jest determinowane przez źródło wywołania funkcji i sposób wywołania.

Z tej perspektywy, istnieją cztery rodzaje wiązania this. Omówimy je tutaj po kolei.

2.1.1. Wiązanie domyślne

foo()

Przy takim wywołaniu, this wewnątrz funkcji foo będzie się odnosiło do obiektu globalnego (w przeglądarce będzie to obiekt window), lub – jeśli w ciele funkcji będzie obowiązywał tryb ścisły – będzie miało wartość undefined.

2.1.2. Wiązanie niejawne

obj1.obj2.foo()

Tutaj this wewnątrz funkcji będzie się odwoływać do obiektu obj2 (ogólnie: w takim łańcuchu wywołań znaczenie ma tylko odniesienie do właściwości obiektu ostatniego poziomu). Ale:

var bar = obj1.obj2.foo;
bar();

W powyższym fragmencie kodu mamy do czynienia z wiązaniem domyślnym (this wewnątrz wywołanej funkcji nie będzie wskazywać na obj2, tylko na obiekt globalny lub undefined).

2.1.3. Wiązanie jawne

foo.call( obj );

Tutaj w wywołaniu funkcji jawnie ustawiamy this na obj. Alternatywnie można użyć foo.apply.

Odmianą wiązania jawnego jest wiązanie twarde:

var bar = function() {
  return foo.apply( obj, arguments ); 
}
bar(5);

W ES5 mamy do tego funkcję pomocniczą Function.prototype.bind():

var bar = foo.bind( obj );
bar(5);

Tutaj mała uwaga: jeśli w wywołaniu call lub apply jako pierwszy argument przekażemy null lub undefined, wówczas wewnątrz funkcji this będzie wskazywać na obiekt globalny. Najczęściej robimy tak, gdy wiązanie this nie ma znaczenia, ale wtedy na wszelki wypadek zamiast null lepiej podać Object.create(null) aby mieć pewność że nie zostanie zaśmiecona przestrzeń globalna.

2.1.4. Wiązanie new

Tutaj trzeba uściślić, czym tak naprawdę jest „konstruktor” w języku JavaScript. Konstruktor to po prostu funkcja, która ma zostać wywołana po użyciu operatora new. Konstruktor nie jest dołączony do klasy ani nie odpowiada za tworzenie egzemplarza klasy. Nie jest to również funkcja specjalnego typu. Konstruktor to zwykła funkcja, która tak naprawdę została przechwycona przez użycie słowa kluczowego new w jej wywołaniu. Nie ma czegoś takiego jak „funkcja konstruktora”, ale raczej wykonanie funkcji za pomocą wywołania konstruktora.

function foo(a) {
  this.a = a;
}
var bar = new foo( 2 );
console.log( bar.a ); // 2

Wywołanie konstruktora wykonuje następujące operacje:

  • Utworzenie (skonstruowanie) zupełnie nowego obiektu.
  • Nowo utworzony obiekt zostaje połączony z łańcuchem [[Prototype]] funkcji.
  • W trakcie wywołania funkcji this wskazuje na ten nowy obiekt.
  • Jeżeli wartością zwrotną funkcji nie jest alternatywny obiekt, wywołanie new automatycznie zwróci nowo skonstruowany obiekt.

2.1.5. Kolejność stosowania reguł

Jak się pewnie domyślasz, najwyższy priorytet ma wiązanie new, potem jawne i twarde, następnie niejawne a na końcu domyślne.

2.1.6. Leksykalne this

W specyfikacji ES6 wprowadzono specjalną syntaktyczną formę deklaracji funkcji, arrow function (zwana również „grubą strzałką”). Najczęściej spotykany przypadek użycia to prawdopodobnie wywołania zwrotne (ang. callbacks) i setTimeout:

function foo() {
  setTimeout( () => {
    // tutaj 'this' wskazuje na to samo, co w 'foo'
    console.log( this.a );
  }, 100);
}

var obj = { a: 2 };

foo.call( obj ); // 2

Wbrew pozorom nie jest to tylko wyeliminowanie konieczności pisania słowa kluczowego function. Pod względem wiązania this funkcja strzałki zachowuje się inaczej niż zwykła funkcja: odrzucane są standardowe reguły wiązania this i zamiast nich pobierana jest wartość this nadrzędnego zakresu leksykalnego, niezależnie od tego, jaki on jest. Takie leksykalne wiązanie nie może być nadpisane nawet za pomocą słowa kluczowego new! Wprawdzie takie rozwiązanie pozwala na tworzenie krótszego kodu, ale stanowi jedynie sposób pomagający w uniknięciu często występującego błędu programistów, którym jest mylenie i mieszanie reguł wiązania this z regułami zakresu leksykalnego. Pod względem syntaktycznym funkcja strzałki może być uznawana za odpowiednik self=this stosowany w kodzie przed wprowadzeniem specyfikacji ES6.

2.2. Obiekty

2.2.1. Właściwości obiektu

Nazwy właściwości (ang. properties) w obiektach zawsze są ciągami tekstowymi. Jeśli dla nazwy właściwości użyjesz innej wartości niż typ string (typ prosty), w pierwszej kolejności zostanie ona przekonwertowana na ciąg tekstowy. To dotyczy także liczb, które są bardzo często używane w charakterze indeksów tablic.

2.2.2. Deskryptory właściwości

Przed wprowadzeniem specyfikacji ES5 język JavaScript nie oferował żadnego bezpośredniego sposobu rozróżniania z poziomu kodu cech charakterystycznych właściwości, na przykład czy dana właściwość jest tylko do odczytu. Jednak począwszy od ES5, każda właściwość ma deskryptor opisujący jej zachowanie.

var myObject = { a: 2 };
Object.getOwnPropertyDescriptor( myObject, 'a' );
// {
//   value: 2,
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

Za pomocą wywołania Object.defineProperty można dodać nową właściwość lub zmodyfikować istniejącą (o ile cecha configurable ma wartość true!)

var myObject = {};
Object.defineProperty( myObject, 'a', {
  value: 2,
  writable: true,
  configurable: true,
  enumerable: true
});
myObject.a; // 2

Poniżej opis poszczególnych cech deskryptora. Próba naruszenia definiowanych przez cechę ograniczeń jest operacją pustą, chyba że użyto trybu ścisłego – wtedy otrzymujemy błąd.

  • writable: określa możliwość zmiany wartości właściwości (cecha value)
  • configurable: ta cecha określa, czy można modyfikować deskryptor za pomocą defineProperty; zmiana wartości tej cechy na false jest nieodwracalna. Nawet jeśli ma wartość false, to zawsze można jeszcze zmienić wartość writable z true na false (ale nie z false na true). Kolejną restrykcją nakładaną przez configurable:false jest brak możliwości użycia operatora delete w celu usunięcia właściwości.
  • enumerable: określa, czy właściwość będzie widoczna w pewnych rodzajach wyliczenia, takich jak pętla for-in. Wartość false nadal daje dostęp do właściwości przez podanie wprost jej nazwy.

2.2.2.1. Niemodyfikowalność

  • Object.preventExtensions( myObj ): uniemożliwia dodawanie nowych właściwości do obiektu i jednocześnie pozostawia w spokoju istniejące właściwości
  • Object.seal( myObj ): to samo co Object.preventExtensions plus configurable:false dla wszystkich istniejących właściwości (czyli nie będzie można konfigurować i usuwać istniejących właściwości, ale można modyfikować ich wartość)
  • Object.freeze( myObj ): to samo co Object.seal plus writable:false dla wszystkich istniejących właściwości

2.2.2.2. Gettery i settery

Począwszy od ES5 możemy przechwycić operacje odczytu i zapisu właściwości. Gdy na jeden z dwóch sposobów zdefiniujemy własny getter/setter dla właściwości, cechy value i writable są ignorowane.

var myObject = {
  get a() { return 2; }
};

Object.defineProperty( myObject, 'b',
  {
    get: function() { return this.a * 2; },
    enumerable: true
  }
);

Uwaga: w ES6 pojawiła się funkcja Object.assign do płytkiego kopiowania własności z jednego obiektu do drugiego. Jest to jednak powielenie w stylu operatora =, więc cechy deskryptora poszczególnych własności obiektu źródłowego nie zostaną zachowane w obiekcie docelowym.

2.2.3. Widoczność właściwości

Pętla for-in iteruje wszystkie wyliczalne właściwości obiektu lub jego łańcucha [[Prototype]]. Kolejność właściwości zwracanych przez for-in nie jest gwarantowana i może być różna w różnych silnikach JavaScript.

Ponadto:

  • 'propName' in myObj: sprawdza, czy właściwość znajduje się w obiekcie lub też na jakimkolwiek wyższym poziomie łańcucha [[Prototype]]. Cecha enumerable:false właściwości powoduje że właściwość ta nie zostanie uwzględniona w pętli for-in, ale istnienie właściwości jest ujawniane przez operator in.
  • myObj.hasOwnProperty('propName'): sprawdza jedynie, czy obiekt ma właściwość, ale nie analizuje łańcucha [[Prototype]]
  • myObj.propertyIsEnumerable('propName'): sprawdza czy właściwość pojawi się w for-in, chyba że nie jest dostępna bezpośrednio, a jedynie poprzez łańcuch [[Prototype]].
  • Object.keys( myObj ): tablica właściwości obiektu ujawnianych przez for-in, ale bez wchodzenia po łańcuchu [[Prototype]]
  • Object.getOwnPropertyNames( myObj ): zwraca tablicę wszystkich właśności obiektu, zarówno wyliczalnych jak i nie, ale bez wchodzenia po łańcuchu [[Prototype]]

Aktualnie nie ma żadnego wbudowanego w JavaScript sposobu na pobranie listy wszystkich właściwości, które będą odpowiednikiem zbioru sprawdzanego przez operator in.

2.3. Prototypy

Obiekty w JavaScript mają wewnętrzną właściwość oznaczaną w specyfikacji jako [[Prototype]]. Jest ona po prostu odwołaniem do innego obiektu. Niemal we wszystkich obiektach wartość właściwości [[Prototype]] jest w chwili tworzenia obiektu inna niż null. Powiązania między obiektami tworzą łańcuch delegowania, przy czym na ogół górnym końcem takiego łańcucha jest wbudowany obiekt Object.prototype.

Powiązanie możesz tworzyć np. tak:

var myObject = Object.create( anotherObject );

Przeanalizujmy teraz możliwe scenariusze dla operacji przypisania myObject.foo = "bar", gdy foo nie znajduje się bezpośrednio w obiekcie myObject, ale jest na wyższym poziomie łańcucha [[Prototype]] dla myObject.

  • Jeżeli standardowy akcesor danych w postaci właściwości o nazwie foo znajduje się gdziekolwiek wyżej w łańcuchu [[Prototype]] i nie jest oznaczony jako tylko do odczytu (writable:false), dodanie nowej właściwości o nazwie foo bezpośrednio do myObject skutkuje przesłonięciem właściwości (kopiowanie przy zapisie).
  • Jeżeli właściwość foo zostanie znaleziona wyżej w łańcuchu [[Prototype]], ale jest oznaczona jako tylko do odczytu (writable:false), ustawienie wartości tej istniejącej właściwości czy utworzenie przesłoniętej właściwości w myObject jest niedozwolone (zostanie zignorowane lub wystąpi błąd, w zależności od tego czy mamy tryb ścisły).
  • Jeżeli właściwość foo zostanie znaleziona wyżej w łańcuchu [[Prototype]] i jest setterem, zawsze będzie wywoływany setter. Do obiektu myObject nie zostanie dodana właściwość foo (nie dojdzie do przesłonięcia) ani nie będziemy mieli do czynienia z ponownym zdefiniowaniem settera.

Jeżeli upierasz się i chcesz przesłonić foo w ostatnich dwóch sytuacjach, nie możesz użyć przypisania (=), ale musisz skorzystać z Object.defineProperty().

Każda funkcja (nazwijmy ją Foo) posiada (niewyliczalną) własność prototype wskazującą na obiekt utworzony po napotkaniu deklaracji funkcji przez interpreter JavaScriptu. Obiekt tworzony za pomocą wywołania new Foo() będzie zawierał powiązanie [[Prototype]] z Foo.prototype:

function Foo() {
  this.myprop = 2;
  ...
}

var a = new Foo();

Object.getPrototypeOf( a ) === Foo.prototype; // true
Foo.prototype.constructor === Foo; // true
a instanceof Foo // true

Pytanie na które udziela odpowiedzi operator instanceof brzmi: czy w całym łańcuchu [[Prototype]] dla a istnieje obiekt wskazujący na Foo.prototype? To samo realizuje polecenie:

Foo.prototype.isPrototypeOf( a ); // true

Skoro mamy JavaScriptowy odpowiednik klasy, możemy stworzyć odpowiednik dziedziczenia:

function Bar() {
  ...
}
// Przed ES6, pozbycie się domyślnego Bar.prototype i Bar.prototype.constructor:
Bar.prototype = Object.create( Foo.prototype );
// Albo (dodane w ES6):
Object.setPrototypeOf( Bar.prototype, Foo.prototype );

Po łańcuchu [[Prototype]] możemy przechodzić za pomocą Object.getPrototypeOf():

Object.getPrototypeOf( a ) === Foo.prototype; // true

W wielu przeglądarkach alternatywnie można użyć właściwości __proto__ (zwanej „dunder proto”), zestandaryzowanej w ES6:

a.__proto__ === Foo.prototype; // true

Wywołanie Object.create(null) tworzy obiekt wraz z pustym (null) połączeniem [[Prototype]].Takie obiekty często nazywane są słownikami, ponieważ najczęściej są używane wyłącznie do przechowywania danych we właściwościach. Wynika to z faktu, że nie sprawiają żadnych problemów podczas obsługi delegowanych właściwości lub funkcji w łańcuchu [[Prototype]], a tym samym są jednorodnym magazynem danych.

Opcjonalny drugi argument Object.create() wskazuje nazwy właściwości, które mają być dodane do nowo utworzonego obiektu za pomocą deskryptora właściwości:

var myObject = Object.create( anotherObject, {
  b: {
    enumerable: false,
    writable: true,
    configurable: false,
    value: 3
  },
  c: {
    enumerable: true,
    writable: false,
    configurable: false,
    value: 4
  }
});

2.4. Tryb ścisły (strict mode)

Tryb włączamy poprzez użycie wyrażenia „use strict” w charakterze instrukcji.

  • powstaje kod bardziej zoptymalizowany przez silnik języka, np. tylko w tym trybie możliwa jest rekursja ogonowa (ang. Tail Call Optimization, TCO),
  • wyznacza przyszły kierunek rozwoju języka,
  • szereg cichych błędów powoduje teraz jawne zgłoszenie błędu, np. przypisanie do niezadeklarowanej zmiennej, naruszenie ograniczeń narzucanych przez deskryptor właściwości obiektu, itp.
  • zabrania pewnych konstrukcji, np. with, deklarowania funkcji w bloku
  • tryb można włączyć nie tylko dla całego pliku kodu źródłowego, ale również dla poszczególnych funkcji – wystarczy wyrażenie "use strict" umieścić na początku ciała funkcji.





3. ECMAScript 6

Oczekuje się, że tempo rozwoju języka JavaScript znacząco wzrośnie, od jednej wersji na kilka lat do jednej oficjalnej aktualizacji każdego roku – stąd też w schemacie nazwy języka ma się pojawić rok, np. ES6 jest znany jako ES2015, a ES7 jako ES2016.

3.1. Polyfill i transpiling

Obydwa pojęcia odnoszą się do możliwości używania nowych możliwości języka (np. ES6) w starszych przeglądarkach, które implementują jedynie starszą wersję języka (np. ES3).

Czasami nową funkcjonalność da się wyrazić w starej wersji języka (np. niektóre funcje dodane do API) – mówimy wtedy o tzw. skryptach polyfill (zwanych również bibliotekami typu shim), które wystarczy załadować do aplikacji by móc używać np. Array.map. Warto tu wspomnieć o ES5-Shim i ES6-Shim.

Ta sztuczka jednak nie zadziała w przypadku składni. Tu jednak nierzadko można dokonać transformacji kodu źródłowego przed załadowaniem go do przeglądarki. Taki proces określany jest mianem transpilingu (wyrażenie powstało z połączenia słów transforming i compiling).

Ponieważ JavaScript ulega nieustannej ewolucji i nowości pojawiają się znacznie częściej niż kiedyś, transpiling powinien być właściwie traktowany jako standardowy etap w procesie programowania w języku JavaScript. Jeśli będziesz domyślnie stosował transpiling, zawsze masz możliwość wykorzystywania najnowszej składni, gdy tylko uznasz ją za użyteczną, zamiast czekać kilka lat aż z rynku znikną obecne wersje przeglądarek internetowych.

Do dyspozycji mamy kilka doskonałych rozwiązań z zakresu transpilingu:

  • Babel (kiedyś 6to5) – pozwala na transpiling kodu zgodnego z ES6+ na postać kodu zgodnego z ES5.
  • Traceur (produkt Google’a) – pozwala na transpiling kodu zgodnego z ES6, ES7 i nowszych na postać kodu zgodnego z ES5.

Jak widać, JavaScript nie posiada czegoś takiego jak klasy znane z języków obiektowych – mamy tu wyłącznie obiekty. Ponieważ klasy można potraktować jak wzorzec projektowy, przy pewnym wysiłku można w JavaScripcie zaimplementować funkcjonalność przypominającą klasy.

3.2. Składnia

3.2.1. Rozproszenie (reszta)

Gdy operator ... jest używany przed dowolnym obiektem iterowalnym, powoduje on „rozproszenie” go na poszczególne wartości:

function foo(x, y, z) {
  console.log( x, y, z );
}

foo( ...[1,2,3] );

var a = [2,3,4];
var b = [1, ...a, 5 ];
b; // [1,2,3,4,5]

Kolejne zastosowanie tego operatora można uznać za odwrotne:

function foo( x, y, ...z ) {
  console.log( x, y, z );
}

foo( 1, 2, 3, 4, 5 ); // 1 2 [3,4,5]

3.2.2. Domyślne wartości parametrów

function foo( x = 11, y = 31 ) {
  console.log( x + y );
}

Parametr używający operatora ... nie może mieć wartości domyślnej. A zatem choć użycie zapisu foo(...vals=[1,2,3]) jest kuszące, to jednak taki zapis jest nieprawidłowy.

Gdy wyliczenie wartości domyślnej wymaga wykonania funkcji:

function foo( x = y+3, z = bar(x) ) { ... }

wówczas można zauważyć, że funkcja bar() jest wykonywana w sposób leniwy i tylko wtedy, gdy nie podano drugiego argumentu aktualnego funkcji foo. (Proszę zauważyć że bar() – o ile w ogóle – jest wywoływana przy wywołaniu funkcji foo, a nie przy jej deklaracji).

3.2.3. Destrukturyzacja

Lepsza nazwa dla destrukturyzacji to strukturalne przypisanie.

function foo() { return [1,2,3]; }
function bar() { return { x: 4, y: 5, z: 6 } }

var [ a, b, c ] = foo();
var { x: d, y: e, z: f } = bar();
console.info( a, d ); // 1, 4

W przypadku literałów obiektowych zapis x:d oznacza, że właściwość x jest wartością źródłową, a d jest zmienną docelową, w której wartość ta zostanie zapisana.

Jeśli zapis właściwości odpowiada nazwie zmiennej, którą chcemy zadeklarować, to zapis ten można jeszcze bardziej skrócić:

var { x, y, z } = bar();
console.info( x, z ); // 4, 6

Co więcej, w wyrażeniach przypisania wcale nie muszą być używane jedynie identyfikatory zmiennych – może to być dowolne prawidłowe wyrażenie, któremu można przypisać wartość:

var o = {};
[o.a, o.b, o.c] = foo();
({x: o.x, y: o.y, z: o.z } = bar());

Destrukturyzacji można użyć do zamiany wartości w zmiennych:

[y, x] = [x, y];

Destrukturyzacja nie wymaga przypisywania wszystkich wartości:

var [,b] = foo();
var { x, z } = bar();

console.log( b, x, z ); // 2 4 6

Także próba przypisania większej liczby wartości niż dostępne nie spowoduje błędu:

var [,,c,d] = foo();
var { w, z } = bar();

console.log( c, z, d, w ); // 3 6 undefined undefined

Jeśli operator rozproszenia (...) zostanie użyty w destrukturyzacji, to spowoduje zbieranie wartości:

var a = [2,3,4];
var [b, ...c] = a;

console.log( b, c ); // 2 [3,4]

Destrukturyzacja pozwala na określanie wartości domyślnych:

var [ a, b, c = 9, d = 12] = foo();
var { x, y, z = 15, w: WW = 20 } = bar();

console.log( a, b, c, d ); // 1 2 3 12
console.log( x, y, z, WW ); // 4 5 6 20

Zagnieżdżone destrukturyzacje mogą stanowić prosty sposób spłaszczania przestrzeni nazw obiektów, np.:

var App = {
  model: {
    User: function() { ... }
  }
};

// zamiast:
// var User = App.model.User;

var { model: { User } } = App;

Destrukturyzację możemy użyć również w kontekście parametrów funkcji:

function f1( [x, y] ) { console.log( x, y ); }
function f2( { x, y } ) { console.log( x, y ); }

f1( [1,2] ); // 1 2
f1( [1] );   // 1 undefined
f1( [] );    // undefined undefined
f2( { y: 1, x: 2 } ); // 2 1
f2( { y: 42 } );      // undefined 42
f2( {} );             // undefined undefined

Celem destrukturyzacji jest nie tylko skrócenie kodu, lecz także poprawa jego czytelności, dlatego nie utrudniaj życia sobie i innym programistom:

var x = 200, y = 300, z = 100;
var o1 = { x: { y: 42 }, z: { y: z } };

( { y: x = { y: y } } = o1 );
( { z: y = { y: z } } = o1 );
( { x: z = { y: x } } = o1 );

3.2.4. Rozszerzenia literałów obiektowych

3.2.4.1. Zwięzłe właściwości

var x = 2, y = 3,
    o = {
      x: x,
      y: y
    };
var x = 2, y = 3,
    o = {
      x,
      y
    };

3.2.4.2. Zwięzłe metody

var o = {
  x: function() {
    ...
  },
  y: function() {
    ...
  }
}
var o = {
  x() {
    ...
  },
  y() {
    ...
  }
}

3.2.4.3. Obliczane nazwy właściwości

var prefix = 'foo';
var myObject = {
  [prefix + 'bar']: 'Witaj,',
  [prefix + 'baz']: 'świecie!'
};
myObject['foobar']; // Witaj,
myObject['foobaz']; // świecie!

3.2.4.4. Ustawienie [[Prototype]]

Czasami przydatne może być ustawienie [[Prototype]] obiektu podczas deklarowania jego literału obiektowego. Poniższe rozwiązanie było niestandardowym rozszerzeniem stosowanym już od jakiegoś czasu w wielu silnikach JavaScript, jednak w ES6 zostało ono zestandaryzowane:

var o1 = { ... };
var o2 = {
  __proto__: o1,
  ...
}

3.2.5. Literały szablonów

Podobnie jak w wielu językach skryptowych (np. Ruby) w JavaScripcie pojawiły się literały łańcuchowe, tzw. literały szablonów (ang. template literals) w których można osadzać interpolowane wyrażenia (w tym zagnieżdżone literały szablonów) – takie łańcuchy obejmujemy znakami odwrotnego apostrofu (`) i można je zapisywać w wielu wierszach.

var text = `Bardzo ${upper("gorąco")} witam
wszystkich ${upper(`${who}ów`)} tej książki`;

3.2.5.1. Literały szablonów ze znacznikiem

Mechanizm literałów szablonów ze znacznikiem (ang. tagged template literals) daje nam dostęp do mechanizmu konstrukcji literału szablonu, po przetworzeniu wyrażeń interpolowanych ale przed ostatecznym połączeniem fragmentów w końcowy łańcuch znaków:

function foo( strings, ...values ) {
  console.log( strings );
  console.log( values );
}

var desc = "niesamowite";

foo`To wszystko jest ${desc}!`;
// ["To wszystko jest", "!"]
// ["niesamowite"]

3.2.6. Funkcje typu arrow function

var f1 = () => 12;
var f2 = x => x * 2;
var f3 = (x,y) => {
  ...
};

Nawias okrągły dla parametrów jest opcjonalny tylko wtedy gdy funkcja ma dokładnie jeden parametr. Nawias klamrowy dla ciała funkcji jest konieczny tylko wtedy gdy zawiera więcej niż jedno wyrażenie lub instrukcję niebędącą wyrażeniem. Brak nawiasku klamrowego zakłada że przed wyrażeniem ciała funkcji jest return.

W odróżnieniu od zwykłych funkcji, w funkcjach typu arrow wiązanie this, arguments i super jest leksykalne – dziedziczone z zakresu zewnętrznego.

3.2.7. Pętla for..of

Pętla tego typu operuje na zbiorze wartości zwracanym przez iterator:

var a = ['a', 'b', 'c', 'd', 'e'];
for( var val of a ) {
  console.log( aval );
}

Taka pętla jest równoważna zapisowi:

for( var val, ret, it = a[Symbol.iterator](); (ret = it.next()) && !ret.done ) {
  console.log( ret.value );
}

Do standardowych, wbudowanych danych JavaScriptu, które domyślnie są wartościami iterowalnymi (lub mogą je udostępniać), należą:

  • tablice,
  • łańcuchy znaków,
  • generatory,
  • kolekcje.

Działanie pętli for..of, tak samo jak wszystkich pozostałych, można przerwać przed jej zakończeniem, używając w tym celu instrukcji break, continue oraz return (w przypadku pętli umieszczonych wewnątrz funkcji), jak również zgłaszając wyjątek. W każdym z tych przypadków jest automatycznie wywoływana funkcja return() iteratora (jeśli tylko istnieje), zapewniając mu możliwość wykonania czynności porządkowych.

3.2.8. Wyrażenia regularne

Własność flags wyrażenia regularnego zwraca jego flagi.

Łańcuchy znaków w języku JavaScript są standardowo traktowane jako sekwencje znaków 16-bitowych, odpowiadających znakom należącym do BMP (ang. Basic Multilingual Plane), tymczasem istnieją znaki Unicode, na które składają się dwa i więcej kody 16-bajtowe. Nowa flaga u wyrażenia regularnego wymusza traktowanie tych rozszerzonych znaków jako pojedynczych. Ma to znaczenie w wyrażeniu regularnym chociażby dla kwantyfikatorów „.”, „+” i „*”, które bez tej flagi operują wyłącznie na tzw. niższym surogacie.

Kolejna nowa flaga, y (tzw. tryb sticky) powoduje że wyrażenie regularne ma na swoim początku jakby wirtualną kotwicę, która sprawia, że dopasowywanie będzie się odbywało wyłącznie w miejscu określonym przez wartość własności lastIndex wyrażenia regularnego. Jeśli uda się dopasować wzorzec, wartość lastIndex jest aktualizowana tak, by wskazywała na znak bezpośrednio za dopasowanym fragmentem łańcucha wejściowego. Gdy nie uda się dopasować wzorca, lastIndex jest ustawiany na 0. Flaga y może mieć sens dla danych strukturalnych. Można także ustawiać lastIndex ręcznie.

3.2.9. Rozszerzenia literałów liczbowych

Teraz dysponujemy już oficjalnie przyjętą formą ósemkową, poprawionym zapisem szesnastkowym oraz całkowicie nowym zapisem dwójkowym. Ze względu na konieczność zapewniania zgodności z wcześniejszym kodem dotychczasowa forma zapisu ósemkowego, 052, wciąż będzie prawidłowa (choć nie została uwzględniona w specyfikacji), ale nie w trybie ścisłym.

var dec = 42,
    oct = 0o52,
    hex = 0x2a,
    bin = 0b101010;

Istnieje również mało znana możliwość przeprowadzenia konwersji w odwrotnym kierunku:

var a = 42;
a.toString(10); // "42"
a.toString(8); // "52"
a.toString(2); // "101010"

3.2.10. Unicode

Obsługa znaków Unicode w ES6 jest znacząco lepsza niż w poprzednich wersjach JavaScriptu, ale wciąż nie jest doskonała.

Znaki Unicode o wartościach z zakresu 0x0000 do 0xFFFF zawierają wszystkie standardowe znaki drukowane (z różnych języków), które być może widziałeś lub już wykorzystywałeś. Ta grupa znaków jest nazywana podstawową płaszczyzną wielojęzyczną (ang. Basic Multilingual Plane, BMP). Zawiera ona także wiele zabawnych symboli, takich jak bałwanek (U+2603):

Jednak istnieje bardzo wiele rozszerzonych znaków Unicode, które nie mieszczą się w zbiorze BMP, i których wartości sięgają do 0X10FFFF. Są one często nazywane symbolami astralnymi, co odpowiada nazwom nadanym 16 płaszczyznom (warstwom, zbiorom) znaków Unicode oprócz BMP. Przykładem takiego symbolu jest U+1D11E:
𝄞

Przed ES6 jedynym sposobem uzyskania astralnego znaku Unicode było zastosowanie tzw. pary surogatów (zapis \uXXXX pozwala na podanie max. 4 cyfr szesnastkowych):

console.log( "\uD834\uDD1E" ); // 𝄞

W ES6 wprowadzono tzw. zapis specjalny punktów kodowych (ang. code point escaping), w którym można podać więcej niż 4 cyfry szesnastkowe:

console.log( "\u{1D11E}" ); // 𝄞

Niestety, większość operacji na łańcuchach zawierających znaki astralne liczy każdy taki znak jako dwa znaki:

"\u{1D11E}".length // 2

Aby prawidłowo wyznaczyć długość takiego łańcucha, trzeba się uciekać do sztuczek (mało wydajnych):

var gclef = "\u{1D11E}";
[...gclef].length; // 1
Array.from( gclef ).length; // 1

Wyczerpujące rozwiązanie tego problemu nie jest tak proste. Oprócz par surogatów istnieją także specjalne punkty kodowe Unicode, które zachowują się w zupełnie inny sposób i których uwzględnienie jest znacznie trudniejsze. Na przykład istnieje zestaw tzw. łączonych znaków diakrytycznych (ang. combining diacritical marks), które modyfikują znaki umieszczone przed nimi. Rozwiązaniem jest tu poddanie łańcucha znaków normalizacji Unicode przed próbą określenia jego długości:

"e\u0301".normalize() == "\xE9"; // true

ES6 udostępnia metody String.codePointAt() i String.fromCodePoint(), które odpowiadają starym String.charCodeAt() i String.fromCharCode(), ale potrafią obsługiwać poprawnie wszystkie znaki Unicode. Ponadto metody operujące na łańcuchach i korzystające z wyrażeń regularnych (replace(), match()) będą działać prawidłowo po zastosowaniu flagi u dla wyrażenia regularnego. Jednak większość metod operujących na łańcuchach (toUpperCase(), substring(), indexOf(), charAt(), slice() itp.) nie obsługuje prawidłowo znaków astralnych i traktuje je jako dwa znaki.

3.2.11. Symbole

W ES6 dodano nowy prosty typ danych: symbol, który nie ma formy literałowej:

var sym = Symbol( "jakiś opis" );

typeof sym; // "symbol"

W wywołaniach Symbol nie można i nie należy używać new.

Symboli można używać bezpośrednio jako właściwości (kluczy) w obiektach, na przykład by tworzyć specjalne właściwości, które chcemy traktować jako ukryte lub jako metadane w tym sensie że nie będą uwzględniane przez tradycyjne wyliczenie (iterator), ale można je wydobyć za pomocą Object.getOwnPropertySymbols(obj).

Język ES6 udostepnia szereg predefiniowanych, wbudowanych symboli zapisywanych jako właściwości obiektu funkcji o nazwie Symbol, np. Symbol.iterator.

3.3. Organizacja

3.3.1. Iteratory

Iteratory stanowią narzędzie do uporządkowanego, sekwencyjnego dostępu do danych pobieranych na żądanie. W ES6 wprowadzony został niejawny standaryzowany interfejs iteratorów. Wiele wbudowanych struktur danych języka JavaScript udostępnia iteratory implementujące ten nowy standard, można także tworzyć własne iteratory.

Kolejne wywołania metody next() iteratora zwracają obiekty postaci:

{ value: ..., done: true/false }

Nic nie stoi na przeszkodzie, by w razie konieczności zawierały one inne właściwości z metadanymi, np. skąd pochodzą dane, jak długo trwało ich pobranie itp.

var arr = [1,2,3];
var it = arr[Symbol.iterator]();

it.next(); // {value: 1, done: false}
it.next(); // {value: 2, done: false}
it.next(); // {value: 3, done: false}
it.next(); // {value: undefined, done: true}

Każde wywołanie metody identyfikowanej przez Symbol.iterator wykonane na rzecz arr spowoduje zwrócenie nowego iteratora. Niemniej jednak może się zdarzyć, że jakieś struktury, np. obiekt obsługujący kolejkę zdarzeń, będą generowały wyłącznie jeden iterator (zgodnie z wzorcem singletonu). Może się też zdarzyć, że struktura będzie udostępniać jeden iterator w danej chwili, wymuszając zakończenie jego działania przed utworzeniem następnego.

Do metody next() iteratora można opcjonalnie przekazać jeden lub większą liczbę argumentów. Wbudowane iteratory przeważnie nie korzystają z tej możliwości, w odróżnieniu od iteratorów generatorów.

Wywołanie next() na rzecz zakończonego iteratora nie powoduje zgłoszenia błędu, lecz zwrócenie obiektu o postaci { value: undefined, done: true }.

Dwie opcjonalne metody iteratorów: return() oraz throw() nie zostały zaimplementowane w większości wbudowanych iteratorów. Niemniej jednak bez wątpienia mają one duże znaczenie w kontekście generatorów. Metoda return() została zdefiniowana jako sygnał oznaczający, że kod korzystający z iteratora nie będzie już pobierał z niego żadnych dalszych wartości. Tego sygnału można użyć do poinformowania producenta (iteratora odpowiadającego na wywołania metody next()), że powinien wykonać wszelkie niezbędne czynności porządkowe, takie jak zwolnienie używanych uchwytów do zasobów sieciowych, baz danych czy też plików. Jeśli iterator udostępnia metodę return() oraz jeśli zaszły jakiekolwiek warunki, które można zinterpretować jako nieprawidłowe lub przedwczesne zakończenie używania tego iteratora, to automatycznie zostanie wywołana metoda return(). Metodę tę można także wywoływać samodzielnie. Wywołanie metody return() spowoduje zwrócenie obiektu IteratorResult, podobnie jak wywołanie metody next(). Ogólnie rzecz biorąc, opcjonalna wartość przekazywana w wywołaniu metody return() będzie zwracana jako wartość właściwości value zwracanego obiektu IteratorResult.

Metoda throw() służy do przekazania iteratorowi informacji o wyjątku lub błędzie, która ewentualnie może zostać użyta przez iterator w inny sposób niż sygnał zakończenia reprezentowany przez metodę return(). Nie musi to jednak oznaczać całkowitego zakończenia iteratora, jak zazwyczaj jest traktowane wywołanie metody return(). Na przykład w przypadku iteratorów stosowanych przez generatory wywołanie throw() powoduje wstawienie zgłoszonego wyjątku do konstekstu wstrzymanego wykonywania generatora, dzięki czemu wyjątek ten może zostac przechwycony przez instrukcję try...catch. Nieprzechwycony wyjątek, zgłoszony przy użyciu metody throw(), doprowadzi do przedwczesnego zakończenia działania generatora.

Na mocy ogólnie przyjętej konwencji po wywołaniu metody return() lub throw() iterator nie powinien już zwracać żadnych dalszych wartości.

Przykład własnego iteratora:

var Fib = {
    [Symbol.iterator]() {
        var n1 = 1, n2 = 1;

        return {
            // make the iterator an iterable
            [Symbol.iterator]() { return this; },
            next() {
                var current = n2;
                n2 = n1;
                n1 = n1 + current;
                return { value: current, done: false };
            },
            return(v) {
                console.log( "Fibonacci sequence abandoned." );
                return { value: v, done: true };
            }
        };
    }
};

for (var v of Fib) {
    console.log( v );
    if (v > 50) break;
}
// 1 1 2 3 5 8 13 21 34 55
// Fibonacci sequence abandoned.

Iteratory działają też z operatorem rozproszenia (...):

var a = [1,2,3,4,5];
var b = [0, ...a, 6];
b; // [0,1,2,3,4,5,6]
var it = a[Symbol.iterator]();
var [x,y] = it;
var [z, ...w] = it;
it.next(); // { value: undefined, done: true }
x; // 1
y; // 2
z; // 3
w; // [4,5]

3.3.2. Moduły

Podstawowe różnice między modułami ES6 oraz modułami stosowanymi w przeszłości (opartymi głównie na domknięciach):

  • Moduły ES6 bazują na plikach, co oznacza że jeden moduł odpowiada jednemu plikowi. Obecnie nie jest dostępny żaden standardowy sposób pozwalający na umieszczenie kilku modułów w jednym pliku. Sądzi się, że oczekiwane nadejście HTTP2 w znaczącym stopniu ograniczy problemy z wydajnością pobierania z serwera wielu plików przez przeglądarkę.
  • Interfejs API modułów ES6 jest statyczny. Wszystkie eksportowane elementy najwyższego poziomu, tworzące publiczny interfejs API modułu, są definiowane statycznie; a później nie można ich już zmieniać.
  • Moduły ES6 są singletonami – istnieje tylko jeden egzemplarz modułu przechowujący jego stan.
  • W ES6 wyeksportowanie lokalnej zmiennej prywatnej, nawet takiej, która zawiera wartość typu prostego (łańcuch znaków, liczbę itd.), powoduje wyeksportowanie powiązania z tą zmienną. Jeśli moduł zmieni wartość zmiennej, to używane na zewnątrz modułu powiązanie z tą zmienną udostępni jej nową wartość. Tworzenie powiązań dwukierunkowych nie jest dozwolone – jeśli z modułu zaimportujemy foo, a następnie spróbujemy zmienić wartość zaimporowanej zmiennej foo, to zostanie zgłoszony błąd.
  • Zaimportowanie modułu jest równoznaczne ze statycznym zażądaniem jego wczytania (jeśli nie został on wczytany już wcześniej). W przypadku aplikacji wykonywanej w przeglądarce oznacza to pobranie modułu przez sieć z zablokowaniem wykonywania aplikacji. W przypadku kodu wykonywanego po stronie serwera (np. w środowisku Node.js) będzie to oznaczało zablokowanie wykonywania aplikacji i wczytanie modułu z lokalnego systemu plików.
  • W module nie ma zakresu globalnego – deklaracja najwyższego poziomu odpowiada zakresowi samego modułu. Moduły wciąż mają dostęp do obiektu window i wszystkich udostępnianych przez niego elementów „globalnych”, nie dysponują jednak zakresem leksykalnym najwyższego poziomu.

3.3.2.1. Eksport

Zacznijmy od tzw. eksportów nazwanych:

function foo() { ... }
export var awesome = 42;
var bar = [1,2,3];
export { foo as myFoo, bar };

W powyższym przykładzie nazwa foo będzie ukryta wewnątrz modułu, a funkcja będzie dostępna na zewnątrz pod nazwą myFoo.

Choć w definicji modułu można wielokrotnie używać słowa kluczowego export, to jednak ES6 zdecydowanie preferuje rozwiązanie, które zakłada, że moduł ma tylko jedną eksportowaną składową, nazywaną eksportem domyślnym (ang. default export). Zgodnie ze słowami jednego z członków komitetu TC39, stosując ten wzorzec, jesteśmy „nagradzani prostszą składnią instrukcji import„, i na odwrót – kiedy go nie stosujemy, jesteśmy „karani” koniecznością korzystania z bardziej złożonej składni.

function foo() { ... }
function bar() { ... }
function baz() { ... }

export { foo as default, bar, baz };

Alternatywnie mogliśmy zapisać:

export default foo;

a nawet

export default function foo() { ... };

Może być kuszące użycie konstrukcji w rodzaju:

export default {
  f1() { ... },
  f2() { ... }
};

ale oficjalne źródła nie zalecają jej stosowania. Chodzi o to, że silniki JavaScript nie są w stanie statycznie analizować zawartości zwyczajnych obiektów, a to oznacza, że nie mogą przeprowadzić pewnych optymalizacji poprawiających wydajność statycznych operacji importu. Jeśli musimy wyeksportować wiele bytów, to niech to będzie jeden domyślny a pozostałe nazwane, lub nawet zróbmy wszystkie nazwane i oczekujmy że użytkownik użyje konstrukcji import * as ... aby zaimportować je wszystkie.

3.3.2.2. Import

Aby zaimportować konkretne, nazwane składowe interfejsu API modułu do zakresu najwyższego poziomu:

import { foo, bar, baz as myBaz } from "foo";

bar();
myBaz();

Jeśli moduł udostępnia wyłącznie eksport domyślny, który chcemy powiązać z identyfikatorem:

import foo from "foo";

To jest to samo co:

import { default as foo } from "foo";

Jeśli chcemy zaimportować eksport domyślny oraz dwa pozostałe eksporty nazwane:

import FOOFN, { bar, baz as BAZ } from "foo";

FOOFN();
bar();
BAZ();

Filozofia stosowania modułów ES6 niezwykle mocno sugeruje, że z modułu należy importować wyłącznie konkretne, niezbędne powiązania, które będą używane w kodzie.

Załóżmy, że interfejs modułu „foo” jest eksportowany w następujący sposób:

export function bar() { ... }
export var x = 42;
export function baz() { ... }

Cały ten interfejs API można zaimportować do jednego powiązania przestrzeni nazw modułu:

import * as foo from "foo";

foo.bar();
foo.baz();
foo.x = 10; // TypeError, bo nie wolno modyfikować z zewnątrz modułu

Jeśli moduł importowany przy użyciu takiego zapisu zawiera eksport domyślny, to w podanej przestrzeni nazw będzie on dostępny pod nazwą default.

I w końcu najprostsza forma instrukcji import ma postać:

import "foo";

Nie powoduje ona zaimportowania żadnych powiązań modułu do bieżącego zakresu, powoduje natomiast wczytanie modułu (jeśli jeszcze nie został wczytany), skompilowanie go oraz przetworzenie.

Niestety, specyfikacja ES6 nie określa jak ma wyglądać implementacja mechanizmu ładowania modułów, i póki co (2016) żadna przeglądarka jeszcze ich nie obsługuje.

3.3.3. Klasy

class Foo {
  constructor( a, b ) {
    this.x = a;
    this.y = b;
  }
  static cool() { console.log( 'super' ); }
  gimmeXY() {
    return this.x * this.y;
  }
}

class Bar extends Foo {
  constructor( a, b, c ) {
    super( a, b );
    this.z = c;
  }
  static awesome() {
    super.cool();
    console.log( 'niesamowite' );
  }
  gimmeXYZ() {
    return super.gimmeXY() * this.z;
  }
}

var bar = new Bar(3, 4, 5);
bar.gimmeXYZ(); // 60
bar instanceof Foo; // true
Foo.cool(); // super
Bar.cool(); // super
Bar.awesome(); // super niesamowite

Składowych klasy nie należy (i nie można) rozdzielać średnikami.

Definiowanie konstruktorów w klasach oraz klasach pochodnych nie jest konieczne – w razie ich pominięcia zostanie zastosowany konstruktor domyślny. Domyślny konstruktor klasy pochodnej automatycznie wywołuje konstruktor klasy nadrzędnej, przekazując przy tym do niego wszystkie dostępne argumenty.

Uwaga: w konstruktorze klasy pochodnej nie można korzystać z this, zanim nie zostanie wykonane wywołanie super.

Można tworzyć pochodne klas wbudowanych, np. Array i Error.

Nowa właściwość new.target jest dostępna we wszystkich funkcjach, choć w przypadku normalnych funkcji będzie ona zawsze mieć wartość undefined. W dowolnym konstruktorze new.target zawsze będzie wskazywać na konstruktor, który faktycznie został bezpośrednio wywołany przez operator new (np. możesz użyć new.target.name aby poznać nazwę klasy). Jeśli właściwość new.target ma wartość undefined, to można mieć pewność że funkcja nie została wywołana z użyciem new – wtedy, jeśli to konieczne, można wymusić wywołanie new.

3.4. Kolekcje

3.4.1. Map

W porównaniu z używaniem Object jako tablicy asocjacyjnej:

  • Kluczem może być dowolny obiekt, nie tylko string. Mechanizm określania unikalności kluczy nie korzysta z konwersji typów, dlatego "1" i 1 są traktowane jako odrębne, unikalne wartości.
  • Mapa ma właściwość size zwracającą rozmiar kolekcji.
  • Nie można użyć operatora [].
var m = new Map();
m.set( 'a', 10 );
m.get( 'a' ); // 10
m.has( 'a' ); // true
m.size // 1
m.delete( 'a' );
m.clear();

W wywołaniu konstruktora Map() można także przekazać obiekt iterowalny, który musi zwracać listę tablic, przy czym pierwszy element każdej tablicy jest kluczem, a drugi – skojarzoną z nim wartością. Ten format iteracji idealnie odpowiada efektom działania metody entries(), dzięki czemu można bardzo łatwo tworzyć kopię map:

var m2 = new Map( m.entries() );
var m2 = new Map( m ); // to samo
var m = new Map( ['a', 10], ['b', 20] );

Ponieważ instancja mapy jest obiektem iterowalnym, a jej domyślny iterator działa tak samo jak metoda entries(), to preferowany jest drugi, krótszy sposób wywoływania konstruktora.

Metody keys() i values() zwracają iteratory odpowiednio kluczy i wartości mapy:

var keys = [ ...m.keys() ];

3.4.2. WeakMap

W przypadku map typu WeakMap kluczami mogą być wyłącznie obiekty. Są one przechowywane w słaby (ang. weak) sposób, co oznacza, że w przypadku ich usunięcia przez mechanizm odśmiecania pamięci usunięty zostanie także cały element mapy. Dobrym przykładem klucza są elementy drzewa DOM. Mapy typu WeakMap nie mają właściwości size, metody clear() ani też nie udostępniają iteratorów. Warto pamiętać, że w mapach tego typu w sposób „słaby” przechowywane są jedynie klucze, ale już nie wartości.

3.4.3. Set

var s = new Set( [5, 10, 15] );
s.add( 20 ).add( 4 );
s.has( 5 ); // true
s.delete( 10 );
s.clear();
s.size; // 0

Iteratory keys() i values() zwracają listę wszystkich unikalnych wartości zbioru. Domyślny iterator to values().

Mechanizm określania unikalności wartości nie korzysta z konwersji typów, dlatego "1" i 1 są traktowane jako odrębne, unikalne wartości.

3.4.4. WeakSet

Wartościami mogą być wyłącznie obiekty (nie mogą być wartości typów prostych) przechowywane w sposób „słaby”, nie można takiego zbioru iterować ani poznać jego liczności – ma wyłącznie metody add, delete i has.

3.5. Modyfikacje API

3.5.1. Array

Array.of(el1, el2, ...)
Tworzy nową tablicę wypełniając ją podanymi elementami.

var c = Array.of( 1, 2, 3 );
Array.from(arrayLike[, mapFn[, thisArg]]))
Tworzy nową tablicę i wypełnia ją elementami wskazanej struktury iterowalnej lub przypominającej tablicę. Opcjonalny drugi argument pozwala przekształcić argumenty kopiowanej struktury.

var arr = Array.from( arguments );
var arrCopy = Array.from( arr );
var c = Array.from( { length: 2 } ); // c == [undefined, undefined]
Array.prototype.copyWithin(target[, start[, end]])
Kopiuje fragment tablicy do innego miejsca w tej samej tablicy, przesłaniając dotychczasową wartość. Jeśli któryś z parametrów będzie liczbą ujemną, oznacza to element liczony względem końca tablicy. Metoda nie zwiększa długości tablicy i zwraca zmodyfikowaną tablicę.

[1,2,3,4,5].copyWithin( 0, -2 ); // [4,5,3,4,5]
Array.prototype.fill(value[, start[, end]])
Wypełnia wskazany fragment tablicy podaną wartością.

var a = [null, null, null, null ].fill( 42, 1, 3 );
a; // [null, 42, 42, null]
Array.prototype.find(callback[, thisArg])
Zwraca pierwszy element tablicy dla którego wywołanie zwrotne zwróci true, albo undefined gdy nie ma takiego elementu. Parametry wywołania zwrotnego to element, index, array.

[1,2,3,4,5].find( function (v) { return v == '2'; } ); // 2
Array.prototype.findIndex(callback[, thisArg])
Zwraca indeks pierwszego elementu tablicy dla którego wywołanie zwrotne zwróci true, albo -1 gdy nie ma takiego elementu. Metoda Array.prototype.indexOf() używa do porównania operatora ===, podczas gdy tu mamy możliwość użycia dowolnego komparatora. Parametry wywołania zwrotnego to element, index, array.

[12, 5, 8, 130, 44].findIndex( function( element ) { return element >= 15; }); // 3
Array.prototype.entries()
Array.prototype.values()
Array.prototype.keys()
var a = [3,4,5];
[...a.values()];           // [3,4,5]
[...a.keys()];             // [0,1,2]
[...a.entries()];          // [[0,3],[1,4],[2,5]]
[...a[Symbol.iterator]()]; // [3,4,5]

3.5.2. Object

Object.getOwnPropertySymbols(obj)
Zwraca nazwy wszystkich specjalnych (meta) właściwości obiektów.
Object.setPrototypeOf(obj, prototype)
Ustanawia połączenie [[Prototype]] między obiektami.
Object.assign(target, ...sources)
Kopiuje wszystkie własne (czyli nie odziedziczone) wyliczalne własności (zarówno łańcuchowe jak symboliczne) z obiektów sources do obiektu target, zwraca obiekt docelowy. Nie są zachowywane deskryptory kopiowanych właściwości.

3.5.3. Math

Trygonometria
Math.cosh(x) Cosinus hiperboliczny
Math.acosh(x) Arcus cosinus hiperboliczny
Math.sinh(x) Sinus hiperboliczny
Math.asinh(x) Arcus sinus hiperboliczny
Math.tanh(x) Tangens hiperboliczny
Math.atanh(x) Arcus tangens hiperboliczny
Math.hypot([value1[, value2[, ...]]])) Pierwiastek kwadratowy sumy kwadratów (np. uogólnione twierdzenie Pitagorasa)
Funkcje arytmetyczne
Math.cbrt(x) Pierwiastek trzeciego stopnia
Math.clz32(x) Zlicza początkowe zera w 32-bitowej reprezentacji liczby
Math.expm1(x) To samo co Math.exp(x)-1
Math.log2(x) Logarytm o podstawie 2
Math.log10(x) Logarytm o podstawie 10
Math.log1p(x) To samo co Math.log(1+x)
Math.imul(a, b) 32-bitowe całkowite mnożenie dwóch liczb
Funkcje dodatkowe
Math.sign(x) Znak liczby (-1, 0, lub 1), albo NaN jeśli argument nie jest liczbą.
Math.trunc(x) Część całkowita liczby, powstała przez usunięcie wszystkich cyfr po przecinku.
Math.fround(x) Zaokrągla do najbliższej 32-bitowej liczby zmiennoprzecinkowej (czyli liczby o pojedynczej precyzji)

3.5.4. Number

Number.EPSILON
Minimalna wartość między dwiema liczbami zmiennoprzecinkowymi: 2^-52 (tolerancja dla niedokładności operacji arytmetycznych na liczbach zmiennoprzecinkowych).
Number.MAX_SAFE_INTEGER
Najwyższa wartość całkowita, którą można „bezpiecznie” wyrazić jako wartość liczbową w języku JavaScript: 2^53-1. „Bezpiecznie” oznacza dokładną reprezentację liczby całkowitej i możliwość porównywania, np.

Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 // true
Number.MIN_SAFE_INTEGER
Najmniejsza wartość całkowita, którą można „bezpiecznie” wyrazić jako wartość liczbową w języku JavaScript: (-2)^53+1.
Number.isNaN(value)
Czy podana wartość to NaN. Dla porównania, globalna funkcja isNaN() zwraca true również dla argumentów nie będących liczbami.
Number.isFinite(value)
Czy podana wartość to liczba skończona (czyli nie Infinity i nie NaN). W odróżnieniu od globalnej funkcji isFinite() nie próbuje konwertować argumentu na liczbę, czyli dla każdej wartości innej niż liczba zwraca false.
Number.isInteger(value)
Czy podana wartość to liczba całkowita. Nie przeprowadza żadnych konwersji, więc zwraca false dla argumentu nie będącego liczbą, ponadto zwraca false dla wartości Infinity i NaN.
Number.isSafeInteger(value)
Czy podana wartość to liczba całkowita mieszcząca się w przedziale Number.MIN_SAFE_INTEGER..Number.MAX_SAFE_INTEGER. Nie przeprowadza żadnych konwersji argumentu.
Number.parseInt(string[, radix])
Alias do globalnej funkcji parseInt()
Number.parseFloat(string)
Alias do globalnej funkcji parseFloat()

3.5.5. String

String.prototype.repeat(count)
Konstruuje nowy łańcuch, powtarzając wskazany łańcuch count razy.

"foo".repeat( 3 ); // "foofoofoo"
String.prototype.startsWith(searchString[, position])
String.prototype.endsWith(searchString[, position])
String.prototype.includes(searchString[, position])
Sprawdza, czy podany searchString występuje odpowiednio na początku, końcu lub gdziekolwiek wewnątrz łańcucha.

Metody String.fromCodePoint(), String.codePointAt() i String.normalize() zostały omówione w rozdziale Składnia/Unicode.

3.6. Metaprogramowanie

3.6.1. Obiekty pośredniczące – Proxy

Obiekt pośredniczący (Proxy) to specjalny, tworzony przez nas obiekt, który „opakowuje” – bądź też przesłania – inny, normalny obiekt. W takim obiekcie pośredniczącym można rejestrować tzw. pułapki (ang. trap) – czyli specjalne metody obsługi, wywoływane kiedy na obiekcie pośredniczącym zostaną wykonane konkretne operacje. Takie metody obsługi mają możliwość wykonania dodatkowej logiki, lecz przede wszystkim przekazują wykonanie operacji do opakowanego, docelowego obiektu.

Przykład użycia proxy to np. przechwytywanie wywołania nieistniejącej metody w obiekcie docelowym.

var obj = { a: 1 };
var var pobj = new Proxy( obj, {
  get( target, key, context ) {
    // target === obj, context === pobj
    console.log( "Odwołanie do: ", key );
    return Reflect.get( target, key, context );
  }
});

Każdej metodzie-pułapce obiektu pośredniczącego odpowiada funkcja obiektu Reflect o tej samej nazwie. Domyślna definicja każdej metody-pułapki wywołuje odpowiednią funkcję Reflect.

get(target, property, receiver)
set(target, property, value, receiver)
deleteProperty(target, property)
apply(target, thisArg, argumentsList)
construct(target, argumentsList, newTarget)
getOwnPropertyDescriptor(target, propName)
defineProperty(target, property, descriptor)
getPrototypeOf(target)
setPrototypeOf(target, prototype)
preventExtensions(target)
isExtensible(target)
ownKeys(target)
enumerate(target, key)
has(target, propName)

Nie wszystkie operacje na obiekcie są przechwytywalne. Póki co nie da się przechwycić:

typeof obj;
String( obj );
obj + "";
obj == pobj;
obj === pobj;

Tzw. revocable proxy to obiekt pośredniczący z możliwością unieważnienia. Po unieważnieniu wszelkie próby wywołania którejkolwiek funkcji obsługi takiego proxy spowodują zgłoszenie wyjątku TypeError. Przykładem możliwości zastosowania obiektów pośredniczących tego typu jest przekazywanie do innych miejsc aplikacji zarządzających danymi modelu pośredniczącego, a nie bezpośredniej referencji do obiektu modelu. W takim przypadku, jeśli w przyszłości obiekt modelu zostanie zmieniony, będziemy mogli unieważnić obiekt pośredniczący, dzięki czemu pozostałe fragmenty aplikacji będą wiedzieć (ze względu na wystąpienie błędów), że powinny zaktualizować używane referencje do modelu.

{ proxy: pobj, revoke: prevoke } = Proxy.revocable( obj, handlers );
pobj.a;
prevoke();
pobj.a; // TypeError

3.6.2. Interfejs API obiektu Reflect

Metaprogramowe możliwości obiektu Reflect oddają w nasze ręce programowe odpowiedniki wielu konstrukcji syntaktycznych, zapewniając tym samym dostęp do wcześniej ukrytych, abstrakcyjnych operacji. Na przykład można ich użyć do rozszerzania możliwości oraz tworzenia interfejsów API tzw. języków dziedzinowych (ang. domain specific languages, DSL).

3.7. Dalszy rozwój języka po ES6

3.7.1. SIMD

API SIMD udostępnia różne instrukcje niskiego poziomu (procesora), pozwalające na jednoczesne wykonanie operacji na więcej niż jednej wartości liczbowej. Na przykład będziemy mogli podać dwa wektory składające się z 4 lub 8 liczb i jednocześnie pomnożyć odpowiadające sobie elementy tych wektorów.

var v1 = SIMD.float32x4( 3.14159, 21.0, 32.3, 55.55 );
var v2 = SIMD.float32x4( 2.1, 3.2, 4.3, 5.4 );

SIMD.float32x4.mul( v1, v2 );

3.7.2. WebAssembly

Jakiś czas temu Brendan Eich, twórca JavaScriptu, przedstawił ważne oświadczenie, które może mieć znaczący wpływ na rozwój języka JavaScript.

WebAssembly (WASM) udostępnia ścieżkę, dzięki której kod nadający się do wykonania w środowisku przeglądarki WWW będzie mógł być tworzony w innych językach programowania, bez konieczności przekształcania go na JavaScript. Najprościej rzecz ujmując, jeśli pomysł WASM wypali, to silniki JavaScript używane w przeglądarkach zyskają dodatkową możliwość wykonywania kodu w formacie binarnym, który można sobie wyobrazić jako coś podobnego do bajtkodu (wykonywanego przez JVM).

WASM proponuje użycie formatu binarnej reprezentacji drzewa składni (AST), która umożliwi przekazywanie instrukcji bezpośrednio do silnika JS oraz używanych przez niego mechanizmów, bez konieczności parsowania. Co więcej, taki kod nie musiałby się nawet stosować do reguł języka JavaScript. Kody pisane w takich językach jak C lub C++ mogłyby być kompilowane bezpośrednio do formatu WASM, zyskując tym samym przewagę szybkości. Najbliższym, krótkoterminowym celem WASM jest zapewnienie, by pod względem możliwości odpowiadał on samemu językowi JavaScript. Oczekuje się jednak, że kiedyś możliwości WASM przekroczą wszystko, co obecnie może język JS. Na przykład naciski, by wprowadzić do języka JavaScript radykalne możliwości takie jak wątki – a bez wątpienia byłaby to zmiana, która wstrząsnęłaby ekosystemem tego języka – mają znacznie większe szanse na realizację jako rozszerzenia WASM, zmniejszając tym samym presję na wprowadzenie ich w samym JS. W rzeczywistości jednak te nowe plany sprawiają, że pojawiają się możliwości, by programy działające w środowisku internetowym można było pisać także w innych językach programowania. To naprawdę ekscytująca nowa droga rozwoju dla całej platformy internetowej.

Choć wydaje się, że JS nie będzie motorem napędowym WASM, to jednak kod JS oraz kod WASM będą mogły ze sobą współpracować na wiele sposobów i to tak naturalnie, jak dziś wyglądają interakcje między modułami.

To, co dziś piszemy z użyciem języka JavaScript, zapewne wciąż będziemy w nim pisać, przynajmniej w przewidywalnej przyszłości. Z kolei kod, który obecnie jest transpilowany do kodu JavaScript (np. języki CoffeeScript, TypeScript, Dart), będzie zapewne wykonywany przy użyciu WASM. Jeśli chodzi o rozwiązania wymagające największej wydajności i odrzucenia wielu warstw abstrakcji, najprawdopodobniej będą one pisane w języku innym niż JavaScript, a następnie przygotowywane do wykonywania przy użyciu WASM.

Na razie projekt WASM udostępnił wczesną wersję biblioteki, która demonstruje jego podstawowe możliwości i stanowi potwierdzenie ich praktycznej przydatności.

Jedno natomiast nie budzi wątpliwości: im bardziej realne będzie się stawać WASM, tym większy będzie wpływ tej technologii na kierunek rozwoju oraz projekt języka JavaScript. To przypuszczalnie największa spośród wszystkich możliwości związanych z „przyszłymi wersjami języka JavaScript”, na które programiści używający tego języka powinni zwracać uwagę.

WASM ma obecnie status „in development” w przeglądarkach MSEdge, Chrome, Firefox i WebKit (a więc też np. Safari).

Share and Enjoy:
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Śledzik
  • Blip
  • Blogger.com
  • Gadu-Gadu Live
  • LinkedIn
  • MySpace
  • Wykop