Co nowego w ECMAScript 2016 i 2017?

Cały czas pojawiają się artykuły o nowościach ECMAScript 6, lecz mamy już rok 2017 i czas przyjrzeć się bliżej dwóm kolejnym wersjom standardu ECMAScript tj. wersji 2016 i 2017. Dzisiaj omówimy kilka wybranych elementów wprowadzonych w najnowszych wersjach JavaScript.

Na początek weźmiemy pod lupę standard ECMAScript 2016, określany także jako ES7 (choć ja w dalszej części artykułu będę raczej operował nazwami zawierającymi rok publikacji).

Nowości w ECMAScript 2016 i 2017

Wprowadzono tutaj w zasadzie dwa nowe elementy, tj. metodę Array.prototype.includes oraz nowy operator potęgowania. Teoretycznie zmiany te nie są istotne, gdyż metodę includes() łatwo można zastąpić polyfill np. z użyciem indexOf. Podobnie wygląda sprawa z operatorem potęgowania, który de facto może zastępować metodę Math.pow().

Dlaczego więc zespół standaryzujący zdecydował się opublikować tak małą liczbę zmian w formie oddzielnej „edycji” ? Otóż wynika to z faktu, że wraz z pojawieniem się ECMAScript 2015 (ES6) komisja zapowiedziała, że co roku będzie wychodzić kolejna wersja ES. Jak dotąd umowy dotrzymali i stąd mamy ES2016 oraz ES2017 (który wprowadza już nieco więcej zmian).

W artykule omówimy tylko kilka wybranych nowości. Nie będziemy dzisiaj zajmować się tematyką async/await czy shared memory gdyż są to zagadnienia na oddzielne wpisy.

Nowy operator potęgowania

Operator exponentiation (potęgowania) pozwala zastąpić standardowe do tej pory wywołanie metody Math.pow().

5 ** 2;           //25
Math.pow( 5, 2 ); //25

const n1 = 6 ** 3;           //216
const n2 = Math.pow( 6, 3 ); //216

let x = 5;
x **= 2; //25

W większości przypadków prawdopodobnie nie będzie to często używany operator i znajdzie zastosowanie głównie w algorytmach obliczeniowych. Warto jednak stosować go rozsądnie i z dużą ostrożnością jeśli wykonujemy wiele operacji zarówno potęgowania jak i mnożenia. W takich wypadkach nietrudno o pomyłkę i przypadkowe użycie pojedynczego symbolu „*” co zupełnie zmieni zachowanie się algorytmu.

Metoda Array.prototype.includes

Drugą i zarazem ostatnią zmianą wprowadzoną w ES2016 jest nowa metoda Array.prototype.includes. Istnieje także metoda String.prototype.includes jednakże została ona wprowadzona już wcześniej wraz z pojawieniem się ES6 (ECMAScript 2015) także nie będziemy jej dzisiaj omawiać.

Metoda Array.prototype.includes jest czasami porównywana z metodą indexOf jednakże nie do końca można na nie patrzeć jako na metody alternatywne.

Przede wszystkim metoda indexOf zwraca wartość liczbową, w tym „-1” w przypadku braku dopasowania do jakiegokolwiek elementu tablicy. Nowa metoda includes zawsze zwraca wartość Boolean (true/false).

Zobaczmy jak działa metoda includes w praktyce:

const arr = ['a', 'b', 'c', 'd']; 

arr.includes( 'b' ); //true
arr.includes( 5 );   //false

arr.indexOf( 'b' ); //1
arr.indexOf( 5 );   //-1

Obie metody przyjmują również drugi argument (opcjonalny) określający miejsce, od którego należy rozpocząć szukanie wskazanego elementu (w formie indeksu, numerowanego od zera):

arr.indexOf( 'b', 1 );  //1
arr.includes( 'b', 1 ); //true

arr.indexOf( 'b', 2 );  //-1
arr.includes( 'b', 2 ); //false

arr.slice( 2 ); //["c", "d"]

Indeks początku wyszukiwania można również określić jako liczbę ujemną co oznacza określenie indeksu licząc od końca tablicy.

Metoda includes vs indexOf

Osobiście zalecam stosowanie raczej metody includes, szczególnie jeśli chcemy od wyniku wyszukiwania uzależnić wykonanie instrukcji warunkowej. W przypadku metody indexOf musimy zawsze pamiętać o ryzykownej sytuacji gdy element zostanie dopasowany na indeksie zerowym, gdyż zero w JavaScript jest niejawnie konwertowane na false.

const arr = ['a', 'b', 'c', 'd'];

if ( arr.indexOf( 'a' ) ) {
    //nigdy tu nie dotrzemy, mimo, że
    //tablica arr zawiera element 'a'
}

Problem ten można rozwiązać na kilka sposobów:

if ( ~arr.indexOf( 'a' ) ) {
    console.log( 'ok' );
}
//'ok'

if ( arr.indexOf( 'a' ) !== -1 ) {
    console.log( 'ok' );
}
//'ok'

if ( arr.includes( 'a' ) ) {
    console.log( 'ok' );
}
//'ok'

Użycie w tym wypadku metody indexOf wymaga zastosowania operatora bitowego NOT lub przyrównania wyniku do wartości „-1”. Moim zdaniem ostatni sposób z użyciem Array.prototype.includes jest najbardziej czytelny i od razu jasno wskazuje intencje programisty. Mając pewność, że metoda zawsze zwraca wartość true jeśli element znajduje się w tablicy nie musimy martwić się o problem niejawnej konwersji do typu Boolean.

I jeszcze jedna mała ciekawostka, która również utwierdzi nas w przekonaniu, że metody includes nie można uważać za bezpośrednią alternatywę dla indexOf:

[NaN].indexOf( NaN );  //-1 ups...

[NaN].includes( NaN ); //true

W większości poradników o JavaScript stosowana jest głównie metoda indexOf. Obecnie dysponujemy jednak znacznie lepszymi metodami do analizy zawartości tablic, które dokładnie wskazują na cel ich użycia:

  • Array.prototype.includes – czy element znajduje się w tablicy?
  • Array.prototype.find – jeśli element znajduje się w tablicy to zwróć jego wartość,
  • Array.prototype.findIndex – jeśli element znajduje się w tablicy to zwróć jego indeks.

Zalecam bliższe zapoznanie się z Array.prototype, gdzie znajduje się wiele na prawdę przydatnych metod, a w razie potrzeby zawsze możemy skorzystać z pętli for-of iterującej po wartościach.

Zaczynamy zabawę z ES2017 – Object.entries oraz Object.values

Metody te omawiałem jakiś czas temu dokładniej w artykule Jak dobrać się do obiektu, dlatego osoby zainteresowane tym zagadnieniem odsyłam do mojego wcześniejszego artykułu.

Metoda Object.values zwraca tablicę zawierającą wyłącznie same wartości właściwości obiektu. Elementy są indeksowane jak zwykłe tablice, czyli numerycznie licząc od zera.

const person = {
    name: 'Tomek',
    age: 31
}

Object.values( person ); //["Tomek", 31]

Z kolei metoda Object.entries zwraca tablicę indeksowaną numerycznie (od zera),  zawierającą kolejne tablice, w których przechowywane są nazwa właściwości i jej wartość (jako oddzielne elementy tablicy).

const person = {
    name: 'Tomek',
    age: 31
}

Object.entries( person ); //[ ["name", "Tomek"], ["age", 31] ]

Metody padStart oraz padEnd

Kolejne metody wprowadzone w ES2017 to String.prototype.padStart oraz String.prototype.padEnd. Ich zadaniem jest dodanie wskazanego testu na początku lub na końcu ciągu znakowego z dopełnieniem do określonej liczby znaków. Jako pierwszy argument metody przyjmują docelową długość ciągu znakowego, a jako drugi element wstawiany od początku lub od końca ciągu.

Spójrzmy na prosty przykład działania obu metod:

'x'.padStart( 4, '+' ); //'+++x'
'x'.padEnd( 4, '+' );   //'x+++'

Jeśli docelowa długość ciągu wskazana w metodach padStart lub padEnd będzie mniejsza niż obecna jego długość to metody po prostu zwrócą istniejący ciąg bez wprowadzania zmian:

'xxx'.padStart( 2, '+' ); //"xxx"
'xxx'.padEnd( 2, '+' );   //"xxx"

No dobrze… fajne metody ale czy są one w ogóle przydatne w praktyce? Myślę, że znajdzie się kilka operacji gdzie ich użycie będzie wygodne i zaoszczędzi kilku linijek kodu czy pętli. Nie zawsze jednak będzie to dobre i bezpieczne rozwiązanie.

Załóżmy na przykład, że mamy tablicę z trzycyfrowymi numerami użytkownika i chcemy do każdego ciągu dodać ciąg „user_”. Zobaczmy jak zrobić to z użyciem nowej metody padStart:

const usersID = ['001', '002', '003'];

const users = usersID.map( id => id.padStart( 7, 'user_' ) );
users; //["user_001", "user_002", "user_003"]

//lub "tradycyjnie":
const users1 = usersID.map( id => `user_${id}` );
users1; //["user_001", "user_002", "user_003"]

Który wariant jest w tym wypadku lepszy? Teoretycznie można powiedzieć, że metoda padStart jasno wskazuje, że chcemy dodać „coś” na początku ciągu. Problem jednak w tym, że musimy precyzyjnie określić docelową długość nowego ciągu. Zobaczmy co się stanie w obu sytuacjach, jeśli nagle numery ID zmienią się z trzycyfrowych na czterocyfrowe:

const usersID = ['0001', '0002', '0003'];
const users1 = usersID.map( id => id.padStart( 8, 'user_' ) );
users1; //["user0001", "user0002", "user0003"] BRAK "_"

//drugi wariant
const users2 = usersID.map( id => `user_${id}` ); //nic nie zmieniamy...
users2; //["user_001", "user_002", "user_003"]    //wynik poprawny

Nie twierdzę, że metody padStart czy padEnd są złe, lecz przestrzegam przed zbyt pochopnym ich stosowaniem „bo są nowe” (co niestety często widać gdy w JS pojawiają się jakieś nowe elementy, metody itp.).

Analiza deskryptorów metodą getOwnPropertyDescriptors

Wprowadzono również nową metodę do obiektu globalnego Object, która umożliwia pobranie wszystkich właściwości obiektu wraz z ich deskryptorami oraz akcesorami dostępu (settery i gettery). Metoda może mieć zastosowanie na przykład do wykonania pełnej kopii obiektu co za chwilę pokażemy krok po kroku. Na początku zdefiniujmy obiekt person wraz z określeniem przykładowych właściwości i ich deskryptorów:

const person = {};

Object.defineProperties(
    person,
    {
        name: {
            value: 'Tomek',
            writable: false,
            enumerable: true,
            configurable: false
        },
        city: {
            value: 'Poznan',
            writable: true,
            enumerable: true,
            configurable: true
        }
    }
);

Teraz spróbujmy podejrzeć zawartość obiektu person:

Object.getOwnPropertyDescriptors( person );

//zawartość obiektu person:
{
    name: {
        value: "Tomek", 
        writable: false, 
        enumerable: true, 
        configurable: false },
    city: {
        value: "Poznan", 
        writable: true, 
        enumerable: true, 
        configurable: true }
}

A teraz spróbujmy sklonować obiekt person z użyciem metody Object.assign:

const personA = {};
Object.assign( personA, person );
Object.getOwnPropertyDescriptors( personA );

//zawartość obiektu personA:
{
    name: {
        value: "Tomek", 
        writable: true,      //ups zmiana
        enumerable: true, 
        configurable: true}, //ups zmiana
    city: {
        value: "Poznan", 
        writable: true, 
        enumerable: true, 
        configurable: true }
}

Widzimy więc, że dwa deskryptory dla właściwości name zostały zmienione podczas procesu klonowania obiektu na wartości domyślne, czyli true. Jeśli nie korzystamy z jawnego wskazywania wartości dla deskryptorów to w zasadzie metoda Object.assign jest wystarczająca gdyż i tak wszystkie deskryptory są ustawiane domyślnie na true. Problem jednak pojawia się, gdy np. wybrane metody chcemy dodatkowo zabezpieczyć właśnie przy użyciu wspomnianych deskryptorów.

W tym momencie z pomocą przychodzi metoda getOwnPropertyDescriptors.  Możemy ją wykorzystać wraz z metodą Object.create aby stworzyć pełną kopię obiektu person wraz z przeniesieniem wszystkich deskryptorów właściwości (a nawet wraz z akcesorami dostępu set i get):

const personB = Object.create(
    person,
    Object.getOwnPropertyDescriptors( person )
);
Object.getOwnPropertyDescriptors( personB );

//zawartość obiektu personB:
{
    name: {
        value: "Tomek", 
        writable: false,       //teraz ok
        enumerable: true, 
        configurable: false},  //teraz ok
    city: {
        value: "Poznan", 
        writable: true, 
        enumerable: true, 
        configurable: true }
}

Bardziej szczegółowo metodę getOwnPropertyDescriptors postaram się omówić w niedalekiej przyszłości gdy poruszę tematykę deskryptorów właściwości obiektów oraz akcesorów dostępu.

Przecinki kończące deklarację funkcji i obiektów

Standard ES2017 wprowadza również pewną zmianę składni, a dokładniej zmienia zachowanie się błędów składniowych w przypadku wymieniania po przecinku kolejnych argumentów funkcji lub właściwości obiektu.

Do tej pory definiując argumenty funkcji należało je oddzielać przecinkami, lecz za ostatnim argumentem przecinek nie mógł występować. Obecnie jest to dopuszczalne, zatem poniższa deklaracja nie spowoduje zgłoszenia wyjątku:

const fn = (a, b, c,) => { //przecinek za argumentem 'c'
    return [a, b, c]; 
}

fn( 1, 2, 3 );  //[1, 2, 3]
fn( 1, 2, 3, ); //[1, 2, 3]  //przecinek za '3'

Widać więc, że końcowe przecinki są dopuszczalne zarówno przy deklaracji funkcji jak i w momencie jej wywołania.

Ponad to tę samą zasadę przyjęto dla definiowania właściwości obiektów, gdzie również za ostatnią właściwością może znajdować się przecinek:

const person = {
    name: 'Tomek',
    age: 31,
    city: 'Poznan',  //przecinek, wcześniej niedopuszczalny
}

person; //{name: "Tomek", age: 31, city: "Poznan"}

O ile w przypadku definiowania bądź wywołania funkcji uważam, że lepiej nie stosować końcowych przecinków, które mogą wprowadzać niejasność (przecinek celowy czy może zapomniałem czegoś dodać…?), o tyle w definiowaniu właściwości obiektów może to być przydatne. Pozwoli bowiem na bezpieczne dodawanie i usuwanie właściwości bez ryzyka, że otrzymamy błąd składniowy wynikający z obecności przecinka za ostatnią właściwością (która wcześniej była np. przedostatnią, ale z jakich powodów zdecydowaliśmy się zmniejszyć ilość właściwości w obiekcie).

Jeśli korzystamy z transpilatora babel to możemy bezpiecznie korzystać z dogodności jaką daje „trailing commas” gdyż babel sam i tak usunie ewentualny przecinek za ostatnią właściwością oraz w definiowaniu i wywołaniu funkcji.

Podsumowanie

Jak widać dwa nowe standardy teoretycznie nie wprowadzają tak wielu zmian jak ES6 ale może to i lepiej, gdyż zawsze łatwiej przyswoić sobie mniejszą porcję nowości 🙂

Nie omówiliśmy jednak jeszcze jednej z najważniejszych zmian, a w zasadzie rozszerzeń składniowych, jakie wprowadza ES2017, tj. składni async function, które mogą być definiowane również jako arrow function czy jako właściwości obiektów. Tematem tym zajmiemy się niedługo w oddzielnym wpisie poświęconym właśnie zagadnieniom asynhroniczności, Promise, async/await…

  • w ES2017 weszło trochę ciekawych zmian, ja na razie opisałem w zasadzie te najmniej znaczące, ale tematem async/await planuję zająć się oddzielnie. Aktualnie pracuję nad jednym małym CRM gdzie wdrażam właśnie stosowanie async/await w node (wersja 8) więc niedługo postaram się coś o tym naskrobać 🙂

    Co do przecinków to dzięki za info, szczerze mówiąc nie pomyślałem o tym 🙂

  • Jak widać nie czeka nas taka rewolucja jak w przypadku ES6, całe szczęście – ciężko byłoby co dwa lata opanować takie ilości zmian i nowych metod.

    Dzięki za sprawne umówienie nowości, dobrze dobrane przykłady to klucz do sukcesu :).

    Co do wiodących przecinków w ES2017 – airbnb w swoim style guide (https://github.com/airbnb/javascript#commas) zachęca do ich argumentując lepsze działanie komendy git diff.