ES6 в деталях: деструктурирование

ES6 в деталях — это цикл статей о новых возможностях языка программирования JavaScript, появившихся в 6-й редакции стандарта ECMAScript, кратко — ES6.

Примечание редактора: более ранняя версия сегодняшней статьи от разработчика Firefox Developer Tools Ника Фицджеральда (Nick Fitzgerald) изначально появилась в блоге Ника под названием «Деструктурирующее присваивание в ES6».

Что такое деструктурирующее присваивание?

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

Без деструктурирующего присваивания вы можете обратиться к первым трём элементам массива вот так:

var first = someArray[0];
var second = someArray[1];
var third = someArray[2];

С деструктурирующим присваиванием эквивалентный код становится более лаконичным и читаемым:

var [first, second, third] = someArray;

В SpiderMonkey (движок JavaScript в Firefox) уже поддерживается большая часть того, что связано с деструктурированием, но пока ещё не всё. Следить за поддержкой деструктурирования (и ES6 в целом) в SpiderMonkey можно в баге 694100.

Деструктурирование массивов и итерируемых объектов

Мы уже видели выше пример того, как деструктурирующее присваивание работает с массивами. В общем виде синтаксис такой:

[ переменная1, переменная2, ..., переменнаяN ] = массив;

Это всего лишь присвоит переменным от переменная1 до переменнаяN соответствующие элементы массива. Если вы хотите, в то же время, определить переменные, вы можете добавить перед присваиванием var, let или const:

var [ переменная1, переменная2, ..., переменнаяN ] = массив;
let [ переменная1, переменная2, ..., переменнаяN ] = массив;
const [ переменная1, переменная2, ..., переменнаяN ] = массив;

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

var [foo, [[bar], baz]] = [1, [[2], 3]];
console.log(foo);
// 1
console.log(bar);
// 2
console.log(baz);
// 3

Более того, можно пропускать элементы массива при деструктурировании:

var [,,third] = ["foo", "bar", "baz"];
console.log(third);
// "baz"

А ещё можно собрать все элементы в хвосте массива используя «остаточный» шаблон:

var [head, ...tail] = [1, 2, 3, 4];
console.log(tail);
// [2, 3, 4]

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

console.log([][0]);
// undefined

var [missing] = [];
console.log(missing);
// undefined

Обратите внимание, деструктурирующее присваивание с шаблоном массива также работает и с любым итерируемым объектом:

function* fibs() {
  var a = 0;
  var b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

var [first, second, third, fourth, fifth, sixth] = fibs();
console.log(sixth);
// 5

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

Деструктурирование объектов позволяет сопоставлять переменным различные свойства объектов. Вы указываете сопоставляемое свойство, а затем переменную, с которой оно сопоставляется.

var robotA = { name: "Бендер" };
var robotB = { name: "Флексо" };

var { name: nameA } = robotA;
var { name: nameB } = robotB;

console.log(nameA);
// "Бендер"
console.log(nameB);
// "Флексо"

Есть ещё полезная синтаксическая конструкция на случай, когда имена свойства и переменной совпадают:

var { foo, bar } = { foo: "lorem", bar: "ipsum" };
console.log(foo);
// "lorem"
console.log(bar);
// "ipsum"

И точно так же, как и с массивами, деструктурирования можно вкладывать и совмещать:

var complicatedObj = {
  arrayProp: [
    "Зепп",
    { second: "Бранниган" }
  ]
};

var { arrayProp: [first, { second }] } = complicatedObj;

console.log(first);
// "Зепп"
console.log(second);
// "Бранниган"

Если при деструктурировании вы обратитесь к свойствам, которые не определены, вы получите undefined:

var { missing } = {};
console.log(missing);
// undefined

С деструктурированием объекта, когда оно используется только для присваивания переменных, но не для их объявления (т.е., когда нет let, const или var), может быть связана одна ошибка, и вам следует об этом знать:

{ blowUp } = { blowUp: 10 };
// Ошибка синтаксиса

Это происходит потому, что грамматика JavaScript указывает движку парсить любую конструкцию, начинающуюся с {, как блок (например, { console } — это вполне допустимый блок кода). Решением может стать оборачивание всего выражения в скобки:

({ safe } = {});
// Нет ошибок

Деструктурирование значений, не являющихся объектом, массивом или итерируемым объектом

Если вы попробуете деструктурировать null или undefined, вы получите ошибку о неподходящем типе:

var {blowUp} = null;
// TypeError: у null нет свойств

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

var {wtf} = NaN;
console.log(wtf);
// undefined

Такое поведение может показаться неожиданным, но после дальнейшего изучения причина окажется простой. При использовании шаблона деструктурирования, деструктурируемое значение должно приводиться к объекту. Большинство типов могут быть преобразованы в объект, но null и undefined так преобразовать нельзя. Если вы используете шаблон массива для присваивания, то значение должно иметь итератор.

Значения по умолчанию

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

var [missing = true] = [];
console.log(missing);
// true

var { message: msg = "Что-то пошло не так" } = {};
console.log(msg);
// "Что-то пошло не так"

var { x = 3 } = {};
console.log(x);
// 3

(Примечание редактора: эта функциональность реализована в Firefox только для первых двух примеров, но не для третьего. См. баг 932080.)

Прикладное применение деструктурирования

Определение параметров функций

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

function removeBreakpoint({ url, line, column }) {
  // ...
}

Это упрощённый пример настоящего, работающего кода из отладчика JavaScript в Firefox DevTools (который в свою очередь сам написан JavaScript. Yo Dawg!) Нам такой подход кажется особенно удобным.

Параметры с объектами-конфигурациями

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

jQuery.ajax = function (url, {
  async = true,
  beforeSend = noop,
  cache = true,
  complete = noop,
  crossDomain = false,
  global = true,
  // ... больше настроек
}) {
  // ... делаем что-то полезное
};

Это позволяет избежать повторения var foo = config.foo || theDefaultFoo; для каждого свойства объекта-конфигурации в отдельности.

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

Использование с протоколом итераторов из ES6

ECMAScript 6 также определяет протокол для работы с итераторами, о котором мы уже говорили ранее в этом цикле статей. Когда вы итерируете Map (дополнение ES6 к стандартной библиотеке), вы получаете набор пар [ключ, значение]. Можно деструктурировать эти пары, чтобы было удобнее работать как с ключом, так и со значением:

var map = new Map();
map.set(window, "глобальный объект");
map.set(document, "документ");

for (var [key, value] of map) {
  console.log(key + " — это " + value);
}
// "[object Window] — это глобальный объект"
// "[object HTMLDocument] — это документ"

Перебираем только ключи:

for (var [key] of map) {
  // ...
}

Или перебираем только значения:

for (var [,value] of map) {
  // ...
}

Возврат нескольких значений

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

function returnMultipleValues() {
  return [1, 2];
}
var [foo, bar] = returnMultipleValues();

Или же вы можете использовать объект в качестве контейнера и обращаться к возвращаемым значениям по именам:

function returnMultipleValues() {
  return {
    foo: 1,
    bar: 2
  };
}
var { foo, bar } = returnMultipleValues();

Оба эти подхода гораздо лучше, чем использование временной переменной:

function returnMultipleValues() {
  return {
    foo: 1,
    bar: 2
  };
}
var temp = returnMultipleValues();
var foo = temp.foo;
var bar = temp.bar;

Или использование стиля передачи продолжений:

function returnMultipleValues(k) {
  k(1, 2);
}
returnMultipleValues((foo, bar) => ...);

Импортирование имён из модуля CommonJS

Ещё не пользуетесь модулями ES6? Всё ещё применяете модули CommonJS? Без проблем! При импорте какого-нибудь модуля X в формате CommonJS зачастую оказывается, что модуль X экспортирует больше функций, чем вы собираетесь использовать. С деструктурированием вы можете явно указать, какие части модуля вам хотелось бы использовать, и таким образом избежать захламления пространства имён:

const { SourceMapConsumer, SourceNode } = require("source-map");

(А если вы пользуетесь модулями ES6, вы уже знаете, что похожим синтаксисом можно пользоваться в декларациях import.)

Заключение

Итак, как вы видите, деструктурирование полезно для множества мелких задач. У нас в Mozilla в этом отношении большой опыт. Ларс Хансен (Lars Hansen) ввёл в JS деструктурирование в Opera десять лет назад, а чуть позже Брендан Айк (Brendan Eich) добавил поддержку в Firefox. Она была в Firefox 2. Так что мы знаем, как деструктурирование проникает в повседневное пользование языком, незаметно делая код немного короче и чище.

Пять недель назад мы сказали, что ES6 изменит то, как вы пишете на JavaScript. Именно такие возможности мы и имели в виду, простые улучшения, которым можно научиться за один раз. А будучи собранными вместе, они повлияют на каждый проект, над которым вы работаете. Революция средствами эволюции.

Обновлением деструктурирования до совместимости с ES6 занималась вся команда. Особое спасибо Tooru Fujisawa (arai) и Arpad Borsos (Swatinem) за их выдающийся вклад.

Поддержка деструктурирования в процессе разработки в Chrome, и в других браузерах, без всякого сомнения, она тоже скоро будет. А пока что нужно использовать Babel или Traceur, если вы хотите пользоваться деструктурированием в вебе.


Ещё раз спасибо Нику Фицджеральду за статью этой недели.

На следующей неделе мы рассмотрим возможность, которая ни больше ни меньше, чем более короткий способ записывать кое-что, что в JS уже есть. Что-то, что было одним из краеугольных камней языка всё это время. Заинтересовались? У вас может вызвать восхищение более короткий синтаксис? Я могу с уверенностью сказать, что ответ будет «да», но не верьте мне на слово. Присоединяйтесь на следующей неделе и узнайте сами, мы рассмотрим стрелочные функции ES6 в деталях.

Джейсон Орендорфф (Jason Orendorff)

Редактор ES6 In Depth