Перейти к содержанию
Фронтенд

14 вопросов по теме «Frontend: JavaScript» на собеседовании

В этом материале — 14 вопросов из русской колоды RecallDeck по теме «Frontend: JavaScript». Сначала сформулируйте короткий ответ сами, затем откройте подробный разбор и проверьте примеры, ограничения и отказные случаи.

13 мин чтения14 подробных ответовПроверено 24 августа 2026
Главная мысль

Связывайте ответ с браузером, доступностью, производительностью и пользовательским состоянием — именно там видна зрелость frontend-инженера.

Вопросы и ответы

14 подробных ответов

01

Что такое хойстинг и чем отличаются var, let и const с точки зрения области видимости и TDZ?

Короткий ответ: Хойстинг — это подъём объявлений в начало их области видимости на этапе компиляции. var поднимается и инициализируется как undefined (функциональная область видимости), а let/const поднимаются, но остаются в TDZ (temporal dead zone) до строки объявления и имеют блочную область видимости.

Подробно:

  1. var — функциональная область видимости, доступна как undefined до объявления, можно переобъявлять.
  2. let — блочная область видимости, в TDZ до объявления, переприсваивание разрешено.
  3. const — как let, но без переприсваивания (само значение объекта при этом мутабельно).
  4. TDZ — обращение к let/const до объявления бросает ReferenceError, а не возвращает undefined.
console.log(a); // undefined — var поднят
var a = 1;

console.log(b); // ReferenceError — b в TDZ
let b = 2;
Область видимости До объявления Переприсваивание
var функция undefined да
let блок TDZ → ошибка да
const блок TDZ → ошибка нет

⚠️ Частая ошибка: считать, что let/const «не поднимаются». Они поднимаются — но обращение к ним до инициализации падает из-за TDZ.

02

Что такое замыкание (closure)? Приведите полезный пример и объясните классический баг с циклом и var.

Короткий ответ: Замыкание — это функция вместе с зафиксированной ссылкой на лексическое окружение, в котором она была создана. Функция «помнит» переменные внешней области даже после того, как та завершила выполнение.

Подробно:

  1. Приватное состояние — замыкания инкапсулируют данные, недоступные снаружи.
  2. Фабрики функций — каждая возвращённая функция держит своё окружение.
  3. Классический багvar имеет функциональную область видимости, поэтому все колбэки в цикле делят одну переменную.
function counter() {
  let count = 0;            // приватная переменная
  return () => ++count;    // замыкание над count
}
const next = counter();
next(); // 1
next(); // 2
// Баг: выведет 3, 3, 3 — одна общая i
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// Фикс: let создаёт новую привязку на каждой итерации → 0, 1, 2
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}

⚠️ Частая ошибка: думать, что замыкание копирует значение переменной. Оно держит ссылку на саму переменную — значение читается в момент вызова.

03

Как определяется значение this в JavaScript? Назовите правила привязки и объясните, чем отличаются стрелочные функции.

Короткий ответ: this определяется тем, КАК функция вызвана, а не где объявлена. Есть четыре правила в порядке приоритета: new, явная привязка (call/apply/bind), вызов как метод объекта, и обычный вызов (дефолтная привязка). Стрелочные функции не имеют своего this — они берут его из окружающей лексической области.

Подробно:

  1. new bindingnew Foo(): this — новый объект.
  2. Явная привязкаfn.call(obj), fn.apply(obj, args), fn.bind(obj): this === obj.
  3. Метод объектаobj.fn(): this === obj.
  4. Дефолтная — обычный вызов: undefined в strict mode, иначе глобальный объект.
const obj = {
  val: 42,
  regular() { return this.val; },     // this = obj
  arrow: () => this?.val,             // this = внешний (не obj!)
};
obj.regular();          // 42
const f = obj.regular;
f();                    // undefined — потеряли контекст
f.call(obj);            // 42 — явная привязка

⚠️ Частая ошибка: использовать стрелочную функцию как метод объекта и ждать, что this укажет на объект. Стрелка игнорирует все правила вызова и берёт this лексически.

04

Как работают прототипы и цепочка прототипов в JavaScript? Как синтаксис class отображается на прототипы?

Короткий ответ: У каждого объекта есть скрытая ссылка [[Prototype]] на другой объект. При чтении свойства движок идёт вверх по цепочке прототипов, пока не найдёт его или не дойдёт до null. class — это синтаксический сахар: методы кладутся в Prototype.prototype, а наследование настраивает цепочку.

Подробно:

  1. Поиск свойства — сначала на самом объекте, затем по цепочке __proto__.
  2. Методы класса — живут в Class.prototype, общие для всех экземпляров (экономия памяти).
  3. extends — связывает прототипы родителя и потомка; super вызывает родительский конструктор/метод.
instance ──[[Prototype]]──► Dog.prototype ──► Animal.prototype ──► Object.prototype ──► null
   own props              speak()              eat()              toString()
class Animal { eat() { return 'eat'; } }
class Dog extends Animal { bark() { return 'woof'; } }
const d = new Dog();
d.bark();                              // 'woof' — Dog.prototype
d.eat();                               // 'eat'  — поднялись к Animal.prototype
Object.getPrototypeOf(d) === Dog.prototype; // true

⚠️ Частая ошибка: путать prototype (свойство функции-конструктора) и __proto__ (ссылка экземпляра на свой прототип). new Dog().__proto__ === Dog.prototype.

05

Как устроен event loop? Объясните разницу между макротасками и микротасками и предскажите порядок вывода setTimeout и Promise.

Короткий ответ: Event loop выполняет синхронный код в call stack, а когда стек пуст — полностью опустошает очередь микротасок (промисы, queueMicrotask), и только потом берёт ОДНУ макротаску (setTimeout, события, I/O). После каждой макротаски очередь микротасок снова опустошается целиком.

Подробно:

  1. Call stack — синхронный код; пока не пуст, очереди не трогаются.
  2. Микротаски.then/.catch/.finally, await, queueMicrotask; имеют приоритет.
  3. МакротаскиsetTimeout, setInterval, DOM-события; по одной за «тик».
┌──────────────┐
│  Call Stack  │ ← синхронный код
└──────┬───────┘
       │ стек пуст

┌──────────────────────┐  опустошить ВСЮ очередь
│  Microtask queue     │  (Promise, queueMicrotask)
└──────┬───────────────┘
       ▼ затем одна
┌──────────────────────┐
│  Macrotask queue     │  (setTimeout, события)
└──────────────────────┘
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// Вывод: 1, 4, 3, 2
// sync (1,4) → микротаска (3) → макротаска (2)

⚠️ Частая ошибка: считать, что setTimeout(fn, 0) выполнится раньше уже зарезолвленного промиса. Микротаски всегда вперёд макротасок.

06

Что такое промис и его состояния? Чем отличаются Promise.all, race, allSettled и any?

Короткий ответ: Промис — объект, представляющий будущий результат асинхронной операции, с тремя состояниями: pending, fulfilled, rejected. После перехода (settled) состояние неизменно. then обрабатывает успех, catch — ошибку, finally — оба случая. Комбинаторы по-разному агрегируют несколько промисов.

Подробно:

  1. Состоянияpendingfulfilled (с value) или rejected (с reason); переход однократный.
  2. then/catch/finally — каждый возвращает новый промис, что и даёт цепочки.
  3. Комбинаторы — выбираются по тому, нужны ли ВСЕ результаты или ПЕРВЫЙ, и как трактовать ошибки.
Метод Резолвится когда Реджектится когда
all все выполнены первый же реджект (fail-fast)
race первый settled (любой) если первый settled — реджект
allSettled все settled никогда (массив {status,...})
any первый fulfilled все реджекты → AggregateError
await Promise.all([p1, p2]);        // [r1, r2] или бросит при первой ошибке
await Promise.allSettled([p1, p2]); // [{status:'fulfilled',value}, {status:'rejected',reason}]
await Promise.any([p1, p2]);        // первый успешный

⚠️ Частая ошибка: брать Promise.all, когда нужны все результаты даже при ошибках. Один реджект отменит всё — для устойчивости берите allSettled.

07

Как обрабатывать ошибки в async/await и в чём разница между последовательным и параллельным выполнением await?

Короткий ответ: async/await — синтаксис поверх промисов; ошибки ловятся обычным try/catch, потому что отклонённый промис превращается в брошенное исключение. Несколько await подряд выполняются последовательно (сумма времён); чтобы запустить их параллельно, стартуйте промисы заранее и ждите через Promise.all.

Подробно:

  1. try/catch — оборачивает await; reject промиса = throw внутри функции.
  2. finally — для очистки независимо от исхода.
  3. Последовательно vs параллельно — независимые задачи не должны ждать друг друга.
// ❌ Последовательно: ~2с (200мс + 1800мс ожидания друг друга)
async function slow() {
  const a = await fetchA(); // ждём A
  const b = await fetchB(); // только потом стартуем B
  return [a, b];
}

// ✅ Параллельно: ~max(времён) — стартуют разом
async function fast() {
  const [a, b] = await Promise.all([fetchA(), fetchB()]);
  return [a, b];
}
try {
  const data = await fetchData();
} catch (err) {
  console.error('Упал запрос:', err);
} finally {
  hideSpinner();
}

⚠️ Частая ошибка: await-ить независимые запросы по очереди и получать суммарную задержку. Для независимых операций используйте Promise.all.

08

В чём разница между == и ===? Объясните приведение типов, truthy/falsy и типичные ловушки.

Короткий ответ: === сравнивает без приведения типов (строгое равенство), == сначала приводит операнды к общему типу (нестрогое). Из-за непредсказуемости приведений в коде почти всегда используют ===. Восемь значений считаются falsy, остальные — truthy.

Подробно:

  1. === — равны, только если совпадают и тип, и значение.
  2. == — приводит типы по сложным правилам (число ↔ строка, к примитиву и т.д.).
  3. Falsy — ровно восемь: false, 0, -0, 0n, '', null, undefined, NaN. Всё прочее truthy.
Выражение Результат Почему
0 == '' true оба → число 0
0 == '0' true '0'0
null == undefined true спецправило
null == 0 false null равен только undefined
NaN === NaN false NaN не равен ничему
[] == ![] true ![]false0, []''0

⚠️ Частая ошибка: проверять if (x == null) и удивляться. Это как раз корректный приём — ловит и null, и undefined. А вот == с числами и строками — источник багов; берите ===.

09

Что такое делегирование событий? Объясните всплытие и погружение, а также разницу stopPropagation и preventDefault.

Короткий ответ: Делегирование — это приём, когда один обработчик вешается на общего родителя и ловит события дочерних элементов на этапе всплытия. Событие сначала идёт сверху вниз (capture), достигает цели (target), затем всплывает вверх (bubble). preventDefault отменяет действие браузера по умолчанию, stopPropagation останавливает дальнейшее распространение.

Подробно:

  1. Фазы — capture (вниз) → target → bubble (вверх). По умолчанию слушатели срабатывают на bubble.
  2. Делегирование — экономит обработчики и работает для динамически добавленных элементов.
  3. preventDefault vs stopPropagation — разные вещи: одно про действие браузера, другое про маршрут события.
capture ▼                          ▲ bubble
  document                      document
    └─ ul  ← обработчик здесь      └─ ul
        └─ li (target) ───────────────┘
document.querySelector('ul').addEventListener('click', (e) => {
  const li = e.target.closest('li');
  if (li) console.log('Клик по', li.dataset.id); // ловим любой li
});
Метод Что делает
preventDefault() отменяет дефолт браузера (переход по ссылке, submit)
stopPropagation() останавливает всплытие/погружение дальше

⚠️ Частая ошибка: путать их. preventDefault НЕ останавливает всплытие, а stopPropagation НЕ отменяет действие браузера.

10

В чём разница между debounce и throttle, когда что применять и как корректно реализовать?

Короткий ответ: Debounce откладывает вызов до тех пор, пока события не прекратятся на заданный интервал — выполняется ОДИН раз после паузы. Throttle гарантирует не более одного вызова за интервал — выполняется РАВНОМЕРНО во время потока событий.

Подробно:

  1. Debounce — «дождись тишины»: автодополнение поиска, валидация поля, ресайз окна.
  2. Throttle — «не чаще, чем»: скролл, mousemove, отслеживание прогресса.
  3. Очистка таймера — для debounce обязательно сбрасывать предыдущий setTimeout.
события: ││││││  ││││││││
debounce:        ▲              ▲   (раз, после паузы)
throttle: ▲   ▲   ▲   ▲   ▲        (равномерно)
function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

function throttle(fn, limit) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= limit) {
      last = now;
      fn.apply(this, args);
    }
  };
}

⚠️ Частая ошибка: в debounce забыть clearTimeout — тогда сработает каждый отложенный вызов, и эффект исчезнет. Также не теряйте this/args — пробрасывайте через apply.

11

В чём разница между поверхностным и глубоким копированием? Сравните spread, Object.assign и structuredClone.

Короткий ответ: Поверхностная копия ({...obj}, Object.assign) дублирует только верхний уровень — вложенные объекты остаются общими по ссылке. Глубокая копия дублирует всю структуру; современный встроенный способ — structuredClone.

Подробно:

  1. Поверхностнаяspread/Object.assign: быстро, но мутация вложенного объекта затронет оригинал.
  2. structuredClone — глубокая, поддерживает Date, Map, Set, циклы; НЕ копирует функции и прототипы.
  3. JSON-трюкJSON.parse(JSON.stringify(x)) теряет undefined, функции, Date (→ строка), падает на циклах.
const orig = { a: 1, nested: { b: 2 } };
const shallow = { ...orig };
shallow.nested.b = 99;
orig.nested.b;            // 99 — общая ссылка!

const deep = structuredClone(orig);
deep.nested.b = 5;
orig.nested.b;            // 99 — независимы
Способ Глубина Date/Map/Set Циклы
spread / Object.assign поверхностная
JSON.parse(stringify) глубокая* теряет падает
structuredClone глубокая да да

⚠️ Частая ошибка: думать, что {...obj} делает глубокую копию. Вложенные объекты остаются общими — мутация затронет оригинал.

12

Какой современный синтаксис JS ожидают на собеседовании: деструктуризация, spread/rest, опциональная цепочка, нулевое слияние?

Короткий ответ: Интервьюеры ждут уверенного владения деструктуризацией (распаковка из объектов/массивов), spread/rest (...), опциональной цепочкой (?.) и оператором нулевого слияния (??). Это базовый словарь современного фронтенда.

Подробно:

  1. Деструктуризация — извлечение с дефолтами и переименованием.
  2. spread/rest... копирует/разворачивает (spread) или собирает остаток (rest).
  3. Optional chaining ?. — безопасный доступ: вернёт undefined вместо ошибки на null/undefined.
  4. Nullish coalescing ?? — дефолт только для null/undefined, в отличие от || (срабатывает на любом falsy).
const { name = 'Гость', address: { city } = {} } = user;
const merged = { ...defaults, ...overrides };  // spread
function sum(...nums) { return nums.reduce((a, b) => a + b, 0); } // rest

user?.profile?.avatar;        // undefined, а не TypeError

const count = input ?? 10;    // 0 ?? 10 → 0   (?? пропускает только null/undefined)
const bad   = input || 10;    // 0 || 10 → 10  (|| съедает валидный 0!)

⚠️ Частая ошибка: ставить || для дефолтов там, где 0, '' или false — валидные значения. || заменит их на дефолт; используйте ??.

13

Как работает управление памятью в JS и какие типичные источники утечек в SPA?

Короткий ответ: JS использует автоматическую сборку мусора по принципу достижимости: объект освобождается, когда на него нет ни одной ссылки из корней (window, стек, замыкания). Утечка возникает, когда мы случайно держим ссылки на ненужные объекты, не давая GC их собрать.

Подробно:

  1. Достижимость — современный GC (mark-and-sweep) собирает всё, до чего нельзя дойти от корней.
  2. Слушатели событий — не снятый addEventListener держит элемент и замыкание.
  3. Таймеры — незакрытые setInterval/setTimeout держат свои колбэки и замкнутые данные.
  4. Detached DOM — удалённый из дерева узел, на который осталась JS-ссылка.
  5. Замыкания и кэши — бесконтрольно растущие Map/массивы; для слабых ссылок — WeakMap/WeakSet.
// ❌ Утечка: слушатель и таймер живут после размонтирования
function mount(node) {
  const onScroll = () => render(node);
  window.addEventListener('scroll', onScroll);
  const id = setInterval(poll, 1000);

  // ✅ Очистка при размонтировании
  return () => {
    window.removeEventListener('scroll', onScroll);
    clearInterval(id);
  };
}

⚠️ Частая ошибка: в SPA при смене страниц не снимать глобальные слушатели и интервалы. Каждый маунт добавляет новый — память и CPU растут с каждым переходом.

14

Чем ESM отличается от CommonJS? Что такое статический и динамический импорт и как ESM включает tree-shaking?

Короткий ответ: ESM (import/export) — официальный стандарт модулей: статичный, асинхронный, с живыми связями между экспортом и импортом. CommonJS (require/module.exports) — синхронная система Node.js, грузящая значение в момент вызова. Статичность ESM позволяет бандлерам делать tree-shaking — выкидывать неиспользуемый код.

Подробно:

  1. Статичностьimport разбирается до выполнения, поэтому связи известны заранее.
  2. Tree-shaking — бандлер видит, какие экспорты не используются, и удаляет их (нужны именованные экспорты и отсутствие сайд-эффектов).
  3. Динамический импортimport() возвращает промис; грузит модуль по требованию (код-сплиттинг, ленивая загрузка).
ESM CommonJS
Синтаксис import/export require/module.exports
Загрузка асинхронная, статичная синхронная, в рантайме
Связи живые (live bindings) копия значения
Tree-shaking да нет
import { debounce } from './utils.js';      // статический — анализируется заранее

button.addEventListener('click', async () => {
  const { openModal } = await import('./modal.js'); // динамический — по требованию
  openModal();
});

⚠️ Частая ошибка: ждать tree-shaking при import * as everything или от CommonJS-пакетов. Импортируйте именованно из ESM-модулей, иначе бандлер не сможет отбросить лишнее.

Источники

Источники и редакционная политика

Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.

От чтения к воспроизведению

Отрепетируйте полный цикл интервью.

RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.

Начать подготовку

Продолжить подготовку

Библиотека собеседований RecallDeck

Подробные русские ответы, разборы этапов найма и планы подготовки для российского IT-рынка.

RSS