Связывайте ответ с браузером, доступностью, производительностью и пользовательским состоянием — именно там видна зрелость frontend-инженера.
Вопросы и ответы
14 подробных ответов
01Что такое хойстинг и чем отличаются var, let и const с точки зрения области видимости и TDZ?
junior
Короткий ответ: Хойстинг — это подъём объявлений в начало их области видимости на этапе компиляции. var поднимается и инициализируется как undefined (функциональная область видимости), а let/const поднимаются, но остаются в TDZ (temporal dead zone) до строки объявления и имеют блочную область видимости.
Подробно:
- var — функциональная область видимости, доступна как
undefinedдо объявления, можно переобъявлять. - let — блочная область видимости, в TDZ до объявления, переприсваивание разрешено.
- const — как
let, но без переприсваивания (само значение объекта при этом мутабельно). - 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.
middle
Короткий ответ: Замыкание — это функция вместе с зафиксированной ссылкой на лексическое окружение, в котором она была создана. Функция «помнит» переменные внешней области даже после того, как та завершила выполнение.
Подробно:
- Приватное состояние — замыкания инкапсулируют данные, недоступные снаружи.
- Фабрики функций — каждая возвращённая функция держит своё окружение.
- Классический баг —
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? Назовите правила привязки и объясните, чем отличаются стрелочные функции.
middle
Короткий ответ: this определяется тем, КАК функция вызвана, а не где объявлена. Есть четыре правила в порядке приоритета: new, явная привязка (call/apply/bind), вызов как метод объекта, и обычный вызов (дефолтная привязка). Стрелочные функции не имеют своего this — они берут его из окружающей лексической области.
Подробно:
- new binding —
new Foo():this— новый объект. - Явная привязка —
fn.call(obj),fn.apply(obj, args),fn.bind(obj):this === obj. - Метод объекта —
obj.fn():this === obj. - Дефолтная — обычный вызов:
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 отображается на прототипы?
middle
Короткий ответ: У каждого объекта есть скрытая ссылка [[Prototype]] на другой объект. При чтении свойства движок идёт вверх по цепочке прототипов, пока не найдёт его или не дойдёт до null. class — это синтаксический сахар: методы кладутся в Prototype.prototype, а наследование настраивает цепочку.
Подробно:
- Поиск свойства — сначала на самом объекте, затем по цепочке
__proto__. - Методы класса — живут в
Class.prototype, общие для всех экземпляров (экономия памяти). - 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.
middle
Короткий ответ: Event loop выполняет синхронный код в call stack, а когда стек пуст — полностью опустошает очередь микротасок (промисы, queueMicrotask), и только потом берёт ОДНУ макротаску (setTimeout, события, I/O). После каждой макротаски очередь микротасок снова опустошается целиком.
Подробно:
- Call stack — синхронный код; пока не пуст, очереди не трогаются.
- Микротаски —
.then/.catch/.finally,await,queueMicrotask; имеют приоритет. - Макротаски —
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?
middle
Короткий ответ: Промис — объект, представляющий будущий результат асинхронной операции, с тремя состояниями: pending, fulfilled, rejected. После перехода (settled) состояние неизменно. then обрабатывает успех, catch — ошибку, finally — оба случая. Комбинаторы по-разному агрегируют несколько промисов.
Подробно:
- Состояния —
pending→fulfilled(с value) илиrejected(с reason); переход однократный. - then/catch/finally — каждый возвращает новый промис, что и даёт цепочки.
- Комбинаторы — выбираются по тому, нужны ли ВСЕ результаты или ПЕРВЫЙ, и как трактовать ошибки.
| Метод | Резолвится когда | Реджектится когда |
|---|---|---|
| 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?
middle
Короткий ответ: async/await — синтаксис поверх промисов; ошибки ловятся обычным try/catch, потому что отклонённый промис превращается в брошенное исключение. Несколько await подряд выполняются последовательно (сумма времён); чтобы запустить их параллельно, стартуйте промисы заранее и ждите через Promise.all.
Подробно:
- try/catch — оборачивает
await;rejectпромиса =throwвнутри функции. - finally — для очистки независимо от исхода.
- Последовательно 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 и типичные ловушки.
junior
Короткий ответ: === сравнивает без приведения типов (строгое равенство), == сначала приводит операнды к общему типу (нестрогое). Из-за непредсказуемости приведений в коде почти всегда используют ===. Восемь значений считаются falsy, остальные — truthy.
Подробно:
- === — равны, только если совпадают и тип, и значение.
- == — приводит типы по сложным правилам (число ↔ строка, к примитиву и т.д.).
- 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 |
![]→false→0, []→''→0 |
⚠️ Частая ошибка: проверять if (x == null) и удивляться. Это как раз корректный приём — ловит и null, и undefined. А вот == с числами и строками — источник багов; берите ===.
09Что такое делегирование событий? Объясните всплытие и погружение, а также разницу stopPropagation и preventDefault.
middle
Короткий ответ: Делегирование — это приём, когда один обработчик вешается на общего родителя и ловит события дочерних элементов на этапе всплытия. Событие сначала идёт сверху вниз (capture), достигает цели (target), затем всплывает вверх (bubble). preventDefault отменяет действие браузера по умолчанию, stopPropagation останавливает дальнейшее распространение.
Подробно:
- Фазы — capture (вниз) → target → bubble (вверх). По умолчанию слушатели срабатывают на bubble.
- Делегирование — экономит обработчики и работает для динамически добавленных элементов.
- 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, когда что применять и как корректно реализовать?
senior
Короткий ответ: Debounce откладывает вызов до тех пор, пока события не прекратятся на заданный интервал — выполняется ОДИН раз после паузы. Throttle гарантирует не более одного вызова за интервал — выполняется РАВНОМЕРНО во время потока событий.
Подробно:
- Debounce — «дождись тишины»: автодополнение поиска, валидация поля, ресайз окна.
- Throttle — «не чаще, чем»: скролл, mousemove, отслеживание прогресса.
- Очистка таймера — для 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.
middle
Короткий ответ: Поверхностная копия ({...obj}, Object.assign) дублирует только верхний уровень — вложенные объекты остаются общими по ссылке. Глубокая копия дублирует всю структуру; современный встроенный способ — structuredClone.
Подробно:
- Поверхностная —
spread/Object.assign: быстро, но мутация вложенного объекта затронет оригинал. - structuredClone — глубокая, поддерживает Date, Map, Set, циклы; НЕ копирует функции и прототипы.
- 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, опциональная цепочка, нулевое слияние?
middle
Короткий ответ: Интервьюеры ждут уверенного владения деструктуризацией (распаковка из объектов/массивов), spread/rest (...), опциональной цепочкой (?.) и оператором нулевого слияния (??). Это базовый словарь современного фронтенда.
Подробно:
- Деструктуризация — извлечение с дефолтами и переименованием.
- spread/rest —
...копирует/разворачивает (spread) или собирает остаток (rest). - Optional chaining
?.— безопасный доступ: вернётundefinedвместо ошибки наnull/undefined. - 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?
senior
Короткий ответ: JS использует автоматическую сборку мусора по принципу достижимости: объект освобождается, когда на него нет ни одной ссылки из корней (window, стек, замыкания). Утечка возникает, когда мы случайно держим ссылки на ненужные объекты, не давая GC их собрать.
Подробно:
- Достижимость — современный GC (mark-and-sweep) собирает всё, до чего нельзя дойти от корней.
- Слушатели событий — не снятый
addEventListenerдержит элемент и замыкание. - Таймеры — незакрытые
setInterval/setTimeoutдержат свои колбэки и замкнутые данные. - Detached DOM — удалённый из дерева узел, на который осталась JS-ссылка.
- Замыкания и кэши — бесконтрольно растущие
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?
middle
Короткий ответ: ESM (import/export) — официальный стандарт модулей: статичный, асинхронный, с живыми связями между экспортом и импортом. CommonJS (require/module.exports) — синхронная система Node.js, грузящая значение в момент вызова. Статичность ESM позволяет бандлерам делать tree-shaking — выкидывать неиспользуемый код.
Подробно:
- Статичность —
importразбирается до выполнения, поэтому связи известны заранее. - Tree-shaking — бандлер видит, какие экспорты не используются, и удаляет их (нужны именованные экспорты и отсутствие сайд-эффектов).
- Динамический импорт —
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, архитектуру и поведенческие истории.