В ES6 появились операторы Spread (расширение) и Rest (остаток), которые позволяют простым образом объединять несколько массивов в один или передавать значения отдельных элементов массива в качестве аргументов функций. Интересно, что оба эти оператора внешне выглядят абсолютно одинаково - это троеточие (...
), но используются в разных случаях, причем не только для массивов, но и для объектов.
Рассмотрим их применение на нескольких типичных задачах.
Оператор spread
Оператор разворота spread используется для:
- объединения 2-х или более массивов в один
- копирования данных из одного массива в другой
- передачи массива в качестве аргументов функции
- применения методов объекта Math
- массива в качестве данных объекта Date
- разбиения строк на символы
- литералов объекта
Оператор spread ...
в переводе с английского - это распространение, расширение или разворот позволит нам склеить 2 массива в один. Например, нам зачем-то захотелось объединить зиму с летом. Попробуем сделать это методами массива push()
, concat()
и оператором spread
:
1 2 3 4 5 6 7 8 | let winter = ['Декабрь', 'Январь', 'Февраль'], summer = ['Июнь', 'Июль', 'Август']; // winter.push(summer); // console.log(winter); let seasons = winter.concat(summer); console.log(seasons); let seasons2 = [...winter, ...summer]; console.log(seasons2); |
Примечание: строчки с winter.push(summer)
и console.log(winter)
нужно закомментировать, прежде, чем применять метод concat()
и оператор spread
, т.к. он "вкладывает" сам второй массив внутрь первого, а не его значения.
В консоли вы увидите такие варианты объединения массивов:
Метод concat(
) объединил массивы, и то же самое сделал spread
оперратор. Но что делать, если нам нужно поместить данные одного массива между данными другого массива? Т.е. нужно сделать примерно так:
1 2 3 4 5 | let seasons3 = ['Апрель','Май', summer, 'Сентябрь','Октябрь']; let seasons4 = ['Апрель','Май', ...summer, 'Сентябрь','Октябрь']; console.log(seasons3, seasons4); |
В консоли вы увидите, что обычным способом мы поместили массив внутрь другого массива, а spread-оператор добавил значения массива в текущий массив.
Копирование данных из одного массива в другой
Еще один вариант применения оператора spread
- это копирование данных из одного массива в другой. Если написать так:
1 2 3 4 5 | let numbers = [10, 20, 30, 40, 50], numbers2 = numbers; console.log('1-й: ', numbers, '2-й: ', numbers2); numbers.unshift(1,2,3); console.log('После добавления 1-й: ', numbers, 'После добавления 2-й: ', numbers2); |
В консоли получим такой результат:
То есть, использовав код numbers2 = numbers
мы на самом деле получили ссылку на первый массив numbers
. И второй массив после добавления данных в первый изменился так же, как и первый. Если же задача заключается не в использовании 2-х переменных, ссылающихся на один массив, а в сохранении в одной из переменных старых данных из массива, например, для последующего сравнения, то как раз имеет смысл использовать оператор расширения. Несколько перепишем код выше:
1 2 3 4 5 | let numbers = [10, 20, 30, 40, 50], numbersSpread = [...numbers]; console.log('1-й: ', numbers, '2-й: ', numbersSpread); numbers.unshift(1,2,3); console.log('После добавления 1-й: ', numbers, 'После добавления 2-й: ', numbersSpread); |
В консоли видим, что одинаковые массивы после добавления данных перестали быть одинаковыми, т.е. копирование с помощью оператора spread
создает нам новый массив.
Можно также проверить, совпадают ли массивы
1 2 | console.log(numbers === numbers2); //true console.log(numbers === numbersSpread); //false |
Примечание: до выхода стандарта ES6 копирование можно было сделать с помощью метода array.slice()
:
1 2 3 4 5 | let numbers = [10, 20, 30, 40, 50], numbersSlice = numbers.slice(); console.log('1-й: ', numbers, '2-й: ', numbersSlice); numbers.unshift(1,2,3); console.log('После добавления 1-й: ', numbers, 'После добавления 2-й: ', numbersSlice); |
Попробуйте сами.
Spread в качестве аргументов функции
Еще одно применение оператора spread позволяет развернуть массив для использования значений его элементов в качестве аргументов функции. В идеале количество элементов массива должно совпадать с количеством аргументов функции. Если элементов массива будет больше, то избыток проигнорируется, если меньше, то вместо отсутствующих элементов будет передано значение undefined
. Например, нам нужно перемножить 3 числа:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | function multiply(a, b, c) { return a * b * c; } let arrNum = [2, 20, 3]; console.log(multiply(...arrNum)); // выведет: 120 console.log(multiply.apply(null, arrNum));// 120 console.log(multiply(...[4,15,5,2,7])); // выведет: 300 //проигнорирует последних 2 аргумента console.log(multiply(...[4,15])); // выведет: NaN, //т.к. последний аргумент будет undefined |
Если элементов массива недостаточно, их можно заменить значениями по умолчанию. Перепишем нашу функцию таким образом:
1 2 3 4 5 6 7 8 | function multiply(a = 1, b = 1, c = 1) { //console.log(a,b,c); return a * b * c; } console.log(multiply(...[4,15])); // выведет: 60 console.log(multiply(...[,3,70])); // выведет: 210 console.log(multiply(...[5,,5])); // выведет: 25 console.log(multiply(...[12])); // выведет: 12 |
Теперь функция будет выводить корректный результат умножения чисел, если их передается меньше 3-х или одно из значений в массиве undefined
.
Для использования методов объекта Math
Например, существует задача найти минимальное и максимальное значение среди элементов массива. Можно решить ее, перебрав все элементы массива методом array.forEach()
и сравнив их с неким начальным значением, например, с 0 или со значением 0-го элемента массива. То же самое можно сделать с помощью метода array.reduce()
:
1 2 3 4 5 | var numbers = [34, -12, 27, 99, 0, 32]; let max = numbers.reduce((prev, next) => Math.max(prev, next) ); let min = numbers.reduce((prev, next) => Math.min(prev, next) ); document.write('<p>Максимальное число в массиве = ' + max + ', минимальное число в массиве = ' + min + '</p>'); |
Для нашего массива numbers это должны быть числа 99, как максимальное, и -12, как минимальное. Давайте посмотрим, что выведет скрипт:
Все работает верно.
Теперь попробуем еще один способ. Это использование метода apply()
для передачи массива аргументов в метод Math.max()
или Math.min()
:
1 2 3 4 | let max1 = Math.max.apply(Math, numbers); let min1 = Math.min.apply(Math, numbers); document.write('<p>Максимальное число в массиве = <strong style="color: blue">' + max1 + '</strong>, минимальное число в массиве = <strong style="color: blue">' + min1 + '</strong></p>'); |
Проверяем на практике - видим те же значения:
И последнее решение, в котором мы будем использовать оператор spread
:
1 2 3 4 | let max2 = Math.max(...numbers); let min2 = Math.min(...numbers); document.write('<p>Максимальное число в массиве = <strong style="color: red">' + max2 + '</strong>, минимальное число в массиве = <strong style="color: red">' + min2 + '</strong></p>'); |
Результат тот же, но кода совсем мало, и он легко читается.
Еще один плюс использования оператора spread
в нашем примере заключается в том, что мы можем очень простым способом найти максимальное и минимальное значения не только в одном массиве, но и в 2-х, 3-х и более, не прибегая к их конкатенации (объединению). И даже можем добавить в скобки любое количество дополнительных чисел, которые не входят ни в один из массивов. Для этого нужно записать такой код:
1 2 3 4 5 6 7 8 9 10 | let firstArr = [30, 12, 0, 14], secondArr = [69, 17, 56, 80]; let max3 = Math.max(...firstArr, ...secondArr); let min3 = Math.min(...firstArr, ...secondArr); document.write('<p>Максимальное число в 2-х массивах = <strong style="color: orange">' + max3 + '</strong>, минимальное число в 2-х массивах = <strong style="color: orange">' + min3 + '</strong></p>'); let max4 = Math.max(...firstArr, -10, 45, ...secondArr, 104, -17); let min4 = Math.min(...firstArr, -10, 45, ...secondArr, 104, -17); document.write('<p>Максимальное число в 2-х массивах и числах = <strong style="color: cornflowerblue">' + max4 + '</strong>, минимальное число в 2-х массивах и числах = <strong style="color: cornflowerblue">' + min4 + '</strong></p>'); |
Думаю, что вы быстро определите, какие числа попадут в переменные max3, max4
и min3, min4
. Проверьте, верен ли код:
Для объекта Date
И это еще не все. Вы можете выполнить разворачивание массива в качестве данных объекта Date с помощью оператора spread и получить корректную дату:
1 2 3 | var dateFields = [2019, 8, 1, 8, 30]; // 01 Sep 2019 8:30:00 var d = new Date(...dateFields); console.log(d.toLocaleString()); //01.09.2019, 08:30:00 |
Использование оператора spread для разбиения строк на символы
Не очень очевидное использование оператора расширения для строк позволит сделать из любой строки массив символов, чтобы выделить в строке каждую букву. Например, так:
1 2 3 | let litera = 'ABCDEFG'; console.log(...litera); [...litera].forEach(character=> document.write(`<strong> ${character} </strong> | `)); |
Давайте посмотрим на результат:
Задача: попробуйте видоизменить код таким образом, чтобы каждая из букв была ссылкой на какую либо страницу сайта flibusta.is, например, так: E | F
А теперь несколько более впечатляющий пример с использованием для каждой буквы строки обертки в виде <span>
с классом animCharacter
, относительного позиционирования для него и анимации @keyframes
:
See the Pen JS Spread operator for string by Elen (@ambassador) on CodePen.
Использование оператора spread для литерала объекта
С выходом стандарта ECMAScript 2018 стало возможным использование оператора spread
для литералов объекта, например, для их копирования:
1 2 3 4 5 6 7 8 9 10 11 | let user = { name: 'Николай', surname: 'Гаврилов', password: 'mypassw' }, userLink = user, userCopy = {...user }; user.id = 20067; console.log('user', user, 'userLink', userLink, 'userCopy', userCopy); |
В переменной userLink
мы получаем ссылку на основной объект user
, поэтому после добавления в user
свойства id
, меняются оба объекта, ведь фактически они указывают путь к одному и тому же литералу объекта. А в переменной userCopy
мы сохранили данные объекта user
в его первоначальном виде.
О других способах копирования объектов читайте в специальной статье, посвященной этому.
Оператор расширения также может нам помочь в случае, если нужно объединить ряд данных, принадлежащих по сути одному объекту, но сохраненных в разных. Например, у нас есть 3 объекта, хранящие тип, возраст и цену одного и того же кота. Мы можем их объединить в один объект, но разница между использованием обычного объекта и полученного за счет оператора разворота очевидна:
1 2 3 4 5 6 7 | let animalData1 = {name: 'Мурзик', type: 'cat'}, animalData2 = {name: 'Мурзик', age: 1}, animalData3 = {name: 'Мурзик', price: '200грн'}; let animalDatas = {animalData1, animalData2, animalData3}; console.log(animalDatas); let animalsObj = {...animalData1, ...animalData2, ...animalData3}; console.log(animalsObj); |
В консоли видно, что оператор расширения собрал все данные в один объект из разных, а не добавил 3 объекта с похожими данными:
Ряд интересных примеров с манипуляциями свойствами литералов объектов вы найдете в статье Rest и Spread в JavaScript. Возможности, о которых вы не знали, а также в справке на MDN.
Преобразование коллекции DOM элементов
При выборе различными методами коллекций html-элементов в DOM-модели браузера мы получаем псевдомассивы, к которым далеко не всегда можно применить методы встроенного объекта Array, с помощью которых очень удобно выполнять какие-либо одинаковые операции.
Например, метод document.querySelectorAll()
реализован таким образом, что возвращает коллекцию NodeList, для которой можно вызвать метод forEach()
, но не другие методы массивов. А свойство document.links
или метод document.getElementsByClassName()
вернет коллекцию HTMLCollection, для которой даже forEach()
вызвать нельзя. Чтобы воспользоваться все-таки методами массивов, применим к ним оператор расширения и посмотрим, стали ли они массивами:
1 2 3 4 5 6 7 8 9 10 11 12 | let pCollection = document.querySelectorAll('#example p'), pArray = [...document.querySelectorAll('#example p')], links = document.links, linksArray = [...document.links]; console.log(pCollection, pCollection instanceof Array); //false console.log(pArray, pArray instanceof Array); //true console.log(links, links instanceof Array); //false console.log(linksArray, linksArray instanceof Array); //true |
Все сработало и для NodeList, и для HTMLCollection.
В примере ниже при клике на кнопках вызываются функции, которые позволяют использовать методы или циклы для работ с массивами, чтобы управлять абзацами или ссылками
See the Pen Array from HTML Coollection with spread operator by Elen (@ambassador) on CodePen.
В сущности, для использования оператора расширения важно, чтобы объект был итерируемым, т.е. для него были бы доступны числовые ключи, которые можно потом использовать в виде индексов массива.
Оператор rest
Оператор rest (оставшиеся параметры) используется для указания массива в качестве аргументов функции. Например:
1 2 3 4 5 6 7 8 9 | function summa(...numbers){ console.log(numbers ); //[45, 20, -5, -10], [5, 4, 3, 2, 1] console.log(numbers instanceof Array); //true let sumNum = 0; numbers.forEach(num => sumNum += num); console.log(sumNum); } summa(45, 20, -5, -10); //50 summa(5, 4, 3, 2, 1); //15 |
В этом примере уже можно увидеть, что оператор rest при внешней идентичности делает нечто противоположное оператору spread, а именно преобразует в массив переданные через запятую параметры функции.
В том случае, если вам нужно, чтобы какие-то параметры задавались и использовались в функции обязательно, можно добавить оператор rest
после нужных аргументов функции.
Допустим, нам нужно сделать простой калькулятор, который позволит указать операцию (сложение, вычитание, умножение и деление), число, с которым нужно произвести вычисление и еще ряд чисел, которые будут прибавляться, отниматься или выступать множителями или делителями. Поскольку оператор оставшихся параметров выдаст нам массив переданных в функцию значений, будет очень удобно использовать для вычислений метод array.reduce()
с указанным начальным значением и стрелочными функциями в качестве callback-ов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | function simpleCalc(a = 'add', b = 1, ...more) { switch (a) { case 'diff': return more.reduce(((item, current) => item - current), b); case 'multi': return more.reduce(((item, current) => item * current), b); case 'div': if(more.includes(0)) return 'На ноль делить нельзя!'; return more.reduce(((item, current) => item / current), b); default: return more.reduce(((item, current) => item + current), b); } } console.log(simpleCalc('diff', 100, 12, 24, 38)); //26 console.log(simpleCalc('multi', 100, 2, 4)); //800 console.log(simpleCalc('multi', 100, 2, 0)); //0 console.log(simpleCalc('div', 100, 2, 4)); //12.5 console.log(simpleCalc('div', 100, 2, 0)); //На ноль делить нельзя! console.log(simpleCalc('add', 100, 12, 14, 38)); //164 console.log(simpleCalc(100, 100, 12, 14, 38)); //164 |
Опять-таки с помощью метода массива array.includes()
мы сделали внутри функции проверку на деление на 0. Очень удобно использовать методы массивов, не так ли?
Следует понимать, что оператор rest (оставшиеся параметры) отличается от объекта arguments
:
- в объекте
arguments
содержатся все аргументы, передаваемые в функцию, а оставшиеся параметры включают только те, которым не задано отдельное имя; - объект
arguments
не является массивом, а оставшиеся параметры, наоборот, принадлежат классуArray
и для них можно использовать любые методы массивов (forEach(), map(), reduce
и др.), которые ранее были доступны только с помощьюcall()
илиapply()
; - объект
arguments
имеет дополнительные свойства (например,callee
), характерные только для него .
Деструктивное присваивание
Синтаксис destructuring assignment
- деструктивного, или деструктурирующего присваивания, предполагает, что в коде используется массив или литерал объекта.
Деструктивное присваивание для элементов массива
Оператор разворота spread позволял нам объединить 2 массива в один, а деструктурирующее присваивание - распределить данные между элементами массива и переменными. Например, у нас есть набор данных в виде массива и нам нужно присвоить значения его элементов, распределив их по определенным переменным. Это будет выглядеть так:
1 2 3 | let [x, y, w, h, type] = [100, 150, 30, 160, 'rect']; console.log(x, y, w, h, type); // 100 150 30 160 "rect" console.log(x, y, type, w, h); // 100 150 "rect" 30 160 |
Обратите внимание, что в первом выводе в console.log()
был сохранен порядок присвоения, а во втором - изменен. И тем не менее, значения этих переменных выведены именно в том виде, в котором произошло деструктивное присваивание.
Частичная деструктуризация
Деструктуризация может быть частичной, т.е. в процессе присваивания данных в переменные какие-либо из этих данных или переменных мы можем опустить. Например, из кода выше мы опустим часть данных в конце или в начале.
1 2 3 4 | let [x, y] = [100, 150, 30, 160, 'rect']; console.log(x, y); // 100 150 let [, , w, h, type] = [100, 150, 30, 160, 'rect']; console.log( w, h, type); // 30 160 "rect" |
Оставшиеся параметры для деструкции
Деструкция может использовать оставшиеся параметры. Например, необходимо разбить строку на части, причем интересует нас первая буква, а остальные не слишком важны. Пример имеет такой код:
1 2 3 4 | let str = 'Иван Довгий'; let [first, ...rest] = str.split(''); console.log(first, rest); // "И" (10)> ["в", "а", "н", " ", "Д", "о", "в", "г", "и", "й"] |
В результате мы получили букву "И" в качестве переменной first
и массив остальных символов в переменной rest
.
Давайте посмотрим, как мы можем использовать этот вариант на html-странице. например, у нас есть ряд комментариев, которые содержат имя автора комментария и сам комментарий. По задумке дизайнера первая буква каждого имени автора должна быть выделена красным цветом. Внешний вид примера таков:
Код окраски имени автора комментария базируется на предыдущем примере и выглядит так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <style> .test-comment-author { font-weight: bold; margin: 10px; } .test-comment-body { margin-left: 18px; border-left: 2px dashed #d00; padding: 10px; } .red {color: #d00;} </style> <div class="test"> <div class="test-comment"> <div class="test-comment-author">Иван Довгий | 20.09.2019</div> <div class="test-comment-body">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eligendi mollitia amet enim quis nulla explicabo!</div> </div> <div class="test-comment"> <div class="test-comment-author">Админ | 21.09.2019</div> <div class="test-comment-body">Perferendis quas esse laboriosam harum quia aliquam! Dolores quidem, porro dolore temporibus unde consequatur nihil?</div> </div> ... </div> <script> let author = document.querySelectorAll('.test-comment-author'); author.forEach(item => { let [first, ...rest] = item.textContent.split(''); item.innerHTML = `<span class="red">${first}</span>${rest.join('')}`; }); </script> |
Здесь мы используем в качестве строки свойства textContent
и innerHTML
, т.к. именно они позволяют получить и переписать строку из JavaScript в HTML.
Деструктуризация для возврата значений из функций
Оператор return
в функции позволяет вернуть какое-то одно значение. Оно может быть массивом или объектом, но в единсвенном экземпляре. А нам, предположим, нужно записать данные в 2 или более переменные. С деструктивным присваиванием это решается просто.
В примере нам нужно умножить 2 числа на разные множители и вернуть результат. Мы можем сделать это при однократном вызове функции.
1 2 3 4 5 | function mutipleSomeValues (x, y){ return [x*2, y*3]; } let [a, b] = mutipleSomeValues(10, 20); console.log(a, b); //20 60 |