Изучив все о параметрах по умолчанию и операторах остатка и расширения, давайте продолжим синтаксический сахар в нашей серии Изучение ES6 с деструктурированием в ECMAScript 6.

TL;DR

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

// object pattern matching
let {lName, fName} = {fName: ‘John’, age: 15, lName: ‘Doe’};
// array pattern matching
let [first, second, third] = [8, 4, 100, -5, 20];
// output: Doe, John
console.log(lName + ‘, ‘+ fName);
// output: 100, 4, 8
console.log(third, second, first);

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

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

Некоторый фон

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

Но сначала давайте посмотрим, как мы сейчас работаем с объектами и массивами. Существует подробный способ определения объектов и массивов:

[js]
var config = new Object(),
 myArray = new Array();
config.delay = 500;
config.title = ‘Hi!’;
config.info = new Object();
// add to nested object
config.info.name = ‘Elijah’;
myArray.push(1);
myArray.push(new Array());
myArray.push(true);
// add to nested array
myArray[1].push(‘hello’);
[/js]

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

var config = {delay: 500, title: ‘Hi!’, info: {name: ‘Elijah’}},
 myArray = [1, [‘hello’], true];

Точно так же есть подробный способ получения данных из объектов и массивов:

var config = {delay: 500, title: ‘Hi!’, info: {name: ‘Elijah’}},
 KEY = ‘info’,
 delay = config.delay,
 configTitle = config[‘title’],
 info = config[KEY],
 fullName = config.name,
myArray = [1, [‘hello’], true],
 first = myArray[0],
 second = myArray[1],
 secondNest = second[0],
 third = myArray[2];

ES6 предоставляет новый синтаксис для быстрого получения значений свойств из объектов и элементов из массивов. Этот процесс называется деструктурирование присваивания, и мы потратим оставшееся время на рассмотрение примеров.

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

Присваивание деструктуризации объекта использует литеральный шаблон объекта в левой части операции присваивания. Давайте преобразуем объектную часть примера ES5 выше, используя деструктурирование ES6 (и let):

let config = {delay: 500, title: ‘Hi!’, info: {name: ‘Elijah’}},
 {delay, info, title} = config;
// output: {name: ‘Elijah’}, 500, ‘Hi!’
console.log(info, delay, title);

Мы смогли сохранить ссылки на 3 значения свойств в config в переменных с именами, которые соответствовали ключам свойств config. На самом деле это сокращенный синтаксис для деструктуризации объекта. Если вместо этого мы хотим использовать разные имена переменных, мы можем использовать полный синтаксис:

let config = {
 delay: 500,
 title: ‘Hi!’,
 info: {name: ‘Elijah’}
 },
 {
 info: one,
 title: two,
 empty: three,
 delay: four
 } = config;
// output: {name: ‘Elijah’}, ‘Hi!’, undefined, 500
// missing properties have `undefined` value
console.log(one, two, three, four);

Мы смогли использовать псевдоним для свойства info из конфигурации, назвав его one. Мы сделали то же самое для заголовка (два) и задержки (четыре). Обратите внимание, что мы также пытались получить пустое свойство из конфигурации, но, поскольку оно не существует, значение three равно undefined. Это работает точно так же, как в ES5 с точечной или квадратной нотацией.

Деструктуризация вложенных объектов

Деструктуризация также поддерживает вложенность. Итак, если мы хотим получить значение config.info.name (переменная fullName в раннем примере), мы можем сделать следующее:

let config = {delay: 500, title: ‘Hi!’, info: {name: ‘Elijah’}},
// `delay` is using shorthand syntax mixed in w/
 // full syntax
 {
 info: {name: fullName},
 delay,
 title: configTitle
 } = config;
// output: ‘Elijah’, ‘Hi!’, 500
console.log(fullName, configTitle, delay);

Шаблон деструктуризации, который мы указываем для назначения fullName, является вложенным объектом. Если бы нас устраивало, что значения config.info.name передаются в переменную с именем name, мы могли бы просто сделать: {info: {name}}. Стоит отметить, что нельзя одновременно назначать родительские и дочерние свойства. Таким образом, в этом примере вы можете либо получить свойство info или его дочерние свойства. Не оба.

Ранее мы узнали, что деструктуризация является отказоустойчивой, поэтому отсутствующие свойства приводят к неопределенным значениям. Однако это не применяется при попытке присвоить отсутствующее дочернее свойство, родительское свойство которого также отсутствует:

let options = {},
// `delay` would be `undefined`, but trying
 // to assign to `name` is an error
 // since `options.info` is already `undefined`
 {delay, info: {name}} = options;

Вычисляемые значения при деструктуризации объекта

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

const KEY = ‘empty’;
let options = {delay:500, empty:true, title:’Hi!’},
 {[KEY]: empty, delay, title} = options;
// outputs: ‘Hi!’, 500, true
console.log(title, delay, empty);

Ловушки деструктурирования объекта

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

let a,
 b = {};
// some code
( {a, b: b.count} = {a: 1, b: 2} );
// output: 1, {count: 2}
console.log(a, b);

Если вы опустите круглые скобки, вы получите SyntaxError. Это связано с тем, что без круглых скобок оператор выглядит как недопустимый блок кода для движка JavaScript. Вы также должны заметить, что в задании мы присваивали непосредственно b.count. По сути, все, что может принимать присваивание, может быть использовано в шаблоне деструктурирования.

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

Деструктуризация массива работает почти так же, как деструктуризация объекта, за исключением того, что вы используете шаблон деструктуризации литерала массива для левой операции присваивания. Как мы видели ранее, способ хранения элементов массива в переменной ES5 может быть довольно громоздким. Мы можем переписать его в ES6 следующим образом:

let myArray = [1, [‘hello’], true],
 [first, second, third] = myArray;
// output: 1, [‘hello’], true
console.log(first, second, third);

Существует только один синтаксис с деструктуризацией массива, и это сокращенный синтаксис, потому что индекс, на который будет отображаться переменная, подразумевается ее порядком в шаблоне литерала деструктуризации массива.

Деструктуризация вложенного массива

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

let myArray = [1, [‘hello’], true],
 [first, [secondNest], third] = myArray;
// output: 1, ‘hello’, true
console.log(first, secondNest, third);

Пропуск индексов при деструктуризации массива

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

let sequence = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34],
 [first, , third, fourth, , , seventh] = sequence
;
// output: 0, 1, 2, 8
console.log(first, third, fourth, seventh);

Довольно круто, да?

Смешанная деструктуризация объектов и массивов

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

let json = {
 shapes: [‘circle’, ‘square’, ‘triangle’],
 colors: 5,
 fill: true,
 author: {
 firstName: ‘Ben’,
 lastName: ‘Ilegbodu’,
 city: ‘Pittsburg’
 }
 },
 {
 fill,
 author: {lastName, firstName, city},
 shapes: [, secondShape],
 colors: numColors
 } = json;
// output: true, square, 5
console.log(fill, secondShape, numColors);
// output: Ilegbodu, Ben, Pittsburg
console.log(lastName, firstName, city);

Вот это мощная штука.

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

Смешанная деструктуризация объектов и массивов с помощью данных JSON — это реальный вариант использования. Вот еще несколько.

Замена значений

Что произойдет, если нам нужно поменять местами значения в двух переменных? В стандартном ответе нам нужна третья временная переменная:

var a = 1,
 b = 2,
 temp;
temp = a;
a = b;
b = temp;

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

let a = 1,
 b = 2;
[b, a] = [a, b];

Видишь, что мы там делали? Мы создали массив, используя синтаксис литерала массива с двумя элементами: a и b. Затем, используя деструктуризацию массива, мы присвоили первому элементу вновь созданного массива значение b, а второму элементу — значение a. В результате значения переменных поменялись местами. Технически мы по-прежнему создавали временный массив, но нам никогда не приходилось хранить его в переменной.

Деструктурирование объектов класса

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

let {
 protocol: scheme,
 host: domain,
 pathname: path,
 search: query,
 hash,
 href: url
 } = location;
// output: true
console.log(
 (scheme + ‘//’ + domain + path + query + hash) == url
);

Деструктурирование возвращаемых значений

Многие методы API-интерфейсов JavaScript возвращают массивы (например, exec для объектов RegExp или split для объектов String). Вы можете использовать деструктуризацию массива, чтобы быстро преобразовать значения массива в переменные:

let [, areaCode, exchange, lineNumber] =
 /^(\d\d\d)-(\d\d\d)-(\d\d\d\d)$/
 .exec(‘650–555–1234’);
// output: 650, 555, 1234
console.log(areaCode, exchange, lineNumber);

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

Обработка нескольких возвращаемых значений

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

Допустим, у нас есть метод find, который принимает в качестве параметров список массивов и токен строки, где он находит первую строку в списке, содержащую токен.

function find(list, token) {
 for (let i = 0; i < list.length; i++) {
 if (list[i].indexOf(token) > -1)
 return {index: i, val: list[i]};
 }
// match not found
 return {index: -1, val: undefined};
}

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

Если вызывающему абоненту нужна ссылка на обе части информации, он может сделать следующее:

let fruits = [‘apple’, ‘grape’, ‘peach’, ‘pear’],
 {index, val} = find(fruits, ‘ape’);

Или, если они просто заботятся о val:

let fruits = [‘apple’, ‘grape’, ‘peach’, ‘pear’],
 {val} = find(fruits, ‘ape’);

Или, если они заботятся только об индексе:

let fruits = [‘apple’, ‘grape’, ‘peach’, ‘pear’],
 {index} = find(fruits, ‘ape’);

Оператор деструктуризации и отдыха массива

Ранее мы узнали об операторе покоя в контексте заголовка функции. Но это работает не только с функциями. Его также можно использовать для упрощения работы с массивами.

Например, мы можем превратить это в ES5:

// ES5
var list = [9, 8, 7, 6, 5],
 first = list[0],
 second = list[1],
 rest = list.slice(2);
// output: [7, 6, 5], 8, 9
console.log(rest, second, first);

Для этого в ES6 используется оператор rest с деструктуризацией массива:

// ES6
let list = [9, 8, 7, 6, 5],
 [first, second, …rest] = list;
// output: [7, 6, 5], 8, 9
console.log(rest, second, first);

Мы можем заменить вызов среза, а также индексацию отдельного массива простым шаблоном деструктурирования.

Деструктурированные параметры

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

// ES5
function ajax(url, options) {
 var method = options.method,
 delay = options.delay,
 callback = options.callback;
console.log(url, method, delay);
 setTimeout(
 function() { callback(‘DONE!’); },
 delay
 );
}
ajax(
 ‘https://api.eventbrite.com/get',
 {
 delay: 2000,
 method: ‘POST’,
 callback: function(message) {
 console.log(message);
 }
 }
);

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

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

// ES6
function ajax(url, {method, delay, callback}) {
 // `method`, `delay` & `callback` are
 // destructured variables
console.log(url, method, delay);
 setTimeout(
 () => callback(‘DONE!’),
 delay
 );
}
ajax(
 ‘https://api.eventbrite.com/get',
 {
 delay: 2000,
 method: ‘POST’,
 callback: function(message) {
 console.log(message);
 }
 }
);

Больше не нужно объявлять и назначать отдельные переменные метода, задержки и обратного вызова.

Что, если мы хотим, чтобы опции были необязательными? Что ж, в ES5 реализация должна начинаться с:

// ES5
function ajax(url, options) {
 // default `options` to empty object
 // so var declarations don’t throw error
 // if `options` is `undefined`
 options = options || {};
// var declarations and
 // rest of the function…
}

Ну угадайте что? Мы можем использовать параметры по умолчанию, чтобы просто использовать параметры по умолчанию для пустого объекта в ES6:

// ES6
function ajax(url, {method, delay, callback}={}) {
 // default {} is used to allow
 // object to be unspecified w/o
 // causing an error
// rest of the function
}

Видишь, что мы там делали? Мы объединили значения по умолчанию с деструктуризацией параметров. Если параметры не указаны или не определены, будет активировано значение по умолчанию {}, которое затем деструктурируется в метод, задержку и обратный вызов. Если бы мы не предоставили значение по умолчанию и не указали параметры в вызове ajax, мы получили бы ошибку, потому что пытаемся деструктурировать undefined. Хорошей практикой является указание значения по умолчанию для деструктурированных параметров объекта.

Хорошо, а что, если мы хотим иметь значения по умолчанию для метода и задержки? Они находятся в пределах опций. В ES5 мы бы сделали что-то вроде:

// ES5
function ajax(url, options) {
 options = options || {};
// default the values of `method` and
 // `delay`
 var method = options.method || ‘GET’,
 delay = options.delay || 1000,
 callback = options.callback;
// rest of the function…
}

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

// ES6
function ajax(url, {method=’GET’, delay=1000, callback} = {}) {
 // default values w/in destructure pattern
// rest of the function
}

Вложенные значения по умолчанию! Круто, да? В этом сила ES6.

Поддержка движка JavaScript

Согласно Таблице совместимости ECMAScript 6, следующие движки JavaScript поддерживают деструктурирование:

  • Вавилон
  • Трейсер
  • Машинопись
  • Firefox (пока не поддерживает значения по умолчанию в шаблонах деструктурирования)
  • Safari (частичная поддержка)

Удивительно, что Chrome еще не поддерживает деструктурирование. Как и Node.js. Это также первая рассмотренная нами функция, которую Microsoft Edge также не поддерживает. Я предполагаю, что эта функция может иметь низкий приоритет для этих поставщиков, поскольку это в основном 100% синтаксический сахар. Но это такая замечательная функция!

Дополнительные ресурсы

Вы можете посетить Learning ES6 страницу примеров для Learning ES6 репозитория Github, где вы найдете весь код, использованный в этой статье, изначально работающий в браузере. (для тех, кто поддерживает деструктурирование). Есть также примеры, проходящие транспиляцию Babel и Traceur.

Вы также можете практиковать все, чему научились на ES6 Katas. Он использует подход TDD (разработка через тестирование), чтобы вы могли реализовать функции ES6, чтобы все тесты прошли. Я очень рекомендую это!

Другие суперполезные ресурсы:

— Бен Илегбоду, старший инженер-программист Eventbrite