Wraz z pojawieniem się JavaScript ES6 (ECMAScript 6), światło dzienne ujrzały nowe rozwiązania, które w wielu przypadkach pozwalają nam na szybsze pisanie kodu. Jednym z takich rozwiązań jest „spread operator”, którego składnia jest niczym innym jak trzema kropkami (…).

Co na ten temat mówi strona developer.mozilla.org?
Składnia rozwinięcia (ang. spread syntax) pozwala na rozwinięcie iterowalnego wyrażenia, takiego jak wyrażenie tablicowe lub ciąg znaków, tam gdzie oczekiwanych jest zero lub więcej argumentów (dla wywołań funkcji) lub elementów (dla literałów tablicowych). Pozwala również na rozwinięcie wyrażeń obiektowych w miejscach, gdzie oczekiwanych jest zero lub więcej par klucz-wartość (dla literałów obiektowych).
Źródło: https://developer.mozilla.org/pl/docs/Web/JavaScript/Referencje/Operatory/Spread_operator)

W związku z tym, że dla mnie definicja ta nie jest zbyt jasna, postaram się przejść przez szereg przykładów, które pomogą zrozumieć, gdzie możemy wykorzystać spred operator, czyli krótko mówiąc – trzy kropki… 🙂

Składnia

Najprościej mówiąc, będą to trzy kropki, które będziemy mogli używać, między innymi:

Podczas wywołania funkcji:

myFunction(...parameters)

Wykonując operacje przy wykorzystaniu tablic:

[...object, 'A', 'B', 'C']

Wykonując operacje przy wykorzystaniu obiektów:

{ ...object };

oraz w kilku innych przypadkach, o czym za chwilę…

Funkcje

Jeżeli mamy do czynienia z funkcją, która przyjmuje dowolną ilość parametrów, dzięki wykorzystaniu spread operatora w łatwy sposób przekażemy do funkcji tablicę parametrów. Doskonałym przykładem takiej wbudowanej funkcji będzie Math.min() lub Math.max()

Przykład:

// poniższy kod bez użycia spread operatora nie działa...
let params = [1,2,3];
Math.max(params); 
// Output: NaN

// kod z użyciem spread operatora (...) 
let params_spread = [1,2,3];
Math.max(...params_spread); 
// Output: 3

Bez użycia spread operatora również moglibyśmy przekazać tablicę, jednak w tym celu należałoby użyć .apply()

let params_without_spread = [1,2,3];
Math.max.apply(null, params_spread); 
// Output: 3

Tablice

Najczęściej spotykane użycie spread operatora występuje podczas łączenia tablic lub kopiowania tablic.

Przykład łączenia tablic:

let color_dark = ['black', 'grey'];
let color_light = ['yellow', 'pink'];
let colous = [...color_dark, ...color_light];

console.log(colors);
// Output: [ 'black', 'grey', 'yellow', 'pink' ]

Podczas użycia spread operatora możemy łączyć wartości tablicy z innymi elementami podawanymi jawnie wewnątrz nowo tworzonej tablicy:

let color_list = ['black', 'grey'];
let colors = ['orange', ...color_list, 'yellow', ...color_list, 'blue'];

console.log(colors);
// Output: [ 'orange', 'black', 'grey', 'yellow', 'black', 'grey', 'blue' ]

Przykład kopiowania zawartości tablicy:

let values = [1,2,3];
let new_values = [...values];
console.log(values);
// Output: [1,2,3]
console.log(new_values);
// Output: [1,2,3]

Dlaczego kopiowanie tablic jest tak ważne? Przede wszystkim w JavaScript działania na tablicach odbywają się przez referencje. Jest to też istotne w przypadku modyfikacji danych w tablicy, czy przestrzegania zasad programowania funkcyjnego.

W przypadku tablicy, proste przypisanie istniejącej tablicy do nowej zmiennej nie stworzy nowej tablicy, a jedynie przekaże referencje do istniejącej tablicy, co pokazuje poniższy przykład. Sprawdźmy wywołanie metody .push().

let values = [1,2,3];
let new_values = values;

console.log(values); // Output: [1,2,3]
console.log(new_values); // Output: [1,2,3]

values.push(4);

console.log(values); // Output: [1,2,3,4]
console.log(new_values); // Output: [1,2,3,4]

Brak użycia spread operatora podczas działania na tablicach:
Warto przy tej okazji sprawdzić, co by się stało, gdybyśmy nie użyli spread operatora. W takim przypadku otrzymamy tablicę z dwoma elementami, gdzie każdy z elementów będzie inną tablicą.
Być może ktoś oczekuje takiego efektu i takie działanie jest ok, natomiast warto o tym pamiętać, bo w wielu przypadkach zależy nam jednak na połączeniu wartości tablic, niż na przechowywaniu tablic wewnątrz innej tablicy…

let array_one = [1,2,3];
let array_two = [4,5,6];
let new_array = [array_one, array_two];

console.log(new_array);
// Output: [ [ 1, 2, 3 ], [ 4, 5, 6 ] ]

Obiekty

Tak jak w przypadku tablic, pracując na obiektach wraz ze spread operatorem, możemy skopiować do nowego obiektu właściwości innego obiektu. W zasadzie działanie i użycie jest bardzo podobne.

let product_obj = {color: 'red', name: 'tshirt'}
let size_obj = {size: 'xxl'}

let new_product = {...product_obj, ...size_obj};
console.log(new_product);
// Output: { color: 'red', name: 'tshirt', size: 'xxl' }
let object = {name: 'Magicweb.pl'}
let new_object = {...object, website: 'https://www.magicweb.pl'}

console.log(new_object);
// Output: { name: 'Magicweb.pl', website: 'https://www.magicweb.pl' }

Kopiowanie obiektu:

let object = {name: 'Magicweb.pl'}
let new_object = {...object};

console.log(object);
// Output: { name: 'Magicweb.pl' }
console.log(new_object);
// Output: { name: 'Magicweb.pl' }

Obiekty, podobnie jak tablice działają na referencji. Przypisanie obiektu do innej zmiennej da nam identyczny efekt, jak w przypadku tablicy. Modyfikując obiekt poprzez nową zmienną, de facto modyfikujemy ten sam obiekt… Warto mieć to na uwadze, bowiem takie przypisanie bardzo często powoduje trudne do znalezienia błędy w aplikacji.

let object = {name: 'Magicweb.pl'}
let new_object = object;

new_object.address = 'https://www.magicweb.pl';

console.log(object);
// Output: { name: 'Magicweb.pl', address: 'https://www.magicweb.pl' }
console.log(new_object);
// Output: { name: 'Magicweb.pl', address: 'https://www.magicweb.pl' }

String

Spread operatora możemy również użyć do „rozbicia” stringów na pojedyncze elementy. Osobiście nie wiem, czy warto stosować takie użycie w kodzie, natomiast jest taka możliwość…

let string_spread = 'Magicweb.pl'
console.log(...string_spread);
// Output: M a g i c w e b . p l

Wróćmy jeszcze do przykładu z Math.min() oraz Math.max(). Jeżeli jako argument podamy string wraz ze spread operatorem, to nasz string zostanie „rozbity” na pojedyncze znaki i Math wybierze najmniejszą, bądź największą wartość.

let string_value = '123';
Math.min(...string_value);
// Output: 1
Math.max(...string_value);
// Output: 3

Immutability

Na koniec chciałbym zwrócić uwagę na kwestię mutowania danych. Spread operator doskonale nadaje się do zapewnienia niemutowania danych i paradygmatu programowania funkcyjnego (niebawem na blogu pojawią się wpisy dotyczące programowania funkcyjnego i jego paradygmatów).

Wróćmy do przykładów z tablicami i obiektami, gdzie poprzez przypisanie nowych właściwości lub wykorzystanie metody .push() na tablicy, dokonywaliśmy modyfikacji tej samej tablicy/obiektu…

Przykład z mutowaniem danych:

function addTodo(todo, newTodo) {
  return todo.push(newTodo)
}

let todoList = ['code review', 'task 1', 'task 2'];
addTodo(todoList, 'task3');

console.log(todoList);
// Output: [ 'code review', 'task 1', 'task 2', 'task3' ]

Kod napisany w ten sposób łamie zasady programowania funkcyjnego. W tym przypadku zmodyfikowaliśmy tablicę "todoList". Powinniśmy jednak zwrócić zupełnie nową tablicę i tutaj właśnie z pomocą przyjdzie nam spread operator.

Brak mutowania danych:

function addTodo(todo, newTodo) {
  return [...todo, newTodo]
}

let todoList = ['code review', 'task 1', 'task 2'];
let newTodoList = addTodo(todoList, 'task3');

console.log(todoList);
// Output: [ 'code review', 'task 1', 'task 2' ]
console.log(newTodoList);
// Output: [ 'code review', 'task 1', 'task 2', 'task3' ]

Źródła:
Obraz: https://unsplash.com/photos/ZrqrP9Xs2vI (zmodyfikowane)