JavaScript

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

  • Оператор spread, в котором тяжело разобраться отчасти из-за сложной документации при отсутствии большого опыта в разработке.
  • Деструктуризация, представляющая собой логическое действие, однако из-за отсутствия в других языках (эта функция есть в Python и является одной из замечательных особенностей этого языка), немногие разработчики знают о ней.

Приступим!

Оператор Spread

Оператор spread является единственным недооцененным оператором JavaScript, однако разобравшись в его особенностях, вы приобретете очень мощный инструмент в свой арсенал.

Рассмотрим документацию из MDN:

С помощью синтаксиса Spread можно расширить итерацию, например, выражение массива или строку, там, где ожидается ноль или более аргументов (для вызовов функций) или элементов (для литералов массива), или расширить выражение объекта там, где ожидается ноль или большее количество пар ключ-значение (для литералов объекта).

Переформулируем для лучшего понимания:

С помощью синтаксиса Spread можно расширить элементы, сгруппированные внутри определенного контейнера в данный момент, и назначить их другому контейнеру. Совместимые контейнеры включают: массивы, строки, объекты и другие итерируемые элементы (такие как Maps, Sets, TypedArrays и т. д.), а их элементы можно расширить в качестве аргументов функции, элементов массива или пар ключ-значение.

Переформулируем для лучшего понимания:

Теперь рассмотрим несколько кратких примеров для закрепления понятия данных механизмов.

let myArray1 = [1,2,3]
let myString = "Hey planet!"
let myObject = {
    name: "Fernando Doglio",
    age: 35,
    country: "Uruguay",
    [Symbol.iterator]: function* () { //мы делаем объект итерируемым, чтобы его можно было расширить
        yield myObject.name
        yield myObject.age
        yield myObject.country
    }
}

function test() {
    console.log(arguments)
}

let splitLetters = [...myString] //myString.split() больше не нужно использовать
console.log(splitLetters)
//[ 'H', 'e', 'y', ' ', 'p', 'l', 'a', 'n', 'e', 't', '!' ]

let objLetters = {...myString}
console.log(objLetters)
/*
{ '0': 'H',
  '1': 'e',
  '2': 'y',
  '3': ' ',
  '4': 'p',
  '5': 'l',
  '6': 'a',
  '7': 'n',
  '8': 'e',
  '9': 't',
  '10': '!' }
*/
test(...myString)
/*
[Arguments] {
  '0': 'H',
  '1': 'e',
  '2': 'y',
  '3': ' ',
  '4': 'p',
  '5': 'l',
  '6': 'a',
  '7': 'n',
  '8': 'e',
  '9': 't',
  '10': '!' }
*/
//то же самое
test.call(null, ...myArray1)
//[Arguments] { '0': 1, '1': 2, '2': 3 }
test.apply(null, myArray1)
//[Arguments] { '0': 1, '1': 2, '2': 3 }

let objValues = [...myObject] //если объект является итерируемым, то он может заменить Object.values(myObject)
console.log(objValues)
//[ 'Fernando Doglio', 35, 'Uruguay' ]

let {name, age} = {...myObject} //разбиение свойств на отдельные переменные
console.log("Name::", name, " age::", age)
//Name:: Fernando Doglio  age:: 35


test(...myObject) //превращение объекта в 3 разных аргумента
//[Arguments] { '0': 'Fernando Doglio', '1': 35, '2': 'Uruguay' }

Здесь есть несколько интересных моментов:

  • Окружая контейнер с помощью {}, [] или (), мы указываем желаемую цель (т. е. выполняем расширение в новый объект, массив или список аргументов).
  • При помощи расширения String разбивается на символы, что обычно выполняется с использованием string.split(). В качестве дополнительного преимущества можно решить, каким образом получить результат разделения: в массиве, объекте или в формате аргументов.
  • Расширение массива в качестве части вызова метода Function.call устраняет необходимость использования метода Function.apply. *Примечание: простое расширение массива в качестве части обычного вызова функции устраняет необходимость использования их обоих.
  • Чтобы поэкспериментировать с пользовательским объектом, его нужно сделать итерируемым. Об этом не стоит забывать, иначе оператор не будет работать в большинстве случаев.

Рассмотрим ряд более продвинутых и полезных примеров с оператором spread:

let array1 = [1,2,3,4]

//Копирование массива
let copyArray = [...array1]
copyArray.push(4)
console.log(array1)
console.log(copyArray)
/*
[ 1, 2, 3, 4 ]
[ 1, 2, 3, 4, 4 ]
*/

//**ПРЕДУПРЕЖДЕНИЕ*/
let otherArray = [[1], [2], [3]]
copyArray = [...otherArray]
copyArray[0][0] = 3
console.log(otherArray)
console.log(copyArray)
/*
Spread делает неглубокую копию
[ [ 3 ], [ 2 ], [ 3 ] ]
[ [ 3 ], [ 2 ], [ 3 ] ]
*/

//Array concats
let array2 = ['a', 'b', 'c']

let result = [...array1, ...array2]
console.log(result)
//[ 1, 2, 3, 4, 'a', 'b', 'c' ]


//**ПРЕДУПРЕЖДЕНИЕ */
let myString = "hello world"
let result2 = [...array1, ...myString] //totally valid but...
console.log(result2)
//[ 1, 2, 3, 4, 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd' ]

result2 = array1.concat(myString) //maybe this is what you really want
console.log(result2)
//[ 1, 2, 3, 4, 'hello world' ]

result2 = [...array1, ...array2, myString] //or this is valid too
console.log(result2)
//[ 1, 2, 3, 4, 'a', 'b', 'c', 'hello world' ]


//Слияние объектов
let myObj1 = {
    name: "Fernando Doglio",
    age: 34
}

let myObj2 = {
    name: "Fernando Doglio",
    age: 35,
    country: "Uruguay"
}

let mergedObj = {...myObj1, ...myObj2}
console.log(mergedObj)
// { name: 'Fernando Doglio', age: 35, country: 'Uruguay' }


//Удаление повторяющихся элементов из массива
let myArray3 = [1,2,3,4,4,4,4,2,3,3,4,6]
let mySet = new Set(myArray3)
myArray3 = [...mySet]
console.log(myArray3)
//[ 1, 2, 3, 4, 6 ]

Основные моменты:

  • Клонирование массива предоставляет неглубокую копию. В приведенном выше примере можно заметить, что многомерный массив клонируется не полностью. Поэтому будьте осторожны при использовании этого shortcut.
  • Объединение массивов также является мощной функцией. Однако будьте осторожны: старайтесь не заменять вызов метода concat оператором spread, поскольку они проявляют различное поведение со своими значениями. При этом расширенная версия конкатенации массивов (при правильном выполнении) гораздо более декларативна, чем версия вызова метода.
  • Объединение объектов теперь является тривиальным способом. Раньше приходилось выполнять цикл, учитывая ключи с одной стороны и значения с другой. Однако теперь с помощью одной строки кода можно объединить несколько объектов в один. Не забывайте о том, что при возникновении коллизии ключей объекты, находящиеся справа, перезаписывают предыдущее значение.
  • И, наконец, удаление повторяющихся элементов из массива. Когда Set был добавлен в язык, мы все плакали от радости (ну, по крайней мере, я так делал!). Но осознав, что метод Set.values не возвращает плоский массив, я захотел заплакать снова. Теперь больше не нужно повторять этот результат, можно просто расширить набор в массив и забыть о нем.

Мы закончили с оператором spread, и я надеюсь, что приведенные выше примеры предоставили достаточно информации для понимания его особенностей. Теперь переходим к теме деструктуризации и ее значения для синтаксиса и кода.

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

Деструктуризация — еще одна интересная функция в JavaScript . С помощью этого синтаксиса можно распаковывать значения из объектов и массивов в отдельные свойства. Несмотря на то, что деструктуризация — сама по себе полезная функция, ее также можно сочетать с оператором spread для получения более интересных результатов.

Основанные на списках функции в таких языках, как Perl или Python, предоставляют множество преимуществ. При выполнении следующих действий можно прочувствовать настоящую мощь:

a = 1
b = 2
a, b = b, a

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

В принципе, не существует простого способа написания универсальной функции, которая возвращает несколько значений без выполнения каких-либо компромиссов на синтаксическом или семантическом уровне.

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

//замена значений
let a = 1
let b = 2
[a, b] = [b, a]

//несколько возвращаемых значений
function fn() {
  return [1,2,4]
}
[a,b,c] = fn()
/*
a = 1
b = 2
c = 4
*/

С помощью нотации массива можно распаковать все имеющиеся с правой стороны значения и назначить их слева. Хотите получить первые два значения из массива, а остальные добавить в другой список? Легко!

let myList = [1,2,3,4,5,6,7]

[first, second, ...tail] = myList
/*
first = 1
second = 2
tail = [3,4,5,6,7]
*/

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

function parseURL(url) { 
  var parsedURL = /^(\w+)\:\/\/([^\/]+)\/(.*)$/.exec(url);
  if (!parsedURL) {
    return [];
  }
  [, ...parsedValues] =  parsedURL // мы игнорируем первый элемент
    return parsedValues.map( v => v.length ? v : undefined) //Мы следим за тем, чтобы пустые совпадения были определены в качестве undefined
}[protocol, host, path] = parseURL("https://www.fdoglio.com/blog")
console.log(`The host is -${host}-, the protocol -${protocol}- and you're accessing the path -${path}-`);

В приведенном выше примере деструктуризация используется в двух местах:

  1. Изначально внутри функции для удаления первого элемента в массиве совпадений. Это действие также можно выполнить с помощью parsedURL.shift(), однако мы стремимся использовать декларативный подход.
  2. Для присвоения возвращаемых значений нескольким отдельным переменным, чтобы их можно было обработать по своему усмотрению. В данном случае они используются индивидуально в строке шаблона.

Также можно установить значения по умолчанию, если правая часть не определена (undefined).

[protocol, host, path="none"] = parseURL("https://www.fdoglio.com/");
console.log(`The host is -${host}-, the protocol -${protocol}- and you're accessing the path -${path}-`);

//The host is -www.fdoglio.com-, the protocol -https- and you're accessing the path -none-

Примечание: это работает, поскольку мы вручную заменяем пустые совпадения на undefined в функции parsing, иначе значения по умолчанию будут игнорироваться.

По тем же стандартам можно использовать именованные атрибуты, переданные функциям, и даже значения по умолчанию во время вызовов функций. Например:

let myObject = {
    name: "Fernando Doglio",
    country: "Uruguay",
    age: 35
}

//destructuring
function wishHappyBirthday({name, age, numberOfKids=2}) {
    console.log(`Hello ${name} happy ${age}th birthday, have a great day with your wife and ${numberOfKids} kids`)
}

wishHappyBirthday(myObject) //расширение объекта в параметры функции

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

Убедитесь, что используете правильные названия в объявлении функции в соответствии с названиями свойств, поскольку присваивание выполняется не по порядку (как с обычными функциями), а по совпадению названий.

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

const student = {
    firstname: 'Fernando',
    lastname: 'Doglio',
    country: 'Uruguay'
};

//pull properties by name
let { firstname, lastname } = student
console.log(`Nice to meet you ${firstname} ${lastname}!`)

//присваивание свойств определенным названиям переменных
let { firstname: primerNombre, lastname: apellido} = student
console.log(primerNombre, apellido);

Первый пример довольно прост: два определенных свойства извлекаются из объекта, оставив country. Однако во втором примере показано, как переназначить содержимое свойства в новую переменную (в случае, если название уже занято или если необходимо установить больший контроль).

Заключение

Деструктуризация и оператор spread не получили широкого распространения. Я надеюсь, что вы перейдете на более декларативный стиль программирования и начнете использовать эти новые инструменты языка.

Читайте также:


Перевод статьи Fernando Doglio: Spread and Destructuring: A How-To Guide for JavaScripters