Оператор spread и rest параметры в JavaScript
В этой статье вы узнаете про возможности применения, такого функционала, как spread оператор и rest параметры. И как при помощи первого, без особого труда можно конвертировать итерируемые объекты в массивы.
Можно переводить эти названия, а можно и нет. Но я лично считаю, что в этом случае лучше придерживаться англоязычной терминологии, так как такой термин, как “оператор расширения” могут понять не все и вообще это довольно грубый перевод, а “остаточные параметры” может выходить из контекста данного применения. Но в тексте будут звучать два варианта.
Переменное количество параметров
Обычно при работе с 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
параметры круче arguments
. Rest
параметры являются частью функциональной сигнатуры. Это означает то, что прямо с “шапки” функции вы можете сразу понять, что она применяет остаточные параметры и следовательно, доступна для переменного количества аргументов. С объектом 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
Тут все это очень легко делается при помощи оператора расширения. Зачастую итерируемые объекты ограничены в выборе доступных для них методов. Но конвертируя их в массив, вы сразу же получаете доступ ко ВСЕМ прекрасным методам, доступным для массивов, таким как filter
, map
, reduce
.
[ ...'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 может много чего означать в зависимости от контекста.
Вы можете использовать её, как остаточные параметры в функции, таким образом упростив себе работу с переменным числом аргументов в ней. Ещё вы можете применять схожий подход с массивами или объектами при деструктуризации, где элементы на которых не было применено деструктурирующее назначение будут собраны в отдельный массив.
В свою очередь, оператор расширения может быть использован для распаковки итерируемых элементов в отдельные элементы, которые уже потом можно будет использовать при создании объектов, массивов и вызовов функций.