Предисловие

Я давно хотел написать статью или пост на эту тему, поскольку заметил проблему с некорректным пониманием синтаксиса так называемой «деструктуризации» в JavaScript. Эта информация будет особенно интересна разработчикам, использующим React, где деструктуризация (например, в хуке useState) встречается повсеместно.

Меня также вдохновило на столь обширное и детальное изложение недавнее видео одного JavaScript-инженера. Он очень подробно, на уровне байт-кода, продемонстрировал, как бездумное использование синтаксического сахара, основанное на наших собственных домыслах о работе JS, может снижать скорость выполнения кода. Соответственно, некоторые примеры и тезисы я буду заимствовать из этого видео для более наглядной демонстрации проблемы (я бы назвал это так), которая существует в мире JS-разработчиков.

Общепринятое понимание

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

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


// Деструктуризация объекта
const someObj = { name: 'Andrii', age: '24' };

const { name, age } = someObj;
console.log(`Name: ${name}, age: ${age}`);

// Деструктуризация массива
const arr = [ 1, 2, 3, 4, 5 ];

const [ , , el3, , el5 ] = arr;
console.log(el3, el5);

Что это на самом деле за конструкции?

Конструкция, расположенная по левую сторону в первом примере, называется не Объект, а Паттерн Присваивания Объекта (Object Assignment Pattern).

const { name, age } = someObj;

А в случае массива данная конструкция называется Паттерн Присваивания Массива (Array Assignment Pattern).

const [ , , el3, , el5 ] = arr;

Что такое Assignment (Присваивание/Связывание)

Assignment — это связывание некоторого идентификатора (переменной) с определёнными данными. Вот три простых примера Assignment:

var variable = 1;
let variable2 = 2;
const variable3 = 3;

Знак = стоит воспринимать не как «variable становится равно 1», а как «идентификатор variable связывается с данными, которые в данном случае являются числом 1».

Что такое Assignment Patterns

Паттерн связывания на основе входящих данных.

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

Посмотрим на пример:

const { name, age } = someObj;

Какие вопросы могут возникнуть:

  • Что именно здесь является паттерном?

  • О чём мы вообще говорим, когда упоминаем паттерн?

  • Чем может быть правая часть?

Рассмотрим случаи, которые могут показаться неочевидными из-за неверного понимания сути Assignment Patterns.

А что будет, если мы сделаем вот так?

const { name, age } = 1;

Результат:

  • name будет undefined.

  • age будет undefined.

Объяснение: Создаются два идентификатора, которые будут указывать на значение undefined, так как правая часть будет приведена к объекту Number. Поскольку этот объект (и его прототип) не имеет собственных свойств name или age, связывание завершится неудачей.

А если так?

const { toFixed } = 1;

Результат:

  • toFixed будет содержать функцию Number.prototype.toFixed().

Объяснение: В этом случае JavaScript также автоматически преобразует примитивное число (1) в его объектный эквивалент (new Number(1)). Объект Number наследует от Number.prototype, который содержит стандартные методы, такие как toFixed. Мы успешно связываем ссылку на этот метод с идентификатором toFixed.

А если так?

const { length } = 'abc';

Результат:

  • length будет содержать число 3.

Объяснение: Аналогично числу, примитивная строка ('abc') временно преобразуется в объект String (new String('abc')). Объект String имеет свойство length, значение которого успешно связывается с идентификатором length.

Почему это так происходит?

На самом деле Assignment Patterns — это всего лишь синтаксический сахар, очень развитый синтаксический сахар, который позволяет описать ровно то, что вы могли бы написать собственноручно, используя традиционный способ объявления идентификаторов.

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

Давайте рассмотрим это на примере:

const someObj = { name: 'Andrii', age: '24' };

Паттерн:

const { name, age } = someObj;

Который можно вручную переписать вот так:

const name = someObj.name;
const age = someObj.age;

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

Assignment Patterns: Вложенность

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

Насколько сложен этот язык?

const someObj = { user: { name: 'Andrii', age: '24' } };
const { user: { name: userName } } = someObj;

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

Если мы рассмотрим описанный паттерн, то увидим, что можем использовать вложенные паттерны для связывания данных с определённым идентификатором на разных уровнях вложенности.

Assignment Patterns: Идентификаторы

Рассмотрим примеры, которые демонстрируют, что это на самом деле мета-язык, а не просто описание объекта.

Два идентификатора для одних и тех же данных:

const someObj = { user: { name: 'Andrii', age: '24' } };
const { user: { name: userName }, user: { name: userName2 } } = someObj;

То есть мы можем сформировать Assignment Patterns таким образом, что одни и те же данные будут связаны с любым необходимым нам количеством идентификаторов.

Один и тот же идентификатор:

const { user: { name: userName }, user: { age: userName } } = someObj;

Мы можем использовать один и тот же идентификатор, и в конечном итоге наш идентификатор userName будет связан с данными '24' (значением, присвоенным последним в процессе деструктуризации).

Assignment Pattern: Промежуточные выводы

На примерах мы увидели, что Assignment Pattern — это своего рода редактор, который описывает правила связывания идентификаторов с данными.

Он позволяет перезаписывать уже существующие идентификаторы, дублировать связи и использовать вложенность.

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

const { user: { status: userName = 'boh' } } = someObj;

Если результат связывания идентификатора userName возвращает undefined, то подобная конструкция (= 'boh') приводит к тому, что наш идентификатор будет связан со значением 'boh'.

То есть, это равносильно тому, как если бы мы вручную проверили вложенность через if, и если бы status был undefined, то связали бы userName со значением 'boh'Важно отметить: это не означает какое-то дефолтное значение или что-то в этом роде, это означает, что если мы получили undefinedто мы свяжем наш идентификатор со значением, указанным справа от =.

Object и Array Assignment Pattern

Важно отметить, что Object и Array Assignment Pattern — это один и тот же механизм, за одним существенным отличием. Названия могут вводить в заблуждение, и мы далее рассмотрим, чем именно они являются на самом деле.

На основе стартовых примеров, рассмотрим, как выглядят эти паттерны:

const someObj = { name: 'Andrii', age: '24' };

const { name, age } = someObj;
console.log(`Name: ${name}, age: ${age}`);

const arr = [ 1, 2, 3, 4, 5 ];

const [ , , el3, , el5 ] = arr;
console.log(el3, el5);

До этого все примеры описывались с использованием Object Assignment Pattern, поскольку он проще для понимания. Теперь перейдём к дополнительной структуре, чтобы объяснить разницу между ними.

Object и Array Assignment Pattern: Разница

Object и Array Assignment Pattern: Разница

В чём же разница? В механизме определения того, какие данные связываются с идентификатором.

  • Object — оперирует ключом/свойством.

  • Array — оперирует итератором.

Когда мы используем Object Assignment Pattern, мы всегда оперируем тем, как ключ связывается с идентификатором (всегда ключ-идентификатор).

А Array Assignment Pattern оперирует номером итерации при выполнении соответствующего итератора. Если посмотреть на пример, то начинает казаться, что мы указываем позицию, связанную с порядком в массиве, то есть el3 просто связывается с 3-м значением или 2-м по индексу — но это не так!

const arr = [ 1, 2, 3, 4, 5 ];

const [ , , el3, , el5 ] = arr;

На самом деле, описывая данный паттерн, мы указываем номер шага итератора, который будет перебирать массив. И в тот момент, когда мы достигнем этого шага, он свяжет значение с идентификатором. То есть мы оперируем не ключами/индексами, а номером шага итератора. Просто в случае массива стандартный итератор и его шаги совпадают с позициями тех значений, которые мы ожидаем увидеть.

Array Assignment Pattern и Итератор

Это не связь по индексу в массиве, это связь с определённым шагом итерации:

const arr = [ 1, 2, 3, 4, 5 ];
const [ , , el3, , el5 ] = arr;

Это не связь по индексу в массиве, это связь с определённым шагом итерации:

function* fun1() {
  yield 1;
  yield 2;
  yield 3;
}

const [ one, two, three ] = fun1();

То есть, Array Assignment Pattern — это не про массивы, это про итераторы.

Где мы можем увидеть работу Array Assignment Pattern, исходя из того, что уже есть в JS:

const [first] = 'abc'
console.log(first) // a

const [, second] = 'abc'
console.log(second) // b

У нас есть String, который также включает в себя итератор, и мы можем написать такой код. Он показывает, что Array Assignment Pattern работает с итераторами. Просто в данном случае он использует базовый итератор, который по умолчанию работает по порядку. Но никто не запрещает нам переопределить итератор и сделать так, чтобы на первой итерации возвращался, например, последний символ или элемент.

Assignment Pattern: Комбинирование

Как следствие — комбинирование способов связывания:

const obj = { user: { name: 'Andrii', numbers: [ 1, 2, 3, 4, 5 ] } };

var { user: { numbers: [ , secondNumber ] } } = obj;

Учитывая возможность использования вложенных паттернов, мы можем комбинировать Object Assignment Pattern и Array Assignment Pattern, создавая длинные описания вложенностей. Хотя на определённом этапе это может стать нечитабельным, важно понимать, что у нас есть возможность описывать паттерн, который позволяет работать с разными типами и уровнями вложенности.

Assignment Pattern: Основные Выводы

Постепенно должно сформироваться понимание, что Assignment Pattern (AP) — это мета-механизм, позволяющий комбинировать разного рода шаблоны связывания идентификаторов с входящей структурой данных. Это подобно тому, как мы могли бы описать это пошагово, используя «традиционное» объявление и/или связывание данных с идентификатором.

То есть, любая синтаксическая конструкция связывания чего-то с чем-то может быть реализована в синтаксисе AP.

Что делает его гибким и «монстроподобным».

V8 и Подводные Камни Производительности

Давайте рассмотрим, как V8 работает с этими двумя Assignment Patterns.

Пример с Object Assignment Pattern:

const fun1 = () => {
    const arr = [ 1, 2, 3, 4 ];

    const { 0: fist, 3: last } = arr;
}

fun1();

Напишем пример Array assignment pattern

const fun2 = () => {
    const arr = [ 1, 2, 3, 4 ];
    
    const [ fist, , , last ] = arr;
}

fun2();

Если мы посмотрим на код, сгенерированный V8 (слева для Object assignment pattern, справа для Array assignment pattern):

Сгенерированный код V8
Сгенерированный код V8

Мы увидим, что объём кода, сгенерированного для Object Assignment Pattern (слева), намного меньше, чем объём кода, сгенерированного для Array Assignment Pattern (справа).

Это прекрасный пример, демонстрирующий разницу в механизмах работы между этими двумя паттернами:

  • Array Assignment Pattern создан для того, чтобы находить значения на определённом шаге итератора.

  • Object Assignment Pattern создан для того, чтобы находить значение по ключу/индексу.

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

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

Если бы мы хотели получить значение по индексу 3, то написали бы что-то вроде:

const value = arr[3]

Что как раз очень похоже на то, что делает Object Assignment Pattern (доступ по ключу).

Но вряд ли мы бы писали что-то вроде этого:

for (let index=0; index<arr.length; index++) {
	if(index===3) {
		value = arr[index];
	}
}

Что в общих чертах напоминает работу механизма Array Assignment Pattern (перебор итератора).

Именно поэтому, когда мы используем Array Assignment Pattern в тех случаях, где мы знаем индекс того, что нужно получить, мы теряем минимум около 10 процентов производительности на каждую такую конструкцию.

? Связь со мной и Мои Ресурсы

Спасибо, что дочитали до конца! Надеюсь, эта статья помогла вам взглянуть на синтаксис Assignment Patterns с более глубокой, V8-ориентированной точки зрения.

Подписывайтесь на мои ресурсы:
Telegram
Github

pproger@pproger.me

Также хотел бы оставить информацию о человеке, чьё видео вдохновило меня на написание этой статьи об Assignment Pattern. Это очень хороший специалист своего дела, рекомендую к просмотру!

Youtube — As For JS

Комментарии (13)


  1. TAZAQ
    13.11.2025 20:53

    Сначала я удивился. Я недавно видел что-то похожее у Мурыча. Подумал, что эта статья - плагиат. Но в конце статьи заметил ссылку.


    1. ppr0ger Автор
      13.11.2025 20:53

      Вообще, я давно хотел написать статью на эту тему, но всё время откладывал на потом. Однако не так давно увидел видео/стрим Мурыча, и он меня вдохновил осветить эту тему, так как он очень детально показал, что и как. И я взял его примеры за основу. Благодаря этому статья получилась довольно обширной, соответственно, я указал автора, так как многое брал оттуда как основу повествования


  1. adminNiochen
    13.11.2025 20:53

    Удали из статьи повторяющиеся абзацы (например аж два раза тебя в конце Мурыч вдохновил), а также nbsp после const из примера с кодом.


    1. ppr0ger Автор
      13.11.2025 20:53

      Спасибо большое! А то вчера выкладывал два раза и первый раз что-то залагало, и даже не сохранило в черновики. А второй раз, похоже уже пропустил вот эти косяки при переносе


  1. voraa
    13.11.2025 20:53

    А есть практический смысл присваивать массив там, где предполагается об'ект? Взяли какой то высосанный из пальца пример и доказываете, что то не эффективно. Возьмите нормальный жизненный пример деструктуризации. Слева { }, справа объект и покажите где там не эффективно.


    1. ppr0ger Автор
      13.11.2025 20:53

      1) Много кода, где людьми возвращается что угодно, как со смыслом, так и бездумно, из разных функций/методов и т. д.

      2) Суть статьи была показать разницу: хоть интуитивно и кажется понятным, что оно работает одинаково на самом деле внутри это работает по-разному.

      3) А кто говорил, что Object Assignment Pattern это неэффективно?

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

      Вообще, если честно, я не понял комментария. Суть повествования была в том чтобы привнести ясность, чтобы разработчик понимал вес того, что он пишет


  1. gabi0605
    13.11.2025 20:53

    Интересно конечно разбираться поглубже в языке, спасибо за статью. Только вот насколько это реально применимо? Вы упомянули про реакт, я как раз сейчас на него перехожу, поэтому тема очень актуальна. Сколько мы сэкономим если будем использовать useState через индексы и насколько сильно будет ругаться команда, когда такое увидит? Вопросы конечно риторические. Но поразмышлять и правда интересно, как часто мы используем что-то не зная, как это работает под капотом


    1. ppr0ger Автор
      13.11.2025 20:53

      Спасибо в первую очередь за интерес и вопрос!

      А почему это не должно быть применимо? Вопрос уже в том, хотите ли вы заботиться о производительности или нет. Если все делают что-то, это же не означает, что это верно, тем более в современном IT

      Так что здесь зависит от команды. Если вы расскажете подробно, почему, и приведете примеры, то команда выслушает вас и, возможно, изменится в лучшую сторону. Если нет, то работа на то и работа, чтобы мы писали там, как хочет команда. Но никто не запрещает писать потом для себя или для своих проектов так, как нужно

      Насчет того, действительно ли это увеличит производительность? Это зависит от проекта: если он маленький, то сильного эффекта вы не увидите. Но тут, даже логически подумать, итератор - это довольно сложная для V8 штука, которая сложно оптимизируется. Поэтому array assignment pattern по-любому медленнее. А теперь сложите все useState в том самом React вместе (до кучи) в каждом файле и подумайте, действительно ли что-то меняется или нет


  1. Fanatos
    13.11.2025 20:53

    Спасибо за статью!

    Правильно ли я понимаю в контексте useState, что условный
    const [number, setNumber] = useState(0);
    работает медленнее, чем

    const someNumber = useState(0);
    const number = someNumber[0];
    const setNumber = someNumber[1];


    1. nihil-pro
      13.11.2025 20:53

      Да, правильно. Но не совсем по той причине, что описаны в статье. Автор пишет:

      основанное на наших собственных домыслах о работе JS

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

      function useState() {
        // ...
        return [get, set];
      }

      И то как он используется:

      function Component() {
        const [state, setState] = useState()
      }

      Прочитав статью можно сделать вывод, что проблема здесь в переборе итератора, или как автор пишет:

      for (let index=0; index<arr.length; index++) {	
        if(index===3) {		
          value = arr[index];	
        }
      }

      Что в общих чертах напоминает работу механизма Array Assignment Pattern (перебор итератора).

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

      const [state, setState] = useState();
      1. Аллоцирем память под новый массив, который каждый раз попадает в случайную область памяти.

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

      return [get, set] // !!!

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


      1. Fanatos
        13.11.2025 20:53

        Спасибо за разъяснения! Вот и я подумал, что useState не совсем правильный пример, но ваш ответ помог лучше понять что творится под капотом.

        Поставил бы вам плюсик, но кто-то подпортил мне карму :)


        1. ppr0ger Автор
          13.11.2025 20:53

          Спасибо, в первую очередь, за комментарий и желание разобраться глубже!

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

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


      1. ppr0ger Автор
        13.11.2025 20:53

        Да, вы правы, это как раз каждый раз новый итератор.


        1)Суть повествования была в том, чтобы показать, что это итератор, поскольку многие даже не догадываются об этом.
        2)Второй пункт - показать что из-за того, что это итератор, и по своей природе плохо оптимизируется, он хуже по производительности чем обжект ассайн паттерн. То есть в случае если мы знаем ключ(индекс) то лучше использовать связывание по ключу


        Что касается useState, это просто самый известный пример, который многие увидят и которым заинтересуются. Здесь больше был расчёт на это