Интенсивная подготовка к JavaScript-интервью: от замыканий до асинхронности

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

1. Продвинутая работа с функциями, лексическое окружение и замыкания

Продвинутая работа с функциями, лексическое окружение и замыкания

Представьте, что вы на собеседовании, и интервьюер просит вас реализовать функцию-счетчик, которая «помнит» свое состояние между вызовами, не используя при этом глобальные переменные. Этот классический вопрос — не просто проверка навыка написания кода, а способ выяснить, понимаете ли вы, как JavaScript управляет памятью и доступом к данным. В основе этого механизма лежат две фундаментальные концепции: лексическое окружение и замыкания. Без них современный JavaScript, от React-хуков до приватных методов в модулях, просто перестал бы существовать.

Механика Lexical Environment: где живут переменные

Каждая выполняемая функция, блок кода или сам скрипт в JavaScript имеют связанный с ними внутренний объект, называемый Lexical Environment (лексическое окружение). Это не тот объект, к которому вы можете обратиться напрямую из кода, а теоретическая конструкция, используемая движком (например, V8) для управления идентификаторами.

Лексическое окружение состоит из двух частей:

  • Environment Record (запись окружения) — объект, в котором хранятся все локальные переменные, константы и функции в качестве свойств.
  • Outer Reference (ссылка на внешнее окружение) — указатель на «родительское» лексическое окружение, то есть то, которое находится снаружи текущего блока кода.
  • Когда вы пытаетесь получить доступ к переменной, JavaScript сначала ищет её в текущем Environment Record. Если не находит — переходит по ссылке Outer Reference во внешнее окружение и ищет там. Этот процесс повторяется до тех пор, пока переменная не будет найдена или пока ссылка на внешнее окружение не станет (что означает достижение глобальной области видимости).

    > Ключевой инсайт: > Область видимости в JavaScript определяется местом в коде, где переменная или функция была объявлена, а не тем, где она была вызвана. Это называется статической (или лексической) областью видимости.

    Рассмотрим пример: если у вас есть функция A, внутри которой объявлена функция B, то B всегда будет иметь доступ к переменным A, даже если вы передадите B в другой конец программы и вызовете её там. Это происходит потому, что при создании функции B она получает скрытое свойство [[Environment]], которое навсегда «приклеивает» её к тому лексическому окружению, где она родилась.

    Замыкание как «живая» связь с прошлым

    Замыкание (closure) — это комбинация функции и лексического окружения, в котором эта функция была определена. Проще говоря, это способность функции запоминать свою «родину», даже когда она выполняется вне своего исходного контекста.

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

    | Концепция | Описание | Роль в коде | | :--- | :--- | :--- | | Scope | Область видимости переменных. | Определяет доступность данных. | | Hoisting | Поднятие объявлений (var, function). | Влияет на порядок инициализации окружения. | | Closure | Функция + внешние переменные. | Позволяет инкапсулировать состояние. |

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

    Пошаговый разбор: создание инкапсулированного счетчика

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

    Шаг 1: Вызов createCounter() Движок создает новое лексическое окружение для этого вызова. В Environment Record записывается переменная count со значением . Ссылка Outer указывает на глобальное окружение.

    Шаг 2: Возврат анонимной функции Внутри createCounter создается анонимная функция. В её скрытое свойство [[Environment]] записывается ссылка на текущее окружение createCounter. Эта функция возвращается и сохраняется в переменную counter1.

    Шаг 3: Завершение createCounter() Обычно окружение функции удаляется сборщиком мусора (Garbage Collector). Но здесь переменная counter1 (в глобальном окружении) хранит ссылку на анонимную функцию, а та, в свою очередь, хранит ссылку на окружение createCounter. Окружение остается в памяти.

    Шаг 4: Первый вызов counter1() Создается новое пустое окружение для вызова counter1. Движок ищет count внутри него — не находит. Переходит по ссылке [[Environment]] в окружение createCounter, находит там count = 0, увеличивает до и возвращает.

    Шаг 5: Второй вызов counter1() Процесс повторяется. Но так как окружение createCounter — то же самое, count там уже равен . Он увеличивается до . Мы получили состояние, которое сохраняется между вызовами, но защищено от внешнего мира.

    Практическое применение и ловушки

    Замыкания — это не только счетчики. Это основа паттерна Module, который использовался повсеместно до появления ES-модулей. Оборачивая код в самовызывающуюся функцию (IIFE), разработчики создавали приватные области видимости, предотвращая конфликты имен в глобальном пространстве.

    Однако у замыканий есть и обратная сторона: риск утечек памяти. Если вы создаете тысячи функций в замыканиях и храните ссылки на них, лексические окружения не будут очищены сборщиком мусора. В современных браузерах V8 пытается оптимизировать этот процесс, удаляя из окружения те переменные, которые явно не используются во вложенных функциях, но полагаться на это на 100% не стоит.

    Частая ошибка на интервью — использование var в циклах с замыканиями.

    Многие ожидают увидеть 0, 1, 2, но увидят 3, 3, 3. Это происходит потому, что var не имеет блочной области видимости, и все три функции setTimeout ссылаются на одно и то же лексическое окружение, где i в итоге равно . Использование let решает эту проблему, так как let создает новое лексическое окружение для каждой итерации цикла.

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

    2. Эффективная итерация: современные методы массивов и трансформация объектов

    Эффективная итерация: современные методы массивов и трансформация объектов

    На техническом интервью по JavaScript вас вряд ли попросят написать обычный цикл for для обхода массива. Скорее всего, проверяющий захочет увидеть ваше умение пользоваться методами высшего порядка. Почему? Потому что декларативный подход — когда вы описываете что хотите получить, а не как это сделать по шагам — делает код менее подверженным ошибкам и более читаемым. Массивы в JS — это не просто списки данных, это мощные структуры с богатым API, понимание которого отделяет новичка от профессионала.

    Декларативность против императивности

    Императивный стиль (цикл for) требует от программиста управления индексами, условиями остановки и ручного наполнения результирующего массива. Это «ручное управление», где легко ошибиться на единицу (). Декларативные методы, такие как .map(), .filter() и .reduce(), скрывают логику итерации внутри себя.

    > Ключевой инсайт: > Методы массивов в JavaScript являются «чистыми», если они не мутируют (не изменяют) исходный массив, а возвращают новый. Это критически важно для предсказуемости кода и работы с такими библиотеками, как Redux или React.

    Рассмотрим три кита обработки данных:

  • .filter() — создает новый массив, в который попадают только те элементы, которые прошли проверку (вернули true в колбэк-функции).
  • .map() — создает новый массив той же длины, где каждый элемент является результатом трансформации исходного.
  • .reduce() — «схлопывает» массив в одно значение (число, строку, объект или даже другой массив), последовательно применяя функцию-аккумулятор.
  • Глубокое погружение в .reduce(): швейцарский нож разработчика

    Если .map и .filter интуитивно понятны, то .reduce() часто вызывает трудности. Его сигнатура выглядит так: array.reduce(callback, initialValue). Колбэк принимает четыре аргумента, но чаще всего используются два: accumulator (результат предыдущей итерации) и currentValue (текущий элемент).

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

    | Метод | Исходный массив | Результат | Типичная задача | | :--- | :--- | :--- | :--- | | map | Не меняется | Новый массив той же длины | Преобразовать prices в formattedPrices. | | filter | Не меняется | Новый массив (длина исходной) | Убрать из списка товаров те, которых нет в наличии. | | find | Не меняется | Один элемент или undefined | Найти пользователя по email. | | reduce | Не меняется | Любое значение | Подсчитать общую стоимость корзины. |

    Пошаговый разбор: группировка данных без библиотек

    Представьте, что у вас есть список транзакций, и вам нужно сгруппировать их по категориям. Это идеальный кейс для .reduce().

    Шаг 1: Инициализация Мы передаем пустой объект {} в качестве initialValue. Аккумулятор acc на старте равен {}.

    Шаг 2: Первая итерация Обрабатываем транзакцию 1 (Food, 100). В acc нет ключа 'Food', создаем его: acc['Food'] = 0. Затем прибавляем 100. Возвращаем { Food: 100 }.

    Шаг 3: Вторая итерация Обрабатываем транзакцию 2 (Transport, 50). В acc нет ключа 'Transport', создаем его. Возвращаем { Food: 100, Transport: 50 }.

    Шаг 4: Третья итерация Снова 'Food'. Ключ уже есть. Прибавляем 200 к существующим 100. Возвращаем { Food: 300, Transport: 50 }.

    Итог: Мы трансформировали массив в объект за один проход (), что гораздо эффективнее, чем сначала фильтровать по каждой категории отдельно.

    Нюансы: мутации и производительность

    Одной из самых опасных ошибок является использование методов, которые изменяют исходный массив «на месте» (in-place). К ним относятся .sort(), .reverse(), .splice() и .push(). На интервью часто дают задачу, где нужно отсортировать массив, а затем проверить исходный. Если вы использовали .sort(), исходный массив тоже изменится, что может привести к непредсказуемым багам. Чтобы этого избежать, сначала сделайте копию: [...arr].sort().

    Также стоит помнить о методах .some() и .every(). Они работают по принципу «короткого замыкания»: .some() прекратит выполнение, как только найдет первый подходящий элемент, а .every() — как только найдет первый неподходящий. Это значительно быстрее, чем прогонять весь массив через .filter(), если вам нужно просто получить логический ответ (да/нет).

    Для трансформации объектов в JavaScript используются методы Object.keys(), Object.values() и Object.entries(). Последний особенно полезен в связке с методами массивов, так как он превращает объект в массив пар [key, value], который можно отфильтровать или изменить, а затем собрать обратно в объект с помощью Object.fromEntries().

    Если из этой главы запомнить три вещи — это: всегда предпочитайте декларативные методы циклам; следите за тем, мутирует ли метод исходный массив; и используйте .reduce() для сложных трансформаций данных, где .map и .filter недостаточно.

    3. Асинхронная модель: Promise, async/await и механика Event Loop

    Асинхронная модель: Promise, async/await и механика Event Loop

    JavaScript часто называют «однопоточным», и это правда: у него всего один «рот», чтобы «есть» команды. Однако он умудряется одновременно загружать данные из сети, реагировать на клики пользователя и проигрывать анимации. Как это возможно? Секрет кроется не в магии параллелизма, а в архитектуре Event Loop (цикл событий) и механизмах обработки асинхронности. На интервью понимание Event Loop — это «золотой стандарт», отделяющий тех, кто просто пишет код, от тех, кто понимает, как он исполняется.

    Event Loop: сердце асинхронности

    Чтобы понять асинхронность, нужно представить три области памяти:

  • Call Stack (Стек вызовов) — здесь выполняются синхронные функции. Если функция попала сюда, JS не перейдет к следующей, пока текущая не завершится.
  • Web APIs (или Node APIs) — внешняя среда, где выполняются долгие задачи: таймеры (setTimeout), сетевые запросы (fetch), работа с файлами.
  • Task Queue (Очередь задач) — «зал ожидания» для колбэков, которые уже выполнились во внешней среде и готовы вернуться в основной поток.
  • Event Loop — это бесконечный цикл, который делает одну простую вещь: он смотрит на Стек вызовов. Если Стек пуст, он берет первую задачу из Очереди и перекидывает её в Стек для выполнения.

    > Ключевой инсайт: > JavaScript никогда не прерывает выполнение текущей функции. Даже если таймер сработал, его код не выполнится, пока Стек не очистится. Именно поэтому setTimeout(() => ..., 0) не означает «выполни сразу», это значит «выполни при первой возможности, когда Стек будет пуст».

    Микрозадачи и макрозадачи: приоритеты в очереди

    Не все задачи в очереди равны. Существует два типа очередей:

  • Macrotasks (Макрозадачи): setTimeout, setInterval, события ввода, рендеринг.
  • Microtasks (Микрозадачи): колбэки промисов (.then, .catch, .finally), MutationObserver.
  • Правило Event Loop гласит: после завершения каждой макрозадачи движок должен выполнить все накопившиеся микрозадачи, прежде чем переходить к следующей макрозадаче или обновлению интерфейса. Это объясняет, почему промисы кажутся «быстрее» таймеров.

    | Тип задачи | Примеры | Когда выполняется | | :--- | :--- | :--- | | Синхронный код | Прямые вызовы функций | Немедленно в Call Stack. | | Microtask | Promise.then | Сразу после текущего кода, до рендеринга. | | Macrotask | setTimeout, fetch | После очистки очереди микрозадач. |

    От колбэков к async/await

    Эволюция асинхронности в JS прошла путь от «ада колбэков» (Callback Hell) до элегантного async/await. Promise — это объект-обещание, который может находиться в трех состояниях: pending (ожидание), fulfilled (успех) или rejected (ошибка).

    async/await — это синтаксический сахар над промисами. Когда вы ставите await перед промисом, выполнение функции «замораживается», пока промис не разрешится. Но важно понимать: «замораживается» только эта конкретная функция, а не весь поток JavaScript. Движок просто выходит из функции, занимается другими делами, а когда промис готов — возвращается и продолжает выполнение с того же места.

    Пошаговый разбор: предсказание вывода кода

    Это классическая задача на интервью. Попробуйте предсказать порядок вывода:

    Шаг 1: Синхронный этап Выполняется console.log('1'). Вывод: 1. Затем встречается setTimeout. Его колбэк отправляется в Web APIs, а затем в очередь макрозадач. Встречается Promise.resolve().then(...). Колбэк отправляется в очередь микрозадач. Выполняется console.log('4'). Вывод: 4.

    Шаг 2: Очередь микрозадач Стек пуст. Event Loop проверяет очередь микрозадач. Там лежит колбэк промиса. Выполняется console.log('3'). Вывод: 3.

    Шаг 3: Очередь макрозадач Микрозадачи закончились. Event Loop берет задачу из очереди макрозадач (таймер). Выполняется console.log('2'). Вывод: 2.

    Итог: 1, 4, 3, 2. Если вы ответили правильно, вы понимаете приоритеты выполнения в JS.

    Обработка ошибок и параллелизм

    При работе с async/await критически важно использовать блоки try...catch. В отличие от цепочек .catch(), ошибки в await выбрасываются как обычные исключения. Если вы забудете обернуть асинхронный вызов, и он завершится ошибкой, это может привести к «тихому» падению логики приложения.

    Еще одна частая ошибка — последовательное выполнение там, где нужно параллельное.

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

    Если из этой главы запомнить три вещи — это: Event Loop позволяет однопоточному JS быть асинхронным; микрозадачи (промисы) всегда имеют приоритет над макрозадачами (таймерами); и async/await делает асинхронный код похожим на синхронный, но требует внимательной обработки ошибок.

    4. Взаимодействие с интерфейсом: манипуляции с DOM-деревом и делегирование событий

    Взаимодействие с интерфейсом: манипуляции с DOM-деревом и делегирование событий

    JavaScript в браузере существует ради одной главной цели — делать страницы «живыми». Все, что вы видите на экране, управляется через DOM (Document Object Model). Это древовидное представление HTML-документа, где каждый тег — это узел, с которым можно взаимодействовать. Однако прямая манипуляция DOM — операция дорогая. Понимание того, как эффективно изменять структуру страницы и управлять событиями, — ключевой навык для прохождения интервью на позицию фронтенд-разработчика.

    DOM как дерево: навигация и поиск

    Когда браузер загружает HTML, он строит дерево объектов. Важно понимать разницу между «узлами» (nodes) и «элементами» (elements). Узлы включают в себя всё: текст, комментарии, переносы строк. Элементы — это только теги. На практике мы почти всегда работаем с элементами, поэтому методы вроде children, parentElement и querySelector являются основными инструментами.

    > Ключевой инсайт: > Методы querySelector и querySelectorAll — универсальны и принимают любые CSS-селекторы. Но помните: querySelectorAll возвращает не массив, а NodeList. Это «коллекция», у которой есть метод .forEach(), но нет .map() или .filter(). Чтобы использовать методы массивов, коллекцию нужно преобразовать: [...list].

    Изменение DOM вызывает процессы Reflow (пересчет геометрии) и Repaint (перерисовка пикселей). Если вы в цикле 100 раз добавите по одному <li> в список, браузер может 100 раз пересчитать макет всей страницы. Правильный подход — использовать DocumentFragment. Это «виртуальный» контейнер, в который вы собираете все изменения в памяти, а затем одним махом добавляете в реальный DOM.

    Жизненный цикл события: погружение и всплытие

    События в JavaScript не просто происходят на элементе, они «путешествуют». Этот процесс состоит из трех фаз:

  • Capturing phase (Фаза погружения) — событие идет от window вниз к целевому элементу.
  • Target phase (Фаза цели) — событие достигло элемента, на который кликнули.
  • Bubbling phase (Фаза всплытия) — событие поднимается обратно вверх к window.
  • Почти все современные обработчики работают на фазе всплытия. Это означает, что если вы кликнули на кнопку внутри <div>, событие «всплывет» и сработает сначала на кнопке, потом на <div>, потом на <body>.

    | Фаза | Направление | Использование | | :--- | :--- | :--- | | Погружение | Сверху вниз | Редко, для перехвата до того, как событие достигнет цели. | | Всплытие | Снизу вверх | Повсеместно, основа для делегирования событий. |

    Делегирование событий: магия оптимизации

    Представьте, что у вас есть список из 1000 строк, и вы хотите, чтобы при клике на любую строку она подсвечивалась. Создавать 1000 обработчиков onclick — это катастрофа для памяти и производительности. Вместо этого мы используем делегирование.

    Мы вешаем один обработчик на родительский элемент (например, на <ul>). Благодаря всплытию, любой клик по <li> дойдет до родителя. Внутри обработчика мы проверяем event.target — это свойство всегда указывает на исходный элемент, по которому реально кликнули.

    Метод .closest() очень полезен: он ищет ближайшего предка (или сам элемент), подходящего под селектор. Это спасает, если внутри <li> есть вложенные иконки или текст — клик по ним всё равно будет корректно соотнесен с нужной строкой.

    Пошаговый разбор: динамическое создание интерфейса

    Давайте разберем процесс создания списка задач (To-Do) с точки зрения производительности.

    Шаг 1: Подготовка данных У нас есть массив строк ['Купить молоко', 'Выучить JS'].

    Шаг 2: Использование DocumentFragment Создаем фрагмент: const fragment = document.createDocumentFragment().

    Шаг 3: Итерация и создание элементов Проходим по массиву через .forEach(). Для каждого элемента создаем li через document.createElement('li'), задаем textContent (это безопаснее, чем innerHTML, так как защищает от XSS-атак) и добавляем в фрагмент через .appendChild().

    Шаг 4: Однократная вставка Находим список в DOM и добавляем в него весь фрагмент: list.appendChild(fragment). Браузер выполнит Reflow только один раз.

    Шаг 5: Навешивание логики Вместо того чтобы вешать обработчик удаления на каждую кнопку внутри li, вешаем один обработчик на list и используем делегирование для определения, по какой кнопке «Удалить» был совершен клик.

    Нюансы и «подводные камни»

    Работа с DOM требует осторожности с утечками памяти. Если вы удаляете элемент из DOM, но на него всё еще ссылается переменная в JavaScript или на него повешен обработчик событий (в старых браузерах), память не будет освобождена. Современные браузеры лучше справляются с этим, но хорошим тоном считается удалять обработчики через removeEventListener, если элемент уничтожается надолго.

    Также стоит помнить о различиях в атрибутах и свойствах. Например, element.getAttribute('value') вернет то, что написано в HTML-коде изначально, а element.value вернет текущее состояние поля ввода, которое ввел пользователь.

    Если из этой главы запомнить три вещи — это: минимизируйте количество прямых манипуляций с DOM (используйте фрагменты); используйте делегирование событий для списков и динамических элементов; и всегда предпочитайте textContent вместо innerHTML для вставки пользовательских данных.

    5. Стратегии ответов на техническом интервью: разбор типовых задач и теоретических вопросов

    Стратегии ответов на техническом интервью: разбор типовых задач и теоретических вопросов

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

    Как «продавать» свои знания теории

    Когда вас просят объяснить термин (например, «что такое замыкание»), худшее, что можно сделать — это выдать зазубренное определение из учебника. Интервьюеры ищут понимание. Используйте формулу: Определение + Механика + Применение.

    Например: «Замыкание — это функция, которая помнит свое лексическое окружение (Определение). Это происходит потому, что в JS функции хранят ссылку на внешнюю область видимости через скрытое свойство [[Environment]] (Механика). Мы используем это для создания приватных переменных или в хуках React, таких как useState (Применение)». Такой ответ показывает глубину ваших знаний.

    > Ключевой инсайт: > Если вы не знаете ответа на вопрос, не говорите просто «я не знаю». Начните рассуждать вслух. «Я не помню точный синтаксис этого метода, но, исходя из логики работы прототипов в JS, я предполагаю, что это должно работать так...». Ход мыслей часто важнее готового ответа.

    Разбор задачи на Live Coding: «Debounce и Throttle»

    Это классическая задача продвинутого уровня. Вас просят написать функцию, которая ограничивает частоту вызова другой функции (например, при вводе в поиске или скролле). Здесь проверяется всё: замыкания, this, setTimeout и работа с аргументами.

    Шаг 1: Понимание проблемы Если мы будем отправлять запрос на сервер при каждом нажатии клавиши, мы «положим» бэкенд. Нам нужно ждать, пока пользователь перестанет печатать хотя бы на 500 мс.

    Шаг 2: Использование замыкания Нам нужно где-то хранить ID таймера между вызовами функции. Глобальная переменная — плохо. Значит, возвращаем функцию из функции.

    Шаг 3: Реализация Debounce

    Шаг 4: Объяснение нюансов Почему ...args? Чтобы пробросить любые аргументы (например, объект события). Почему .apply(this, args)? Чтобы сохранить контекст вызова исходной функции. Почему clearTimeout? Это и есть магия: каждый новый вызов сбрасывает предыдущий таймер, и функция выполнится только тогда, когда наступит «затишье».

    Сравнение концепций: Таблица-выручалочка

    На интервью часто просят сравнить две технологии. Будьте готовы четко аргументировать выбор.

    | Вопрос | Ключевое различие | Когда использовать | | :--- | :--- | :--- | | var vs let/const | Область видимости (функция vs блок) и поднятие (hoisting). | Всегда const по умолчанию, let если значение меняется. Забудьте про var. | | == vs === | Приведение типов. == пытается сделать типы одинаковыми. | Всегда === (строгое равенство), чтобы избежать странных багов. | | Arrow vs Regular Function | У стрелочных функций нет своего this, arguments и prototype. | Стрелки — для колбэков и методов, где нужен контекст родителя. Обычные — для методов объектов. | | Promise vs Async/Await | Синтаксис. Async/await делает код линейным. | Async/await для чистоты кода, Promise.all для параллелизма. |

    Психология решения задач: алгоритм действий

    Когда вам дают задачу на алгоритмы или манипуляцию данными (например, «нормализовать массив объектов»), следуйте четкому плану:

  • Уточните условия. Спросите: «Могут ли быть пустые входные данные?», «Нужно ли мутировать исходный массив или вернуть новый?». Это показывает ваш профессионализм.
  • Озвучьте «грубое» решение. Скажите: «Сначала я могу решить это через вложенный цикл, это будет работать за , но потом я оптимизирую это через Map/объект до ».
  • Пишите код чисто. Используйте понятные имена переменных (user, а не u), делайте отступы.
  • Протестируйте крайние случаи. После написания кода сами проверьте его: «А что если массив будет состоять из одного элемента?».
  • Частая ловушка на интервью — вопросы про this. Помните правило: this определяется в момент вызова, а не в момент объявления (кроме стрелочных функций). Если функция вызывается как obj.method(), то this — это obj. Если просто method(), то this — это undefined (в строгом режиме) или window.

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