Руководство по деструктуризации в 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"
}
}
С ним гораздо проще работать, чем со строкой за строкой точечной нотации и выдёргиванием свойств одно за другим из любых навязанных нам неорганизованных объектов. А минусы, спросите вы? Я имею в виду… Думаю, наш код в итоге может стать немного более… ну… неважно…