Подписывайтесь на мой твиттер, там всегда что-нибудь интересное!

Оператор spread и rest параметры в JavaScript

В этой статье вы узнаете про возможности применения, такого функционала, как spread оператор и rest параметры. И как при помощи первого, без особого труда можно конвертировать итерируемые объекты в массивы.

Можно переводить эти названия, а можно и нет. Но я лично считаю, что в этом случае лучше придерживаться англоязычной терминологии, так как такой термин, как “оператор расширения” могут понять не все и вообще это довольно грубый перевод, а “остаточные параметры” может выходить из контекста данного применения. Но в тексте будут звучать два варианта.

Переводы статей Javascript spread operator and rest parameters (…) и Convert Iterables to Array using Spread in JavaScript

Переменное количество параметров

Обычно при работе с JavaScript функциями у нас имеется конкретное число параметров для каждой функции. Если вы хотите посчитать квадратный корень числа с помощью Math.sqrt(x), то вам всегда нужно вводить один параметр. Само собой, некоторые функции могут требовать и несколько параметров. Но в некоторых случаях с большим количеством оных, вы можете спокойно пропустить несколько последних из них, в силу их опциональности. Во всех этих случаях, все параметры указываются, как часть сигнатуры функции:

function doSomething(first, second, third) { 
    // Do something cool
}

Но что, если количество параметров вообще неизвестно заранее и их может быть вообще неизвестно сколько?

Для примера, давайте возьмём Math.min() и Math.max(). Они работают с любым количеством параметров, хоть с 0, 2-мя или 30-ю.

Math.Min(); // Infinity
Math.Min(1); // 1
Math.Min(1,2,3,4,3,1,5,7,9,0,-9,18,37,81); // -9

Как бы вы написали такую функцию?

Arguments — традиционный подход

Когда вам приходится работать с переменным количеством параметров в функции, то обычно вашим единственным вариантом был бы объект arguments. Внутри каждой функции существует объект argument, который имеет массивоподобную структуру, содержащую все аргументы, переданные функции. Каждый из которых вы можете получить начиная с нулевого индекса, также вы можете проверить количество параметров, как и с любым другим массивом.

function doSomething() { 
    arguments[0]; // "A"
    arguments[1]; // "B"
    arguments[2]; // "C"
    arguments.length; // 3
}

doSomething("A","B","C");

Ограничения arguments

Обратите внимание на то, что arguments это массивоподобный объект, а не полноценный массив. Это говорит о том, что такие полезные методы, как arguments.sort()arguments.map() или arguments.filter() будут вам недоступны. В этом случае у вас будет только свойство length.

Еще одно ограничение в том, что в стрелочных функциях недоступен объект arguments. В случае, если бы вы использовали arguments в стрелочной функции, то она бы перенаправила вас к коллекции аргументов вложенной функции (если таковая бы имелась).

Rest параметры (остаточные параметры)

К нашей радости, начиная с ES6, объект arguments больше не является единственным способом работы с переменным количеством параметров. ES6 дал нам такую концепцию, как rest параметры. Это означает то, что вы просто можете вставить перед последним параметром функции.

function doSomething(first, second, ...rest) {
    // do something cool
}

Теперь, в этом примере вы можете получить доступ к первым двум именованным параметрам как и обычно. А вот уже к другим аргументам, переданным функции начиная с третьего, вы сможете получить доступ через массив под названием rest, куда все они автоматически будут собираться.

function doSomething(first, second, ...rest) {
    console.log(first); // Первый аргумент
    console.log(second); // Второй аргумент
    console.log(rest[0]); // Третий аргумент
    console.log(rest[1]); // Четвертый
    // Etc.
}

Если вы передадите меньше, чем 3 параметра, то rest будет просто пустым массивом.

В отличие от объекта arguments, rest параметры отдают реальный массив, так что вы в этом случае сможете использовать буквально все доступные в этом случае методы. Более того, в отличие от arguments, они работают в стрелочных функциях.

let doSomething = (...rest) => {
    rest[0]; // Доступ к первому аргументу
};

let doSomething = () => {
    arguments[0]; // У стрелочных функций нет аргументов
};

В придачу к преимуществам описанным выше, есть ещё одно, которое делает rest параметры круче argumentsRest параметры являются частью функциональной сигнатуры. Это означает то, что прямо с “шапки” функции вы можете сразу понять, что она применяет остаточные параметры и следовательно, доступна для переменного количества аргументов. С объектом arguments, такого трюка не получится. С ним нам придется вчитываться в тело функции или в комментарии, которые будут нам говорить о возможности добавления переменного количества параметров. Что ещё важно, так это то, что редакторы кода могут определять остаточные параметры в сигнатуре и ещё лучше помогать вам при разработке.

Ограничения

Rest параметры берут все оставшиеся аргументы функции и складывают их в массив. Этот подход влечет собой два ограничения. Вы можете использовать указание rest параметров только единожды при написании функции, большее их количество будет просто недопустимо:

// Так делать нельзя, нужен только один параметр rest
function doSomething(first, ...second, ...third) {
}

И вы можете использовать эти параметры только, как последние параметры функции:

// Тут ничего не выйдет, rest параметры не находятся в конце функции
function doSomething(first, ...second, third) {
}

Spread оператор (оператор расширения)

Что тут смущает, так это такое же троеточие , как и в rest параметрах. Но называется уже по-другому, а именно spread оператором. Его назначение почти противоположно вышеупомянутым остаточным параметрам. Вместо сбора нескольких значений в один массив, он позволяет расширить заданный массив (или другой итерируемый объект) в несколько значений. Давайте посмотрим на разнообразные способы его применения:

Вызовы функций

Давайте предположим, что у нас есть массив с тремя элементами и функция, которой нужно три параметра.

let myArray = [1, 2, 3];

function doSomething(first, second, third) {
}

Как вы передадите три значения нашего массива, как три отдельных аргумента для функции doSomething()? Есть довольно наивный подход к этому делу:

doSomething(myArray[0], myArray[1], myArray[2]);

Очевидно, что это не очень хороший подход, особенно с учетом большого количества параметров и работать он будет только если мы будем знать количество этих самых параметров заранее. Давайте попробуем что-нибудь другое. Перед появлением оператора расширения, вот такой подход использовался для вызова функции и передачи массива в виде раздельных параметров:

doSomething.apply(null, myArray);

Первым параметром в apply() является то, что мы собираемся использовать, как this. Вторым параметром тут будет массив, который мы хотим передать функции, как аргументы.

Но с оператором расширения вы можете достигнуть тех же результатов с помощью:

doSomething(...myArray);
// то же самое
doSomething(myArray[0], myArray[1], myArray[2]);

Обратите внимание, что этот метод работает с любым итерируемым объектом, а не только с массивами. Для примера, оператор расширения разделит строку на отдельные символы из которых состоит слово или предложение. Но об этом немного дальше.

А еще это можно совмещать вместе с передачей отдельных параметров. В отличие от rest параметров, вы можете применить несколько spread операторов в одном вызове функции и им не обязательно быть последним элементом в переданных параметрах.

// Всё это возможно
doSomething(1, ...myArray);
doSomething(1, ...myArray, 2);
doSomething(...myArray, ...otherArray);
doSomething(2, ...myArray, ...otherArray, 3, 7);

Массив литералы

Оператор расширения также можно использовать при создании массива. Этим способом вы сможете вставить элементы из других массивов (или перечисляемые объекты, такие как строки) на указанные места в будущем массиве.

let firstArray = ["A", "B", "C"];
let secondArray = ["X", ...firstArray, "Y", "Z"];
// вторым массивом будет [ "X", "A", "B", "C", "Y", "Z" ]

Используя оператор расширения в массив литерале, мы как бы говорим: второй массив должен иметь “X”, как первый аргумент, а далее уже идут все элементы массива firstArray, вне зависимости от их количества. И под конец у нас два элемента “Y” и “Z”.

Объединение массивов

Естественно, вы можете использовать оператор расширения в массив литерале множество раз. Это может пригодиться, к примеру, для объединения нескольких уже существующих массивов, что было бы затруднительнее, при использовании традиционного императивного подхода.

let mergedArray = […firstArray, …secondArray, …thirdArray];

Копирование массивов

Иногда также полезно создать копию существующего массива с таким же набором элементов, как и у оригинального. Это можно легко и просто сделать с помощью spread оператора.

var original = [1, 2, 3];
var copy = [...original];

Объект литералы

Оператор расширения очень схоже используется при создании объектов. Вы можете взять свойства одного объекта и включить их в новый объект, созданный объект литералом. Этот функционал доступен с ES2018 версии.

let firstObject = {a: 1, b: 2};

let secondObject = {...firstObject, c: 3, d: 4};

console.log(secondObject); // { a: 1, b: 2, c: 3, d: 4 }

Имейте ввиду, что spread берет только собственные (не наследуемые) и перечислимые свойства объекта, все другие свойства будут попросту игнорироваться.

Поверхностная копия

Тут всё очень схоже с массивами. Вы можете смело склеивать и клонировать объекты.

let clone = {…original};

Это может быть хорошей альтернативой для клонирования объектов с помощью Object.assign(). Но обратите внимание, что это поверхностная копия. Новый объект создастся, но клонированные свойства все равно будут от оригинала, а не клонами.

Потерянный прототип

Клонируя объект способом выше вам стоит знать то, что прототип оригинального объекта не сохранится. Там просто копируются свойства исходного объекта и создастся совершенно новый объект, используя объект литерал, у которого прототип Object.prototype.

Конфликты свойств

Что случиться когда вы укажите свойство через оператор расширения, но это свойство уже будет существовать в объекте? Это не приведет к ошибке. Просто напросто, если будет несколько свойств объекта с одним и тем же именем, но разными свойствами, то победит последний.

let firstObject = {a: 1};
let secondObject = {a: 2};

let mergedObject = {...firstObject, ...secondObject};
// a: 2 is after a: 1 so it wins
console.log(mergedObject); // { a: 2 }

Изменение иммутабельных объектов

Момент, когда ранее объявленное свойство с тем же именем перезаписывает другое, можно применить при изменении иммутабельных объектов. Когда вы работаете с таковыми или не хотите напрямую изменять изменять объекты, вы можете смело применить оператор расширения для создания нового объекта, как измененного варианта исходной копии.

let original = {
      someProperty: "oldValue", 
      someOtherProperty: 42
    };

let updated = {...original, someProperty: "newValue"};
// Теперь { someProperty: "newValue", someOtherProperty: 42 }

Так как в оригинальной копии есть someProperty и в дальнейшем оно также используется, то будет взято значение при последнем её упоминании. Исходный объект не изменится в любом случае, но создастся новый объект.

Деструктурирующее присваивание

Если кратко, то деструктурирующее присваивание это способ назначения свойств объекта или значений из массивов в отдельные переменные.

Деструктуризация массивов

Предположим, что у нас есть массив из трех элементов и каждый из них нам надо назначить как самостоятельную переменную.

let myArray = [1, 2, 3];
let a,b,c; //Мы хотим назначить a=1, b=2, c=3

Мы это можем легко сделать с помощью деструктуризации:

let myArray = [1,2,3];
let [a, b, c] = myArray;

console.log(a); // 1
console.log(b); // 2
console.log(c); // 3

Но как это вообще связано с синтаксисом из трех точек, про который мы говорили выше? Помните остаточные параметры? Так же, как и все оставшиеся аргументы пакуются в один массив, мы можем назначить все оставшиеся после деструктурирующего присваивания элементы в новую переменную, которая будет являться массивом:

let myArray = [1, 2, 3, 4, 5];
let [a, b, c, ...d] = myArray;

console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
console.log(d); // [4, 5]

Деструктуризация объектов

Тут все очень похоже на тему с массивами. Имя каждой переменной совпадает с именем свойства из деструктуризированного объекта.

let myObject = { a: 1, b: 2, c: 3, d: 4};
let {b, d} = myObject;

console.log(b); // 2
console.log(d); // 4

И также, как и с массивами, вы можете использовать троеточие для сбора всех оставшихся свойств, которые не были назначены на переменные, которые в последствии будут представлять собой отдельный объект.

let myObject = { a: 1, b: 2, c: 3, d: 4};
let {b, d, ...remaining} = myObject;

console.log(b); // 2
console.log(d); // 4
console.log(remaining); // { a: 1, c: 3 }

Конвертируем итерируемые в массив при помощи Spread

Тут все это очень легко делается при помощи оператора расширения. Зачастую итерируемые объекты ограничены в выборе доступных для них методов. Но конвертируя их в массив, вы сразу же получаете доступ ко ВСЕМ прекрасным методам, доступным для массивов, таким как filtermapreduce.

[ ...'hi' ]; // // ['h', 'i']
[ ...new Set([1,2,3]) ]; // [1,2,3]
[ ...new Map([[1, 'one']]) ]; // [[1, 'one']]
[ ...document.querySelectorAll('div') ] // [ div, div, div]

Уже готовые итерируемые

В JavaScript у нас есть несколько уже готовых итерируемых объектов, которые мы можем конвертировать в массив при помощи spread:

Строки

Массивы

Map

Set

Есть ещё один, но мы о нем пока не будем говорить это — TypedArray.

Что такое итерируемые?

Это структуры данных, которые предоставляют механизм для доступа сторонних операторов данных, в виде общедоступного доступа к своим элементам последовательным образом.

Строка  Массив

const myString = 'hello';
const array = [...myString] // [ 'h', 'e', 'l', 'l', 'o' ]

Мы можем конвертировать массив обратно в строку, используя join():

array.join(''); // 'hello'

Set  Массив

const mySet = new Set([1, 2, 3]);
const array = [...mySet] // [1, 2, 3]

Мы можем конвертировать массив обратно в set, передав его new Set

new Set(array); // Set { 1, 2, 3 }

Map  Массив

const myMap = new Map([[1, 'one'], [2, 'two']]);
const array = [...myMap] // [ [ 1, 'one' ], [ 2, 'two' ] ]

Так же, как и с Set, мы можем конвертировать массив обратно в Map, передав его в new Map

new Map(array); // Map { 1 => 'one', 2 => 'two' }

NodeList  Массив

const nodeList = document.querySelectorAll('div');
const array = [ ...document.querySelectorAll('div') ];
// [ div, div, div] *

Тут я думаю, что понятно, то что настоящую выдачу можно увидеть только в браузере

Array.from vs Spread

Ещё один очень схожий с Spread синтаксисом метод это Array.from. По факту, мы можем смело заменить наши примеры вот так:

Array.from('hi') // // ['h', 'i']
Array.from(new Set([1,2,3])) // [1,2,3]
Array.from(new Map([[1, 'one']])) // [[1, 'one']]
Array.from(document.querySelectorAll('div')) // [ div, div, div]

Так в чем разница?

Разница в определении.

Array.from работает для:

Массивоподобных объектов (объекты с свойством длины и индексированными элементами)

Итерируемых объектов

Spread работает только для:

Итерируемых объектов

Итак, давайте посмотрим на этот массивоподобный объект:

const arrayLikeObject = {
  0: 'a', // indexed element
  1: 'b', // indexed element
  length: 1, // length property
};
Array.from(arrayLikeObject); // [ 'a', 'b' ]
[...arrayLikeObject]; // TypeError: arrayLikeObject is not iterable

Тут мы получаем ошибку о том, что arrayLikeObject не является итерируемым.

Заключение

Троеточие в JS может много чего означать в зависимости от контекста.

Вы можете использовать её, как остаточные параметры в функции, таким образом упростив себе работу с переменным числом аргументов в ней. Ещё вы можете применять схожий подход с массивами или объектами при деструктуризации, где элементы на которых не было применено деструктурирующее назначение будут собраны в отдельный массив.

В свою очередь, оператор расширения может быть использован для распаковки итерируемых элементов в отдельные элементы, которые уже потом можно будет использовать при создании объектов, массивов и вызовов функций.