Каверзная задача по JavaScript — это короткий фрагмент кода, где нужно предсказать вывод или найти ошибку, а правильный ответ зависит от одного механизма языка: области видимости, поднятия объявлений, значения this, приведения типов или очереди задач. Такие задачи любят давать на технических интервью, в том числе в крупных компаниях: они быстро показывают, понимает ли кандидат, как исполняется код, а не только как его писать.
Содержание
Ниже шесть задач в стиле собеседований крупных компаний. Это не «официальный список вопросов Google»: компании свои наборы не публикуют и регулярно меняют. Но механизмы, на которых построены задачи, спрашивают почти везде. Весь код проверен в Node.js 24 LTS (v24.21.0) 23.09.2026; тексты ошибок — из движка V8 (Node.js и Chrome), в Firefox формулировки другие.
Как решать любую задачу из этой статьи на интервью:
- Прочитать код и произнести вслух, какой вывод ожидаешь.
- Назвать механизм, от которого зависит ответ.
- Объяснить, почему «очевидный» ответ неверен.
- Предложить исправление и сказать, какой вариант выбрал бы в рабочем коде.
Задача 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
Упрощенная модель, которой достаточно для таких задач:
- Сначала выполняется весь синхронный код. Тело
async-функции до первогоawait— тоже синхронное, поэтому строка 5 идет раньше 7. - Затем очередь микрозадач выполняется до конца: колбэки
then,queueMicrotask, продолжения послеawait— в порядке постановки. - Только после этого — макрозадача из таймера, даже с задержкой 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.



