Коротка відповідь. Списки «топ-100 питань» не працюють, бо інтерв'юер оцінює не завчену відповідь, а розуміння. Ефективніше розібрати сім тем, з яких складається практично будь-яка JavaScript-співбесіда: типи й coercion, scope і замикання, event loop, асинхронність, структури даних, обробка помилок і live coding. Нижче – кожна тема з типовими питаннями, кодом і поясненням, чого насправді чекає інтерв'юер.

Типи й coercion: звідки беруться «дивацтва» JS

Базове питання: «які типи є в JavaScript?» Відповідь: сім примітивів (string, number, bigint, boolean, undefined, symbol, null) плюс object. Але цікаве починається далі – з неявного приведення типів:

console.log(1 + '2');    // '12'  – число приводиться до рядка
console.log('5' - 2);    // 3     – рядок приводиться до числа
console.log([] + []);    // ''    – обидва масиви → порожні рядки
console.log(0.1 + 0.2 === 0.3); // false – двійкова арифметика з плаваючою комою

Чого чекає інтерв'юер: не завчених прикладів, а правила. Оператор + за наявності рядка конкатенує, решта арифметичних операторів приводять до числа. Порівняння == виконує приведення типів, === – ні, тому в реальному коді за замовчуванням використовують строге порівняння. Деталі алгоритмів приведення – у довіднику MDN з JavaScript.

Питання-пастка: «чому typeof null === 'object'?» Це історична помилка першої реалізації мови, яку не виправили заради сумісності. Знати такі речі корисно, але важливіше показати, що ти розумієш наслідок: перевірка на null робиться через value === null, а не через typeof.

Scope і замикання

Замикання – функція разом із лексичним оточенням, у якому її створили. Класична перевірка:

function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    current: () => count,
  };
}

const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.current()); // 2

Питання «як зробити приватну змінну без класів» – саме про це: count недоступний ззовні, але живе між викликами. Якщо тема дається важко, у нас є окремий детальний розбір із питаннями-пастками: closures, scope і hoisting.

Event loop: чому Promise раніше за setTimeout

Мінімум, який треба вміти пояснити словами: синхронний код виконується в call stack; колбеки таймерів потрапляють у чергу макрозадач; .then/await – у чергу мікрозадач; мікрозадачі виконуються всі до останньої перед наступною макрозадачею.

console.log('a');
setTimeout(() => console.log('b'), 0);
Promise.resolve().then(() => console.log('c'));
console.log('d');
// a, d, c, b

Це питання ставлять майже завжди, і на ньому ж найчастіше «сипляться». Покроковий розбір механіки з трьома класичними задачами – в окремій статті про event loop у JavaScript.

Асинхронність: async/await і помилки

Типове завдання: «перепиши промісовий код на async/await і додай обробку помилок».

async function loadUser(id: string): Promise<User | null> {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return (await response.json()) as User;
  } catch (error) {
    console.error('loadUser failed:', error);
    return null;
  }
}

На що дивиться інтерв'юер: чи перевіряєш ти response.ok (fetch не reject-ить на 404), чи не ковтаєш помилку мовчки, чи розумієш різницю між послідовним await у циклі та паралельним Promise.all. Останнє – улюблене уточнення: «а якщо треба завантажити 10 користувачів одночасно?»

Масиви, Map і Set: обери структуру свідомо

Питання рівня Middle: «чим Map кращий за об'єкт і коли достатньо масиву?» Показова задача – унікальні елементи масиву. Два розв'язки:

// Варіант 1: через Set – O(n)
function uniqueWithSet(items: number[]): number[] {
  return [...new Set(items)];
}

// Варіант 2: без Set – O(n²), бо includes лінійний
function uniqueNaive(items: number[]): number[] {
  const result: number[] = [];
  for (const item of items) {
    if (!result.includes(item)) result.push(item);
  }
  return result;
}

Тест-кейси, які варто назвати вголос: порожній масив, масив без дублікатів, усі елементи однакові, NaN (Set вважає NaN рівним собі, includes – теж, а от indexOf – ні). Пояснення складності важить більше за сам код: перший варіант проходить по масиву один раз, другий на кожен елемент шукає лінійно, тому на 10 000 елементів різниця вже помітна. Глибше про складність операцій – у статті про структури даних JavaScript.

this і стрілочні функції

Друга за частотою тема після замикань. Правило коротке: у звичайній функції this визначається способом виклику; стрілочна функція свого this не має і бере його з місця, де була створена.

const user = {
  name: 'Оля',
  hiRegular() {
    console.log(`Привіт, ${this.name}`);
  },
  hiArrow: () => {
    console.log(`Привіт, ${this?.name}`);
  },
};

user.hiRegular();            // 'Привіт, Оля' – виклик через крапку
user.hiArrow();              // 'Привіт, undefined' – стрілка взяла зовнішній this

const detached = user.hiRegular;
detached();                  // 'Привіт, undefined' – втратили контекст

Питання «як полагодити detached» має три відповіді, і сильний кандидат називає всі: bind, обгортка-стрілка, або виклик через об'єкт. А питання «чому стрілка зручна в колбеках класових компонентів і таймерах» перевіряє, чи розумієш ти, що вона не втрачає зовнішній this, бо просто його не має.

Обробка помилок

Коротка рамка для відповіді: синхронні помилки ловить try/catch; помилки в промісах – .catch або try/catch навколо await; викидай екземпляри Error (а не рядки), бо вони мають stack trace; на межах системи (API-запити, парсинг JSON, робота зі сховищем) помилки очікувані, тому обробляй їх явно, а не глобальним «catch усього».

class ApiError extends Error {
  constructor(
    message: string,
    public readonly status: number
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

Питання «навіщо власний клас помилки» – перевірка на досвід: тип помилки дозволяє в одному місці відрізнити «немає мережі» від «токен протух» і відреагувати по-різному.

Live coding: код пишеться інакше, ніж розповідається

Теоретичні відповіді не рятують, коли перед тобою порожній редактор і таймер. Робочий процес на будь-якій задачі: проговори вимоги → напиши найпростішу робочу версію → назви edge cases → покращуй. Добірка типових задач із повними розборами (debounce, groupBy, flatten, EventEmitter) – у статті JavaScript live coding: задачі з розбором.

Як пояснювати складність алгоритму

Не треба бути олімпіадником – достатньо трьох чесних речень: скільки разів проходимо по даних, чи є вкладені цикли по тих самих даних, скільки додаткової пам'яті використовуємо. «Тут O(n), бо один прохід і Set з доступом за константу; пам'ять теж O(n) під Set» – цього формулювання вистачає для більшості frontend-співбесід.

Чекліст самоперевірки за рівнями

  • Junior: пояснити типи і coercion своїми словами; написати функцію із замиканням; знати різницю ==/===, let/const/var; розв'язати задачу на масиви без підказок.
  • Middle: передбачити порядок виводу з setTimeout/Promise; пояснити, коли Map/Set замість об'єкта/масиву і чому; впевнено писати async/await з обробкою помилок; оцінити складність свого рішення.
  • Senior: усе вище + аргументувати компроміси: чому тут достатньо простого рішення, де межа оптимізації, як це рішення тестувати й підтримувати.

Кожен пункт перевіряється однаково: не «читав і зрозумів», а «написав код і пояснив уголос». Якщо готуєшся системно, вбудуй ці теми в тижневий план зі статті як підготуватися до технічної співбесіди за 30 днів.