Руководство по деструктуризации в JavaScript

Источник: «A guide to destructuring in JavaScript»
Если вы проводите много времени, пробираясь через современный JavaScript, то, скорее всего, встречали столько многоточий (...), что даже самый задумчивый герой ролевых игр 90-х был бы посрамлён. Я не буду винить вас за то, что вы находите их немного запутанными. Конечно, я не виню вас за то, что находите что-то в JavaScript запутанным, но я всегда считал эти многоточия уникально не интуитивными с первого взгляда. Не помогает и то, что часто сталкиваетесь с этими маленькими чудаками в контексте деструктурирующего присваивания, которое само по себе является странным синтаксисом.

Использование деструктуризации позволяет извлекать отдельные значения из массива или объекта и присваивать их набору идентификаторов без необходимости обращаться к значениям каждого элемента по старинке — по одному за раз, по индексу или ключу, как здесь:

const myArray = [ true, false, false ];
const firstElement = myArray[0];
const secondElement = myArray[1];
const thirdElement = myArray[2];

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

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

const myArray = [10, 200, 3000 ];
const [ firstElement, secondElement, thirdElement ] = myArray;
firstElement;
> 10

secondElement;
> 200

thirdElement;
> 3000

Элементы можно пропустить, используя запятую, но опуская идентификатор, как при создании разреженного массива опускается значение:

const myArray = [ "goose", "duck", "duck", "goose" ];
const [ firstElement, , , fourthElement ] = myArray;
firstElement;
> "goose"

fourthElement;
> "goose"

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

const myArray = [ "first", "second", "third" ];
const [ startElement, middleElement, endElement ] = myArray;
myArray;
> Array(3) [ "first", "second", "third" ]

Деструктуризация: не только для массивов

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

const myImage = {
"src": "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs",
"alt": "A single black pixel.",
"size": {
"width": 600,
"height": 400
}
};

Разбор этого объекта на части, конечно, не самая сложная задача для веб-разработки, но делать это по одной строке немного неудобно:

const imgContainer = document.querySelector( ".img-container" );
const src = myImage.src;
const alt = myImage.alt || "";
const width = myImage.size.width || 800;
const height = myImage.size.height || 400;

if(imgSource) {
imgContainer.innerHTML = `<img src="${ src }" alt="${ alt }" height="${ height }" width="${ width }">`;
}

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

const { alt, size, src } = myImage;
const { height, width } = size;
alt;
> "A single black pixel."

Точно так же, как присвоение этих значений идентификаторам с помощью точечной нотации, можно задать значения по умолчанию, присваиваемые, если свойство вообще отсутствует или явно содержит значение undefined:

const { size, src, alt = "" } = myImage;
const { width = 800, height = 450 } = size;

Можно сделать это ещё лаконичнее. Не нужно распаковывать вложенный объект size отдельно; его можно распаковывать одновременно.

const { src, alt = "", size: { width = 800, height = 450 } } = myImage;

В результате получаем обновлённый код:

const imgContainer = document.querySelector( ".img-container" );
const { src, alt = "", size: { width = 800, height = 450 } } = myImage;

if(imgSource) {
imgContainer.innerHTML = `<img src="${ src }" alt="${ alt }" height="${ height }" width="${ width }">`;
}

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

Так где же здесь многоточие

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

const myArray = [ false, true, false ];
const [ firstElement, ...remainingElements ] = myArray;
firstElement;
> false

remainingElements;
> Array [ true, false ]

Другой пример:

const myObject = {
"key1": "first value",
"key2": "second value",
"key3": "third value"
};
const { key1, ...otherProperties } = myObject;
key1;
> "first value"

otherProperties;
> Object { key2: "second value", key3: "third value" }

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

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

const postData = {
inputPath: './index.md',
url: '/',
lede: "This is the introduction to the post",
date: new Date(),
title: 'My Title',
postId: 25,
tags: ['tag1', 'tag2'],
body: 'This is the body of the post'
}

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

const { inputPath, url, postId, tags, ...postContent } = postData;
postContent;
> Object { lede: "This is the introduction to the post", date: Date Fri Aug 23 2024 14:05:19 GMT-0400 (Eastern Daylight Time), title: "My Title", body: "This is the body of the post" }

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

Операторы rest и spread

Rest

Чаще всего в деструктуризации встречается оператор rest (...), но, подобно нерешительному текстовому мессенджеру, JavaScript может предложить многоточие в нескольких неожиданных местах. Все эти случаи использования имеют нечто общее с тем, что вы узнали из деструктуризации: все они связаны с объединением данных в структуру данных или их распространением из неё.

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

function myFunction( firstParameter, ...remainingParameters ) {

};

Spread

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

Наиболее часто оператор spread используется для копирования и объединения массивов:

const myArray = [ 4, 5, 6 ];
const myMergedArray = [1, 2, 3, ...myArray ];
myMergedArray;
> Array(6) [ 1, 2, 3, 4, 5, 6 ]

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

const myArray = [ true, false ];
const myObject = { ...myArray };
myObject;
> Object { 0: true, 1: false }

Можно с уверенностью сказать, что вы никогда не окажетесь в ситуации, когда нужно разложить содержимое структуры данных на… ну, ни на что. Если бы попытались, скажем, ради примера в статье о многочисленных многоточиях JavaScript:

const myArray = [ 1, 2, 3 ];
...myArray;
> Uncaught SyntaxError: expected expression, got '...'

Не получилось. Но если бы я использовал тот же синтаксис внутри console.log, то использовал бы оператор spread в контексте аргумента метода console.log, так что он работает:

const myArray = [ 1, 2, 3 ];

console.log( ...myArray );
> 1 2 3

Распространение объекта / Object Spread

Использование оператора spread с объектными литералами появилось в JavaScript совсем недавно: хотя сам оператор spread был добавлен в ES6 в 2015 году, он применяется к объектным литералам только в ES2018. Оператор spread создаёт поверхностные копии объекта. То есть он распространяет значения собственных свойств — то есть все перечисляемые свойства, не наследуемые через цепочку прототипов, — в новый объект.

const oldObject = {
"key1": "first value",
"key2": "second value",
"key3": "third value"
};

const myObject = {
"key0": "zeroth value",
...oldObject
};
myObject;
> Object { key0: "zeroth value", key1: "first value", key2: "second value", key3: "third value" }

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

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

const firstObject = {
"key1" : "first value",
"key2" : "second value",
"key3" : "third value"
};

const secondObject = {
"key0" : "zeroth value",
"key1" : "another value"
};

const myObject = { ...firstObject, ...secondObject }
myObject;
> Object { key1: "another value", key2: "second value", key3: "third value", key0: "zeroth value" }

Кроме того, поскольку объект не является итерируемым, как массив или строка, контекст для распространения объекта не совсем тот же — в то время как массивы и строки могут быть распространены на объект, массив или аргументы функции, объект может быть распространён только на другой объект:

const myObject = {
"key1": "first value",
"key2": "second value",
"key3": "third value"
};
console.log( ...myObject );
> Uncaught SyntaxError: expected expression, got '...'

Собираем всё воедино

Разобравшись со всеми этими синтаксисами, нетрудно понять, как в файле скрипта может оказаться больше многоточий, чем в сообщении LiveJournal из 2005 года.

Давайте вернёмся к обоим более близким к реальности примерам, как для деструктуризации, так и для синтаксиса распространения (spread syntax): объект, содержащий информацию об изображении, которое необходимо отобразить, и объект, содержащий кучу информации об одной записи в блоге.

const apiPost = {
inputPath: './index.md',
url: '/',
lede: "This is the introduction to the post",
date: new Date(),
title: 'My Title',
postId: 25,
tags: ['tag1', 'tag2'],
body: 'This is the body of the post'
};

const apiImage = {
"src": "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs",
"alt": "...",
"size": {
"width": 600,
"height": 400
}
};

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

const { inputPath, url, postId, tags, ...postContent } = apiPost;
postContent;
> Object { lede: "This is the introduction to the post", date: Date Fri Sep 13 2024 15:39:50 GMT-0400 (Eastern Daylight Time), title: "My Title", body: "This is the body of the post" }

Теперь давайте воспользуемся функцией распространения объекта, чтобы объединить только что созданный объект postContent с объектом, содержащим информацию об изображении, но с одним дополнением: поскольку будем размещать это изображение в верхней части страницы и не хотим столкнуться с проблемами LCP, можно сделать его рендеринг с явным атрибутом loading="eager" — добавим его к объекту, представляющему данные изображения.

const myPost = {
...postContent,
"heroImg": {
"loading": "eager",
...apiImage
}
};

И в итоге получаем объект, содержащий только нужные свойства, включая быстрое добавление собственных — единое целое для всех наших потребностей в редактировании постов в блоге:

myPost;
> Object {
lede: "This is the introduction to the post",
date: Date Fri Sep 13 2024 13:05:12 GMT-0400 (Eastern Daylight Time),
title: "My Title",
body: "This is the body of the post",
heroImg: {
alt: "...",
loading: "eager",
size: Object { width: 600, height: 400 },
src: "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs"
}
}

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

Комментарии


Дополнительные материалы

Предыдущая Статья

Практические советы по доступности, которые можно применить сегодня

Следующая Статья

Новые возможности Symfony