Каверзные задачи по JavaScript на собеседовании: разбор с ответами

Каверзные задачи по JavaScript на собеседовании: разбор с ответами Полезное

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

Ниже шесть задач в стиле собеседований крупных компаний. Это не «официальный список вопросов Google»: компании свои наборы не публикуют и регулярно меняют. Но механизмы, на которых построены задачи, спрашивают почти везде. Весь код проверен в Node.js 24 LTS (v24.21.0) 23.09.2026; тексты ошибок — из движка V8 (Node.js и Chrome), в Firefox формулировки другие.

Как решать любую задачу из этой статьи на интервью:

  1. Прочитать код и произнести вслух, какой вывод ожидаешь.
  2. Назвать механизм, от которого зависит ответ.
  3. Объяснить, почему «очевидный» ответ неверен.
  4. Предложить исправление и сказать, какой вариант выбрал бы в рабочем коде.

Задача 1. setTimeout в цикле с var

Условие: функция проходит по массиву и выводит индекс и значение каждого элемента с задержкой 3 секунды. Что выведет этот код?

const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
  setTimeout(function () {
    console.log('Index: ' + i + ', element: ' + arr[i]);
  }, 3000);
}

Через 3 секунды в консоли появится четыре одинаковые строки:

Index: 4, element: undefined
Index: 4, element: undefined
Index: 4, element: undefined
Index: 4, element: undefined

Почему так: var создает одну переменную i на всю функцию (или на весь скрипт), а не на каждую итерацию. Колбэки setTimeout — замыкания: они запоминают не значение, а ссылку на эту переменную. Цикл завершается мгновенно, i к этому моменту равна 4, и через 3 секунды все четыре колбэка читают одну и ту же i = 4. Элемента arr[4] нет, поэтому undefined.

Исправление, которое стоит назвать первым, — let. У let в заголовке цикла for своя привязка на каждую итерацию, и каждый колбэк замыкает свою копию:

const arr = [10, 12, 15, 21];
for (let i = 0; i < arr.length; i++) {
  setTimeout(function () {
    console.log('Index: ' + i + ', element: ' + arr[i]);
  }, 3000);
}
Index: 0, element: 10
Index: 1, element: 12
Index: 2, element: 15
Index: 3, element: 21

Второй допустимый ответ — передать значение аргументом: setTimeout принимает дополнительные параметры после задержки и передает их в колбэк. Так работает даже с var, потому что i копируется в момент вызова setTimeout:

const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
  setTimeout(function (index) {
    console.log('Index: ' + index + ', element: ' + arr[index]);
  }, 3000, i);
}

Вывод тот же, что у варианта с let. Частый уточняющий вопрос: «А если нужна пауза 3 секунды между строками, а не одна общая?» Тогда задержку считают от индекса: 3000 * (index + 1). В исходном коде все четыре таймера ставятся почти одновременно и срабатывают вместе.

Задача 2. Поднятие объявлений (hoisting)

console.log(typeof hello);
console.log(a);
var a = 1;

function hello() {
  return 'hi';
}

try {
  console.log(b);
} catch (e) {
  console.log(e.name + ': ' + e.message);
}
let b = 2;

try {
  sayHi();
} catch (e) {
  console.log(e.name + ': ' + e.message);
}
var sayHi = function () {
  return 'hi';
};
function
undefined
ReferenceError: Cannot access 'b' before initialization
TypeError: sayHi is not a function

Поднятие (hoisting) — упрощенное название того, что движок заранее регистрирует объявления области видимости до выполнения ее кода. Четыре случая ведут себя по-разному, и именно их различие проверяют на интервью:

Объявление Что доступно до строки объявления Результат обращения
function hello() {} функция целиком можно вызывать
var a = 1 переменная без значения undefined
let b / const b переменная есть, но в «временной мертвой зоне» ReferenceError
var sayHi = function () {} только переменная sayHi undefined, вызов дает TypeError

Формулировка «let и const не поднимаются» неточна: они тоже регистрируются заранее, иначе не было бы ошибки про инициализацию, но читать их до строки объявления нельзя.

Задача 3. Чему равен this

'use strict';

const user = {
  name: 'Анна',
  greet() {
    return 'Привет, ' + this.name;
  },
};

console.log(user.greet());

const greet = user.greet;
try {
  console.log(greet());
} catch (e) {
  console.log(e.name + ': ' + e.message);
}

const bound = user.greet.bind(user);
console.log(bound());
Привет, Анна
TypeError: Cannot read properties of undefined (reading 'name')
Привет, Анна

Правило для обычных функций: this определяется способом вызова, а не местом объявления. user.greet() — вызов как метода, this = user. greet() — та же функция без объекта слева от точки, в строгом режиме this = undefined. Без 'use strict' результат зависит от среды (в браузере this станет window), поэтому в задаче режим задан явно. Модули ES и классы работают в строгом режиме всегда.

Отсюда типичная ошибка: передать user.greet колбэком в setTimeout или обработчик события — метод «отрывается» от объекта. Исправления: bind, как выше, или обертка-стрелка () => user.greet().

Второй вопрос по this — про стрелочные функции. У стрелки нет своего this, она берет его из окружающей функции:

'use strict';

const counter = {
  count: 0,
  incBroken() {
    [1, 2, 3].forEach(function () {
      this.count++;
    });
  },
  inc() {
    [1, 2, 3].forEach(() => {
      this.count++;
    });
    return this.count;
  },
};

try {
  counter.incBroken();
} catch (e) {
  console.log(e.name + ': ' + e.message);
}
console.log(counter.inc());
TypeError: Cannot read properties of undefined (reading 'count')
3

Обратная сторона: метод объекта, объявленный стрелкой (greet: () => this.name), не получит объект в this. Для методов берут обычный синтаксис, для колбэков внутри методов — стрелки.

Задача 4. == или ===

console.log(0 == '');           // true
console.log('0' == false);      // true
console.log([] == false);       // true
console.log(null == undefined); // true
console.log(null == 0);         // false
console.log(NaN == NaN);        // false
console.log(0 === '');          // false
console.log(Number.isNaN(NaN), Object.is(NaN, NaN)); // true true
console.log(Object.is(0, -0), 0 === -0);             // false true

Три оператора сравнения отвечают на разные вопросы:

Оператор Приводит типы NaN равен NaN 0 и -0
== (нестрогое) да, по правилам абстрактного сравнения нет равны
=== (строгое) нет нет равны
Object.is нет да различает

[] == false дает true, потому что оба операнда приводятся к числу: false -> 0, [] -> '' -> 0. При этом null при == равен только undefined (и самому себе), поэтому null == 0 — false. Практический ответ на интервью: в рабочем коде по умолчанию ===; x == null иногда оставляют сознательно как короткую проверку «null или undefined», но это должно быть решением команды, а не случайностью.

Задача 5. Порядок вывода: event loop

console.log('1: синхронный код');

setTimeout(() => console.log('2: setTimeout 0'), 0);

Promise.resolve().then(() => console.log('3: then'));

queueMicrotask(() => console.log('4: queueMicrotask'));

(async () => {
  console.log('5: async до await');
  await null;
  console.log('6: async после await');
})();

console.log('7: конец синхронного кода');
1: синхронный код
5: async до await
7: конец синхронного кода
3: then
4: queueMicrotask
6: async после await
2: setTimeout 0

Упрощенная модель, которой достаточно для таких задач:

  1. Сначала выполняется весь синхронный код. Тело async-функции до первого await — тоже синхронное, поэтому строка 5 идет раньше 7.
  2. Затем очередь микрозадач выполняется до конца: колбэки then, queueMicrotask, продолжения после await — в порядке постановки.
  3. Только после этого — макрозадача из таймера, даже с задержкой 0.

Граница модели: в Node.js есть еще process.nextTick. В CommonJS-скрипте его колбэки выполняются раньше промисов, а в ES-модуле (.mjs, "type": "module") — после уже поставленных микрозадач, потому что сам модуль исполняется внутри микрозадачи. Кроме того, у цикла событий Node.js несколько фаз (таймеры, ввод-вывод, setImmediate). Порядок setTimeout(..., 0) и setImmediate в главном модуле Node.js не гарантирован. Задержка у setTimeout — минимальная, а не точная.

Задача 6. Каррирование

Каррирование — преобразование функции от нескольких аргументов в цепочку функций, принимающих их по одному (или порциями): sum(1, 2, 3) -> sum(1)(2)(3). Близкое понятие — частичное применение: фиксируем часть аргументов и получаем функцию от остальных. Каррирование делает частичное применение удобным, но это разные вещи.

Задача: написать curry(fn), чтобы работали все три формы вызова.

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return (...more) => curried.apply(this, [...args, ...more]);
  };
}

const sum3 = curry((a, b, c) => a + b + c);

console.log(sum3(1)(2)(3));   // 6
console.log(sum3(1, 2)(3));   // 6
console.log(sum3(1)(2, 3));   // 6

const add10 = sum3(10);       // частичное применение
console.log(add10(5)(1));     // 16

const sumAll = curry((...nums) => nums.reduce((s, n) => s + n, 0));
console.log(sumAll(1));       // 1 - вызвалась сразу

Разбор: curried копит аргументы, пока их меньше, чем fn.length (число объявленных параметров), затем вызывает исходную функцию. Граница решения: fn.length считает только параметры до первого со значением по умолчанию или ...rest (у (a, b = 1, c) => {} длина 1), поэтому для sumAll с длиной 0 каррирования нет. На интервью это стоит назвать самому. Старый вариант с добавлением метода в Function.prototype в рабочем коде не используют: изменение встроенных прототипов ломает чужой код и библиотеки.

Если ответ на интервью не сошелся

  • Предсказал «0, 1, 2, 3», а вышло «4» четыре раза — проверь, var или let в заголовке цикла и что именно замыкает колбэк.
  • Ожидал undefined, а получил ReferenceError — обращение к let/const до объявления.
  • Cannot read properties of undefined внутри метода — метод вызван без объекта, ищи место, где его передали колбэком.
  • Таймер «обогнал» промис в ответе — перечитай порядок: синхронный код, все микрозадачи, потом таймеры.

Выводы

  • Каверзные задачи по JS проверяют механизмы исполнения: замыкания, поднятие объявлений, this, приведение типов, очередь задач.
  • Замыкание в цикле с var дает один общий i; исправление — let или передача значения аргументом.
  • this у обычной функции зависит от вызова, у стрелки — берется из окружения.
  • В рабочем коде по умолчанию ===; Object.is различает NaN и -0.
  • Порядок вывода: синхронный код, затем все микрозадачи, затем таймеры; в Node.js есть свои фазы и process.nextTick, место которого зависит от формата модуля.

Где применяется / связь с практикой

Освойте тему на практике

Эти механизмы нужны не только на интервью: потерянный this в обработчике, гонка промиса и таймера, неожиданное == — частые причины реальных багов во фронтенде и на Node.js. Разобраться глубже в устройстве языка, асинхронности и архитектуре JS-приложений можно на курсе JavaScript Developer. Professional. Попробовать формат занятий бесплатно — на открытых уроках Otus.

FAQ

Спрашивают ли в Google именно эти задачи?
Гарантировать нельзя: наборы вопросов компании не публикуют и меняют. Здесь собраны задачи на механизмы, которые встречаются на JS-интервью в разных компаниях; алгоритмические задачи — отдельный блок подготовки.

Нужно ли знать точные тексты ошибок?
Нет, важен тип ошибки и причина. Тексты зависят от движка: в V8 (Chrome, Node.js) они одни, в SpiderMonkey (Firefox) — другие.

Почему typeof null равно 'object'?
Это историческая особенность первых реализаций JavaScript, сохраненная ради совместимости. Проверять null нужно строгим сравнением x === null.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)