W ostatnim już artykule z serii dotyczącej nowości w ECMAScript 6 przyjrzymy się bliżej pojęciom Rest operator oraz Spread properties i nowym możliwościom, które dzięki nim zyskujemy.

Rest operator

Zacznijmy, jak lubię, od przykładu i powoli wyjaśnimy sobie, co się w nim dzieje:

function f(a, b, ...x) {
  return { a, b, x }
}

const result = f('to będzie a', 'to będzie b', 'zas', 'pozostałe', 'wpadną', 'do', 'x');

W pierwszej linijce, jako prefiks ostatniego argumentu w deklaracji funkcji zastosowaliśmy trzy kropki ..., a więc tak zwany "Rest operator". Zbiera on do tablicy o podanej nazwie wszystkie pozostałe przekazane do funkcji argumenty, które nie zostały wymienione przed nim. W tym przypadku wymienione zostały jedynie a i b, a więc wszystkie pozostałe parametry, które do funkcji przekażemy zebrane zostaną w tablicę x. Innymi słowy result będzie miał taką zawartość:

{
  'a': 'to będzie a',
  'b': 'to będzie b',
  'x': [
    'zaś',
    'pozostałe',
    'wpadną',
    'do',
    'x'
  ]
}

No fajnie, możemy zapomnieć o obiekcie arguments i jego ograniczeniach, ale do czego można to praktycznie wykorzystać? Pytanie jak najbardziej zasadne, bo zastosowanie nie jest kompletnie oczywiste, a zazwyczaj wiąże się z nieco bardziej zaawansowanymi tematami. Osobiście pierwszy raz skorzystałem z "operatora reszty" (tak to się tłumaczy?) przy tak zwanej aplikacji częściowej. Wkrótce napiszę cały artykuł poświęcony temu tematowi, a na razie niech wystarczy ten prosty przykład:

const greet = (greeting, name) => `${greeting} ${name}`;

Na początek prosta funkcja witająca osobę zadanym powitaniem i imieniem. Póki co nic trudnego. Ale załóżmy, że wykorzystujemy ją na tyle często jako pierwszy argument podając "Hi", że postanowiliśmy uczynić z tego przypadku osobną funkcję, gdzie podajemy tylko imię, a powitanie przez "Hi" mamy już z głowy.

const sayHiTo = (name, ...args) => greet('Hi', name, ...args);

sayHiTo("Józek"); // Hi Józek

No dobra, ale po co tutaj ten rest? Przecież nawet nigdzie nie wykorzystujemy zmiennej args. To prawda, nie wykorzystujemy... ale moglibyśmy :-). Tak naprawdę chodzi o to, że nasza funkcja sayHiTo jest teraz uniwersalna i jeśli zmienimy liczbę parametrów przyjmowanych przez funkcję greet, to automatycznie będą one obsługiwane także przez funkcję sayHiTo.

Przykład? Zmieniamy funkcję greet tak, by przyjmowała jeszcze jeden argument:

const greet = (greeting, name, bonus) => `${greeting} ${name}, ${bonus}`;

I teraz bez zmiany czegokolwiek w funkcji sayHiTo, zmieniamy jej wywołanie na:

sayHiTo("Józek", "ty byku!");

I otrzymujemy bardziej swojskie "Hi Józek, ty byku!"

Jeśli ten przykład wydaje Ci się nieco wydumany, albo nie rozumiesz, po co tworzyć tego typu funkcje używające już istniejących tylko z pre-uzupełnionym parametrem, proszę uzbrój się w cierpliwość - już wkrótce opublikuję artykuł o programowaniu funkcyjnym w JavaScript, gdzie postaram się lepiej to wyjaśnić. A jeśli nie chcesz czekać, zapraszam do poczytania tutaj (po angielsku).

Spread properties

Spread również zapisywany jest w postaci trzech kropek ..., lecz nie jako przedrostek do ostatniego argumentu w deklaracji funkcji, a jako przedrostek do jakiejkolwiek tablicy. Jego zadaniem jest "rozbić" tablicę na listę jej elementów. Brzmi to banalnie ale ma to olbrzymią liczbę zastosowań, z których kilka przedstawiam poniżej.

Łączenie tablic

const arr1 = ['trzy', 'cztery'];
const arr2 = ['jeden', 'dwa', ...arr1, 'pięć'];

console.log(arr2); // ['jeden', 'dwa', 'trzy', 'cztery', 'pięć']

Kopiowanie tablic

const arr1 = ['jeden', 'dwa'];
const arr2 = [...arr1];

console.log(arr2); // ['jeden', 'dwa']

Tutaj słowo wyjaśnienia. Ktoś mógłby spytać, dlaczego nie napisać po prostu const arr2 = arr1. Ano dlatego, że w ten sposób do arr2 przypisujemy jedynie referencję, a więc jakiekolwiek zmiany w arr1 będą także widoczne w arr2, gdyż będą one po prostu wskazywać na to samo miejsce w pamięci.

Trzeba jednak pamiętać, że taka kopia utworzona przez spread jest jedynie jedno-poziomowa. To znaczy, że jeśli w w arr1 znajdują się jakiekolwiek obiekty, czy tablice, zostaną one skopiowane przez referencje, a nie przez wartość. Aby zrobić klonowanie tablicy upewniając się, że kopiowane są wartości, nie zaś referencje niezależnie od ilości poziomów zagnieżdżenia, należałoby zastosować rekurencję, bądź jedną z dostępnych bibliotek, które takie funkcje już posiadają, jak na przykład lodash.

Usuwanie duplikatów z tablic

const arr = [7, 3, 1, 3, 3, 7];
const noDupes = [...new Set(arr)];
console.log(noDupes); // [ 7, 3, 1 ]

Wykorzystujemy tutaj właściwość nowej struktury danych udostępnionej w ES6, zwanej Set. Jest to struktura przechowująca elementy, które nie mogą się powtarzać. Set możemy utworzyć poprzez podanie mu jako argument tablicy. Następnie taki Set za pomocą spread zamieniamy na listę elementów, które umieszczamy w tablicy. Wszystko w jednej linijce. Czyż to nie piękne? :-)

Zamiana NodeList na tablicę

[...document.querySelectorAll('div')]

To zastosowanie bywa szczególnie przydatne, gdy chcemy wykonać w pętli operacje na wszystkich elementach wybranych przez querySelectorAll, gdyż funkcja ta nie zwraca tablicy, a właśnie obiekt typu NodeList, na którym zwykły forEach nie zadziała (przynajmniej nie we wszystkich przeglądarkach).