Связывайте ответ с браузером, доступностью, производительностью и пользовательским состоянием — именно там видна зрелость frontend-инженера.
Вопросы и ответы
45 подробных ответов
011. Типы данных: примитивы vs объекты?
junior
Короткий ответ: В JS 7 примитивных типов (string, number, boolean, null, undefined, symbol, bigint) и один ссылочный тип — объект (включая массивы, функции, даты). Примитивы иммутабельны и копируются по значению, объекты — по ссылке.
Подробно:
// Примитивы — копируются по значению
let a = 10;
let b = a;
b = 20;
console.log(a); // 10 — a не изменилось
// Объекты — копируются по ссылке
let obj1 = { x: 1 };
let obj2 = obj1;
obj2.x = 99;
console.log(obj1.x); // 99 — обе переменные указывают на один объект
// Примитивы иммутабельны: "изменить" строку нельзя, можно только создать новую
let str = "hello";
str[0] = "H";
console.log(str); // "hello" — не изменилось
// bigint — для целых чисел больше 2^53 - 1
const big = 9007199254740993n;
console.log(big + 1n); // 9007199254740994n
// symbol — уникальный идентификатор
const id = Symbol("id");
Примитивы хранят значение напрямую. У них «есть методы» благодаря автоупаковке (autoboxing): "abc".toUpperCase() временно оборачивает строку в объект-обёртку String.
⚠️ Ловушка: typeof function(){} возвращает 'function', хотя функция — это объект. А typeof [] — 'object', поэтому для массивов используйте Array.isArray([]).
022. typeof и баг с typeof null?
junior
Короткий ответ: typeof возвращает строку с именем типа. Известный баг: typeof null === 'object', хотя null — примитив.
Подробно:
typeof 42; // "number"
typeof "str"; // "string"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof Symbol(); // "symbol"
typeof 10n; // "bigint"
typeof {}; // "object"
typeof []; // "object" (!)
typeof function(){}; // "function"
typeof null; // "object" ← баг
typeof null === 'object' — историческая ошибка ещё первой версии JS (1995). В представлении значений тип хранился в младших битах; у объектов был тег 000, а у null — нулевой указатель, тоже 000. Исправить нельзя — сломается слишком много кода.
// Правильная проверка на null
function isNull(v) { return v === null; }
// Проверка «настоящего» объекта (не null, не массив)
function isPlainObject(v) {
return typeof v === "object" && v !== null && !Array.isArray(v);
}
⚠️ Ловушка: typeof undeclaredVariable НЕ кидает ReferenceError, а возвращает 'undefined'. Это единственный безопасный способ проверить необъявленную переменную.
033. null vs undefined?
junior
Короткий ответ: undefined — переменная объявлена, но значение не присвоено (по умолчанию). null — явное «пустое значение», которое программист ставит намеренно.
Подробно:
let x;
console.log(x); // undefined — не инициализирована
let y = null;
console.log(y); // null — явно «ничего»
// undefined возникает само:
function f(a) { return a; }
f(); // undefined — аргумент не передан
({}).noSuchProp; // undefined — нет свойства
[].length === 0; // длина есть, а [5] нет → arr[5] === undefined
// Сравнения
null == undefined; // true (нестрогое — спец-правило)
null === undefined; // false (разные типы)
typeof undefined; // "undefined"
typeof null; // "object"
// JSON.stringify обрабатывает их по-разному
JSON.stringify({ a: undefined, b: null }); // '{"b":null}' — undefined выпадает
Соглашение: null стоит присваивать самому, когда хотите сказать «значения нет специально»; undefined оставляйте движку.
⚠️ Ловушка: Значения по умолчанию в параметрах и деструктуризации срабатывают только для undefined, но НЕ для null:
function g(a = 5) { return a; }
g(undefined); // 5
g(null); // null — default не применился!
044. == vs ===?
middle
Короткий ответ: === (строгое) сравнивает без приведения типов — типы должны совпадать. == (нестрогое) приводит типы перед сравнением, что порождает много неочевидных результатов.
Подробно:
1 === "1"; // false — разные типы
1 == "1"; // true — строка приводится к числу
// Таблица сюрпризов ==
0 == ""; // true ("" → 0)
0 == "0"; // true ("0" → 0)
0 == false; // true (false → 0)
"" == false; // true (оба → 0)
null == undefined; // true (спец-правило)
null == 0; // false (null не приводится к числу здесь)
NaN == NaN; // false (NaN не равен ничему, даже себе)
[] == ![]; // true (см. разбор ниже)
[] == ""; // true ([] → "")
[] == 0; // true ([] → "" → 0)
[null] == 0; // true
[0] == false; // true
// Разбор [] == ![]:
// 1) ![] → false (массив truthy, отрицание → false)
// 2) [] == false
// 3) false → 0, [] → "" → 0
// 4) 0 == 0 → true
// NaN проверяют так:
Number.isNaN(NaN); // true
NaN !== NaN; // true — хак-проверка на NaN
Когда == уместен? Практически всегда используйте ===. Единственный оправданный кейс == — проверка x == null, которая одной операцией ловит и null, и undefined:
if (x == null) { /* x — null или undefined */ }
⚠️ Ловушка: == не транзитивен: "" == 0 (true) и "0" == 0 (true), но "" == "0" (false). Поэтому полагаться на == для цепочек нельзя.
055. Приведение типов и truthy/falsy?
junior
Короткий ответ: Coercion — автоматическое преобразование типов. В булевом контексте есть ровно 8 falsy-значений, всё остальное — truthy.
Подробно:
Список falsy (приводятся к false):
false
0, -0, 0n // нули
"" // пустая строка
null
undefined
NaN
Всё остальное truthy, в том числе ловушки:
Boolean("0"); // true — непустая строка
Boolean("false"); // true — непустая строка
Boolean([]); // true — пустой массив truthy!
Boolean({}); // true — пустой объект truthy!
Boolean(" "); // true — пробел непустой
Boolean(-1); // true — любое ненулевое число
Виды приведения:
// К строке (оператор +, если один из операндов строка)
1 + "2"; // "12"
"5" + 3; // "53"
// К числу (арифметика кроме +)
"5" - 1; // 4
"5" * 2; // 10
+"42"; // 42
+""; // 0
+"abc"; // NaN
// К булеву (if, ||, &&, !)
if ([]) console.log("массив truthy"); // выполнится
⚠️ Ловушка: [] + [] → "" (пустая строка), [] + {} → "[object Object]", {} + [] → 0 в некоторых контекстах (если {} трактуется как блок кода). Избегайте арифметики с объектами/массивами.
066. var vs let vs const?
junior
Короткий ответ: var — функциональная область видимости, всплытие с undefined, можно переобъявлять. let/const — блочная область, temporal dead zone, нельзя переобъявлять. const запрещает переназначение ссылки.
Подробно:
| Свойство | var |
let |
const |
|---|---|---|---|
| Область видимости | функция | блок {} |
блок {} |
| Hoisting | да, = undefined |
да, но TDZ | да, но TDZ |
| Переназначение | да | да | нет |
| Переобъявление | да | нет | нет |
| Свойство global | да (в браузере) | нет | нет |
// Блочная vs функциональная область
function scope() {
if (true) {
var v = "var";
let l = "let";
}
console.log(v); // "var" — видна вне блока
console.log(l); // ReferenceError — l не существует
}
// Temporal Dead Zone (TDZ)
console.log(a); // undefined (var всплыл)
console.log(b); // ReferenceError: Cannot access 'b' before initialization
var a = 1;
let b = 2;
// const запрещает переназначение, но не мутацию объекта
const obj = { x: 1 };
obj.x = 2; // OK — мутация содержимого
obj = {}; // TypeError — переназначение ссылки
const arr = [1];
arr.push(2); // OK
⚠️ Ловушка: const НЕ делает значение иммутабельным — он замораживает только привязку (binding). Содержимое объекта/массива по-прежнему можно менять. Для иммутабельности нужен Object.freeze.
077. Hoisting (всплытие)?
middle
Короткий ответ: Объявления переменных и функций «поднимаются» в начало своей области видимости на этапе компиляции. var инициализируется undefined, function declaration поднимается целиком, let/const попадают в TDZ.
Подробно:
// Как мы пишем // Как это видит движок
console.log(x); var x; // объявление наверх
var x = 5; console.log(x); // undefined
x = 5;
// Function declaration поднимается полностью
sayHi(); // "Привет" — работает до объявления
function sayHi() { console.log("Привет"); }
// Function expression НЕ поднимается (поднимается только переменная)
sayBye(); // TypeError: sayBye is not a function
var sayBye = function() { console.log("Пока"); };
// var sayBye всплыл как undefined → undefined() → ошибка
// let/const в TDZ
{
// TDZ для name начинается здесь
console.log(name); // ReferenceError
let name = "Анна"; // TDZ заканчивается
}
Порядок приоритета при всплытии: function declaration перекрывает var с тем же именем.
⚠️ Ловушка: Function declaration внутри блока if ведёт себя по-разному в strict/non-strict режиме и между движками. Внутри блоков объявляйте функции через let/const + arrow/expression.
088. Замыкания (closures)?
middle
Короткий ответ: Замыкание — это функция вместе с лексическим окружением, в котором она была создана. Функция «запоминает» переменные внешней области, даже когда внешняя функция уже завершилась.
Подробно:
// Счётчик — классический пример
function makeCounter() {
let count = 0; // приватная переменная
return function () {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count живёт между вызовами, хотя makeCounter завершилась
// Приватные переменные (инкапсуляция)
function createBankAccount(initial) {
let balance = initial; // недоступна снаружи напрямую
return {
deposit(sum) { balance += sum; return balance; },
getBalance() { return balance; },
};
}
const acc = createBankAccount(100);
acc.deposit(50); // 150
console.log(acc.getBalance()); // 150
console.log(acc.balance); // undefined — нет прямого доступа
// Фабрика функций
function multiplier(factor) {
return (n) => n * factor; // замыкается на factor
}
const double = multiplier(2);
console.log(double(5)); // 10
Замыкание создаётся при создании функции, а не при вызове. Каждый вызов внешней функции порождает новое независимое окружение.
⚠️ Ловушка: замыкания держат ссылки на переменные и могут удерживать большие объекты дольше необходимого. Кроме того, функции, созданные в цикле с var, захватывают одну общую переменную; после цикла все увидят её финальное значение. let создаёт отдельное лексическое связывание на каждой итерации.
099. Ловушка var в цикле и setTimeout?
middle
Короткий ответ: При var все колбэки замыкаются на одну и ту же переменную, поэтому после цикла видят её финальное значение. С let каждая итерация создаёт новую привязку.
Подробно:
// Проблема с var
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Вывод: 3, 3, 3
// var i — одна переменная на весь цикл. К моменту срабатывания
// таймеров (после цикла) i уже стало 3.
// Решение 1: let — блочная область, новая привязка на итерацию
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Вывод: 0, 1, 2
// Решение 2: IIFE, захватываем значение копией
for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(() => console.log(j), 100);
})(i);
}
// Вывод: 0, 1, 2
// Решение 3: второй аргумент setTimeout
for (var i = 0; i < 3; i++) {
setTimeout((j) => console.log(j), 100, i);
}
// Вывод: 0, 1, 2
⚠️ Ловушка: Даже с let ловушка вернётся, если объявить переменную ВНЕ цикла: let i; for (i = 0; ...). Привязка на итерацию создаётся только когда let стоит в самом заголовке for.
1010. this: 4 правила привязки?
middle
Короткий ответ: Значение this определяется в момент ВЫЗОВА функции по 4 правилам приоритета: 1) new 2) явная привязка (call/apply/bind) 3) метод объекта 4) обычный вызов (default).
Подробно:
// 1. Обычный вызов (default binding)
function show() { console.log(this); }
show();
// non-strict: глобальный объект (window/global)
// strict mode: undefined
// 2. Метод объекта (implicit binding) — this = объект слева от точки
const user = {
name: "Анна",
greet() { console.log(this.name); },
};
user.greet(); // "Анна" — this = user
// 3. new (new binding) — this = новый объект
function User(name) { this.name = name; }
const u = new User("Иван");
console.log(u.name); // "Иван"
// 4. Явная привязка (explicit binding)
function say() { console.log(this.role); }
const admin = { role: "admin" };
say.call(admin); // "admin"
say.apply(admin); // "admin"
const bound = say.bind(admin);
bound(); // "admin"
Приоритет (от высшего к низшему): new > bind/call/apply > метод объекта > обычный вызов.
Важно: this определяется НЕ местом, где функция объявлена, а тем, КАК её вызвали (кроме стрелочных).
⚠️ Ловушка: «Потеря» контекста при передаче метода как ссылки:
const greet = user.greet;
greet(); // this = undefined/window → ошибка или undefined
// Метод оторван от объекта → правило обычного вызова.
1111. call / apply / bind?
middle
Короткий ответ: Все три задают this явно. call вызывает сразу с аргументами через запятую, apply — с массивом аргументов, bind возвращает новую функцию с зафиксированным this (не вызывает).
Подробно:
function introduce(greeting, punct) {
return `${greeting}, я ${this.name}${punct}`;
}
const person = { name: "Лена" };
// call — аргументы перечислением
introduce.call(person, "Привет", "!"); // "Привет, я Лена!"
// apply — аргументы массивом
introduce.apply(person, ["Здравствуйте", "."]); // "Здравствуйте, я Лена."
// bind — возвращает новую функцию, не вызывает
const boundIntro = introduce.bind(person, "Хай");
boundIntro("?"); // "Хай, я Лена?"
// Практика: bind для сохранения this в колбэке
class Timer {
constructor() { this.seconds = 0; }
start() {
setInterval(function () {
this.seconds++; // без bind this потерян
}.bind(this), 1000);
}
}
// apply для max/min массива (до spread)
Math.max.apply(null, [1, 5, 3]); // 5
Math.max(...[1, 5, 3]); // 5 — современный аналог
// Заимствование методов
const arrayLike = { 0: "a", 1: "b", length: 2 };
Array.prototype.join.call(arrayLike, "-"); // "a-b"
Мнемоника: Apply — Array, Call — Comma.
⚠️ Ловушка: bind создаёт «жёсткую» привязку — повторный bind или даже new-вызов в общем случае не переопределит уже привязанный this (хотя new поверх bound-функции — особый случай и игнорирует привязанный this, но использует привязанные аргументы). Также bind каждый раз возвращает НОВУЮ функцию, поэтому removeEventListener с забинженным обработчиком не сработает, если бинд делался заново.
1212. Стрелочные функции vs обычные?
middle
Короткий ответ: Стрелочные не имеют собственного this (берут лексически из окружения), нет arguments, нельзя вызвать через new, нет prototype, не всплывают как объявления.
Подробно:
// this — лексический (из места объявления, не вызова)
const obj = {
name: "Том",
regular() {
setTimeout(function () {
console.log(this.name); // undefined — свой this
}, 100);
setTimeout(() => {
console.log(this.name); // "Том" — this из regular
}, 100);
},
};
// Нет своего arguments
function regular() { return arguments; } // работает
const arrow = () => arguments; // ошибка/внешний arguments
// Вместо этого — rest:
const arrowRest = (...args) => args;
// Нельзя использовать как конструктор
const Arrow = () => {};
new Arrow(); // TypeError: Arrow is not a constructor
// Нет prototype
console.log(Arrow.prototype); // undefined
// Неявный возврат
const sum = (a, b) => a + b; // без return
const makeObj = () => ({ x: 1 }); // объект в скобках!
Когда НЕ использовать стрелочные: методы объекта (нужен динамический this), методы прототипа, обработчики событий, где нужен this = element, функции-конструкторы.
⚠️ Ловушка: Метод объекта стрелкой ломает this:
const counter = {
count: 0,
inc: () => { this.count++; }, // this — внешний (window/undefined), не counter!
};
1313. Потеря this в колбэках?
middle
Короткий ответ: Когда метод передаётся как колбэк (в setTimeout, обработчик события, map), он вызывается «оторванно» от объекта и this теряется. Решения: стрелочная функция, bind, сохранение this в переменную.
Подробно:
class Button {
constructor(label) {
this.label = label;
}
// Проблема: this теряется при передаче
handleClickBroken() {
console.log(this.label); // this = element, не Button
}
// Решение 1: bind в конструкторе
// this.handleClick = this.handleClick.bind(this);
// Решение 2: стрелочное поле класса (лексический this)
handleClick = () => {
console.log(this.label); // всегда экземпляр Button
};
}
const btn = new Button("OK");
element.addEventListener("click", btn.handleClick); // работает (arrow-поле)
// В массивах: второй аргумент thisArg
const obj = {
prefix: ">> ",
items: ["a", "b"],
render() {
// forEach принимает thisArg вторым параметром
this.items.forEach(function (i) {
console.log(this.prefix + i);
}, this); // ← передаём this
// или просто стрелка:
this.items.forEach((i) => console.log(this.prefix + i));
},
};
obj.render();
⚠️ Ловушка: setTimeout(this.method, 1000) теряет контекст. Нужно setTimeout(() => this.method(), 1000) или setTimeout(this.method.bind(this), 1000).
1414. Прототипы и прототипное наследование?
senior
Короткий ответ: Каждый объект имеет внутреннюю ссылку [[Prototype]] (доступна как __proto__) на другой объект. При обращении к свойству движок ищет его по цепочке прототипов вверх. prototype — это свойство функций-конструкторов, которое становится прототипом создаваемых объектов.
Подробно:
// prototype (у конструктора) vs __proto__ (у экземпляра)
function Animal(name) { this.name = name; }
Animal.prototype.speak = function () {
return `${this.name} издаёт звук`;
};
const cat = new Animal("Кот");
console.log(cat.speak()); // "Кот издаёт звук"
console.log(cat.__proto__ === Animal.prototype); // true
console.log(Object.getPrototypeOf(cat) === Animal.prototype); // true
// Цепочка прототипов: cat → Animal.prototype → Object.prototype → null
console.log(cat.toString()); // найден в Object.prototype
// Поиск свойства идёт вверх по цепочке
const parent = { greet() { return "привет"; } };
const child = Object.create(parent); // child.__proto__ = parent
child.name = "ребёнок";
console.log(child.greet()); // "привет" — найдено в parent
console.log(child.hasOwnProperty("greet")); // false — не своё
console.log(child.hasOwnProperty("name")); // true
// Object.create — создание с заданным прототипом
const proto = { type: "base" };
const o = Object.create(proto);
console.log(o.type); // "base"
Схема цепочки для cat:
cat ──__proto__──▶ Animal.prototype ──__proto__──▶ Object.prototype ──__proto__──▶ null
⚠️ Ловушка: __proto__ — это геттер/сеттер из Object.prototype, формально устаревший. Используйте Object.getPrototypeOf / Object.setPrototypeOf. Изменение прототипа существующего объекта (Object.setPrototypeOf) сильно бьёт по производительности.
1515. Оператор new под капотом?
middle
Короткий ответ: new делает 4 вещи: создаёт пустой объект, привязывает его прототип к prototype конструктора, вызывает конструктор с this = новый объект, возвращает объект (если конструктор не вернул свой).
Подробно:
function User(name) {
this.name = name;
}
const u = new User("Аня");
// Что происходит под капотом — эмуляция:
function myNew(Constructor, ...args) {
// 1. Создаём пустой объект
const obj = {};
// 2. Привязываем прототип
Object.setPrototypeOf(obj, Constructor.prototype);
// 3. Вызываем конструктор с this = obj
const result = Constructor.apply(obj, args);
// 4. Возвращаем obj, если конструктор не вернул объект
return typeof result === "object" && result !== null ? result : obj;
}
const u2 = myNew(User, "Боб");
console.log(u2.name); // "Боб"
// Если конструктор возвращает объект — он перекрывает this
function Weird() {
this.a = 1;
return { b: 2 };
}
console.log(new Weird()); // { b: 2 } — примитивный return игнорируется
⚠️ Ловушка: Забыли new — в non-strict this станет глобальным объектом и конструктор «загрязнит» его. Защита: class кидает TypeError без new, либо проверка if (!(this instanceof User)) return new User(...).
1616. class — сахар над прототипами?
middle
Короткий ответ: class — синтаксический сахар над функциями-конструкторами и прототипами. Методы класса кладутся в prototype, наследование через extends устанавливает цепочку прототипов.
Подробно:
class Animal {
constructor(name) {
this.name = name; // свойства экземпляра
}
speak() { // попадает в Animal.prototype
return `${this.name} говорит`;
}
static create(name) { // статический метод — на самом классе
return new Animal(name);
}
}
class Dog extends Animal {
constructor(name) {
super(name); // вызов родительского конструктора
}
speak() {
return super.speak() + " гав"; // super — доступ к родителю
}
}
const d = new Dog("Рекс");
console.log(d.speak()); // "Рекс говорит гав"
// Доказательство, что это прототипы:
console.log(typeof Animal); // "function"
console.log(d.speak === Dog.prototype.speak); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
Отличия от обычных функций: класс не всплывает (TDZ), всегда в strict mode, нельзя вызвать без new, методы неперечисляемы.
⚠️ Ловушка: Приватные поля #field — реально приватны (синтаксически), в отличие от соглашения _field. Обращение к #field снаружи — синтаксическая ошибка, а не undefined.
1717. Event loop?
senior
Короткий ответ: JS однопоточный. Event loop постоянно проверяет: если call stack пуст — берёт задачу из очереди. Сначала опустошается ВСЯ очередь микрозадач (промисы), потом берётся одна макрозадача (setTimeout, события), затем снова все микрозадачи.
Подробно:
Компоненты:
- Call Stack — стек вызовов синхронного кода.
- Web APIs — браузерные API (таймеры, fetch, DOM-события) работают вне движка.
- Callback Queue (macrotask) —
setTimeout,setInterval, события,setImmediate. - Microtask Queue —
Promise.then/catch/finally,queueMicrotask,MutationObserver.
Алгоритм одного «тика»:
- Выполнить весь синхронный код (опустошить стек).
- Опустошить ВСЮ очередь микрозадач (включая те, что добавились по ходу).
- Отрисовать (в браузере, при необходимости).
- Взять ОДНУ макрозадачу, выполнить.
- Повторить с шага 2.
console.log("1 — синхронно");
setTimeout(() => console.log("2 — макротаска"), 0);
Promise.resolve().then(() => console.log("3 — микротаска"));
console.log("4 — синхронно");
// Вывод:
// 1 — синхронно
// 4 — синхронно
// 3 — микротаска ← микрозадачи раньше макрозадач
// 2 — макротаска
⚠️ Ловушка: Бесконечное добавление микрозадач (например, рекурсивный Promise.then или queueMicrotask) заблокирует рендеринг и макрозадачи — очередь микрозадач опустошается полностью перед любой макрозадачей.
1818. Microtask vs macrotask — порядок вывода?
senior
Короткий ответ: Микрозадачи (промисы) всегда выполняются перед макрозадачами (таймеры) после каждого синхронного блока. Это частый вопрос «угадай порядок».
Подробно:
console.log("start");
setTimeout(() => console.log("timeout 1"), 0);
Promise.resolve()
.then(() => console.log("promise 1"))
.then(() => console.log("promise 2"));
setTimeout(() => console.log("timeout 2"), 0);
console.log("end");
// Вывод:
// start
// end
// promise 1
// promise 2
// timeout 1
// timeout 2
Более сложный пример с async/await:
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end"); // эквивалент .then → микротаска
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(() => console.log("setTimeout"), 0);
async1();
Promise.resolve().then(() => console.log("promise"));
console.log("script end");
// Вывод:
// script start
// async1 start
// async2
// script end
// async1 end ← код после await = микротаска
// promise
// setTimeout
⚠️ Ловушка: Код после await ставится в очередь микрозадач, поэтому он выполняется ПОСЛЕ всего синхронного кода текущего блока, но ПЕРЕД любым setTimeout. await async2() сначала синхронно выполняет тело async2, и только продолжение откладывается.
1919. Промисы: состояния и методы?
middle
Короткий ответ: Промис — объект, представляющий результат асинхронной операции. Три состояния: pending → fulfilled (resolve) или rejected (reject). Переход необратим. Методы: then, catch, finally.
Подробно:
const promise = new Promise((resolve, reject) => {
const ok = true;
if (ok) resolve("успех");
else reject(new Error("ошибка"));
});
promise
.then((value) => {
console.log(value); // "успех"
return value.toUpperCase(); // возврат → следующий then
})
.then((upper) => console.log(upper)) // "УСПЕХ"
.catch((err) => console.error(err)) // ловит любую ошибку выше
.finally(() => console.log("готово")); // выполнится всегда
// Цепочки: каждый then возвращает новый промис
fetchUser()
.then((user) => fetchPosts(user.id)) // возврат промиса → ждём его
.then((posts) => console.log(posts))
.catch((err) => console.error("Любая ошибка в цепочке:", err));
// Состояния необратимы
const p = new Promise((resolve, reject) => {
resolve("первый");
resolve("второй"); // игнорируется
reject("ошибка"); // игнорируется
});
catch ловит ошибки из ВСЕХ предыдущих then. finally не получает значение и не меняет его (но если бросит/вернёт reject — повлияет).
⚠️ Ловушка: Ошибки, выброшенные внутри then, попадают в ближайший следующий catch, но НЕ в catch, стоящий ДО них. Также: непойманный rejected-промис вызывает unhandledrejection. Возврат значения из then оборачивается в resolved-промис автоматически.
2020. Promise.all / allSettled / race / any?
middle
Короткий ответ: all — ждёт все, падает на первой ошибке. allSettled — ждёт все, никогда не падает. race — первый завершившийся (любой исход). any — первый успешный.
Подробно:
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.reject("ошибка");
// all — массив результатов или первая ошибка
Promise.all([p1, p2])
.then((res) => console.log(res)); // [1, 2]
Promise.all([p1, p3])
.catch((err) => console.log(err)); // "ошибка" — упал весь all
// allSettled — статус каждого, не падает
Promise.allSettled([p1, p3]).then((res) => console.log(res));
// [
// { status: "fulfilled", value: 1 },
// { status: "rejected", reason: "ошибка" }
// ]
// race — первый завершившийся (успех ИЛИ ошибка)
Promise.race([
new Promise((r) => setTimeout(() => r("медленный"), 200)),
new Promise((r) => setTimeout(() => r("быстрый"), 100)),
]).then((res) => console.log(res)); // "быстрый"
// any — первый УСПЕШНЫЙ, игнорирует ошибки
Promise.any([p3, p1]).then((res) => console.log(res)); // 1
// если все упали → AggregateError
| Метод | Когда резолвится | При ошибке |
|---|---|---|
all |
все успешны | падает сразу при первой |
allSettled |
все завершились | не падает никогда |
race |
первый завершившийся | падает, если первый упал |
any |
первый успешный | падает только если все упали |
⚠️ Ловушка: Promise.all останавливается на первой ошибке, но остальные промисы НЕ отменяются — они продолжают выполняться (промисы вообще нельзя отменить). Для отказоустойчивости (нужны все результаты независимо) используйте allSettled.
2121. async/await?
middle
Короткий ответ: async/await — синтаксический сахар над промисами. async-функция всегда возвращает промис, await приостанавливает её до резолва промиса. Ошибки ловятся через try/catch.
Подробно:
// async-функция всегда возвращает промис
async function getValue() {
return 42; // оборачивается в Promise.resolve(42)
}
getValue().then((v) => console.log(v)); // 42
// await «разворачивает» промис
async function loadUser() {
const response = await fetch("/api/user"); // ждём
const user = await response.json(); // ждём
return user;
}
// Обработка ошибок через try/catch
async function safeLoad() {
try {
const user = await loadUser();
console.log(user);
} catch (err) {
console.error("Ошибка загрузки:", err); // ловит reject и throw
} finally {
console.log("завершено");
}
}
// Эквивалент в промисах:
function loadUserPromise() {
return fetch("/api/user").then((r) => r.json());
}
await можно применять только внутри async (или на верхнем уровне модуля — top-level await).
⚠️ Ловушка: Забытый await возвращает промис вместо значения:
async function bug() {
const data = loadUser(); // забыли await
console.log(data); // Promise { <pending> }, а не объект
}
И try/catch НЕ поймает ошибку, если перед промисом нет await.
2222. Параллельное vs последовательное выполнение await?
middle
Короткий ответ: Несколько await подряд выполняются последовательно (медленно). Для параллельного запуска независимых операций используйте Promise.all.
Подробно:
// ПЛОХО: последовательно — 3 секунды суммарно
async function sequential() {
const a = await fetchA(); // 1 сек, ждём
const b = await fetchB(); // ещё 1 сек
const c = await fetchC(); // ещё 1 сек
return [a, b, c];
}
// ХОРОШО: параллельно — ~1 секунда (max из трёх)
async function parallel() {
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
return [a, b, c];
}
// Промежуточный вариант: запустить, потом дождаться
async function startThenAwait() {
const pA = fetchA(); // запуск без await — операции стартуют сразу
const pB = fetchB();
const a = await pA; // теперь ждём
const b = await pB;
return [a, b];
}
Последовательный await оправдан только если операции зависимы (результат одной нужен для следующей).
⚠️ Ловушка: await в цикле for...of — последовательный по итерациям:
// Медленно — по одному
for (const url of urls) {
await fetch(url);
}
// Быстро — все сразу
await Promise.all(urls.map((url) => fetch(url)));
Иногда последовательность нужна намеренно (rate limiting), но чаще это случайное замедление.
2323. Колбэки и callback hell?
junior
Короткий ответ: Колбэк — функция, переданная в другую функцию для вызова позже. Вложение множества асинхронных колбэков порождает «callback hell» — нечитаемую пирамиду. Решается промисами и async/await.
Подробно:
// Callback hell — «пирамида гибели»
getUser(1, (user) => {
getPosts(user.id, (posts) => {
getComments(posts[0].id, (comments) => {
getAuthor(comments[0].id, (author) => {
console.log(author);
// обработка ошибок? на каждом уровне отдельно...
}, handleError);
}, handleError);
}, handleError);
}, handleError);
// То же на промисах — плоско
getUser(1)
.then((user) => getPosts(user.id))
.then((posts) => getComments(posts[0].id))
.then((comments) => getAuthor(comments[0].id))
.then((author) => console.log(author))
.catch(handleError); // одна обработка ошибок
// То же на async/await — читается как синхронный код
async function load() {
try {
const user = await getUser(1);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
const author = await getAuthor(comments[0].id);
console.log(author);
} catch (err) {
handleError(err);
}
}
Проблемы callback hell: трудно читать, дублирование обработки ошибок, нет единого потока управления, инверсия контроля (доверяем чужому коду вызвать колбэк правильно).
⚠️ Ловушка: Соглашение Node «error-first callback» (err, data) => {} — забыть проверить err легко, а необработанные ошибки в колбэках не ловятся try/catch снаружи (асинхронность).
2424. Деструктуризация?
junior
Короткий ответ: Синтаксис для извлечения значений из массивов/объектов в переменные. Поддерживает значения по умолчанию, переименование, вложенность, rest.
Подробно:
// Массивы — по позиции
const [first, second, , fourth] = [1, 2, 3, 4]; // пропуск третьего
console.log(first, second, fourth); // 1 2 4
// Обмен значений без temp
let a = 1, b = 2;
[a, b] = [b, a]; // a=2, b=1
// Объекты — по имени ключа
const user = { name: "Аня", age: 30, city: "Москва" };
const { name, age } = user;
// Переименование
const { name: userName } = user; // userName = "Аня"
// Значения по умолчанию (только для undefined!)
const { role = "user" } = user; // role = "user" (нет в объекте)
// Вложенная деструктуризация
const data = { user: { profile: { email: "a@b.com" } } };
const { user: { profile: { email } } } = data;
console.log(email); // "a@b.com"
// В параметрах функции
function greet({ name, greeting = "Привет" }) {
return `${greeting}, ${name}`;
}
greet({ name: "Боб" }); // "Привет, Боб"
// С rest
const { name: n, ...rest } = user; // rest = { age, city }
⚠️ Ловушка: Деструктуризация с присваиванием (без let/const) требует скобок, иначе { парсится как блок:
let x;
({ x } = { x: 5 }); // нужны скобки
И деструктуризация null/undefined падает: const { a } = null → TypeError.
2525. Spread / rest?
junior
Короткий ответ: Один и тот же синтаксис .... Spread — «разворачивает» итерируемое/объект (при вызове, в литералах). Rest — «собирает» остаток в массив/объект (в параметрах, при деструктуризации).
Подробно:
// SPREAD — разворачивание
const arr = [1, 2, 3];
const copy = [...arr]; // поверхностная копия
const merged = [...arr, 4, 5]; // [1,2,3,4,5]
Math.max(...arr); // 3 — разворот в аргументы
const obj = { a: 1, b: 2 };
const objCopy = { ...obj, c: 3 }; // { a:1, b:2, c:3 }
const override = { ...obj, b: 99 };// { a:1, b:99 } — последний выигрывает
// Строка → массив символов
const chars = [..."abc"]; // ["a","b","c"]
// REST — сбор остатка
function sum(...nums) { // все аргументы → массив
return nums.reduce((s, n) => s + n, 0);
}
sum(1, 2, 3, 4); // 10
const [head, ...tail] = [1, 2, 3]; // head=1, tail=[2,3]
const { a, ...others } = obj; // others = { b: 2 }
// Rest должен быть последним
function f(first, ...rest) {} // OK
// function f(...rest, last) {} // SyntaxError
⚠️ Ловушка: Spread делает только ПОВЕРХНОСТНУЮ копию — вложенные объекты остаются общими по ссылке:
const original = { nested: { x: 1 } };
const copy = { ...original };
copy.nested.x = 99;
console.log(original.nested.x); // 99 — вложенный объект не скопирован!
2626. Шаблонные строки?
junior
Короткий ответ: Строки в обратных кавычках с интерполяцией ${...}, многострочностью и тегированными шаблонами.
Подробно:
const name = "Аня";
const age = 30;
// Интерполяция
const greeting = `Привет, ${name}! Тебе ${age} лет.`;
// Выражения внутри
const msg = `Через год будет ${age + 1}`;
const cond = `Статус: ${age >= 18 ? "взрослый" : "ребёнок"}`;
// Многострочность без \n
const html = `
<div>
<h1>${name}</h1>
</div>
`;
// Тегированные шаблоны — функция получает части
function highlight(strings, ...values) {
return strings.reduce((acc, str, i) => {
const val = values[i] ? `<b>${values[i]}</b>` : "";
return acc + str + val;
}, "");
}
const result = highlight`Имя: ${name}, возраст: ${age}`;
// "Имя: <b>Аня</b>, возраст: <b>30</b>"
Тегированные шаблоны применяются для экранирования (защита от XSS), локализации, CSS-in-JS (styled-components).
⚠️ Ловушка: Многострочный шаблон сохраняет ВСЕ отступы и переводы строк как есть — отступы кода попадут в строку. Это влияет на сравнение строк и вывод.
2727. map / filter / reduce / forEach / find / some / every?
junior
Короткий ответ: Методы перебора массивов. map — трансформация, filter — отбор, reduce — свёртка, forEach — побочные эффекты, find — первый подходящий, some/every — проверки.
Подробно:
const nums = [1, 2, 3, 4, 5];
// map — новый массив той же длины (1:1 трансформация)
nums.map((n) => n * 2); // [2, 4, 6, 8, 10]
// filter — новый массив с отобранными элементами
nums.filter((n) => n % 2 === 0); // [2, 4]
// reduce — свёртка к одному значению
nums.reduce((acc, n) => acc + n, 0); // 15
// forEach — только побочные эффекты, возвращает undefined
nums.forEach((n) => console.log(n)); // ничего не возвращает
// find — первый подходящий элемент (или undefined)
nums.find((n) => n > 3); // 4
// findIndex — индекс первого подходящего
nums.findIndex((n) => n > 3); // 3
// some — есть ли хоть один подходящий → boolean
nums.some((n) => n > 4); // true
// every — все ли подходят → boolean
nums.every((n) => n > 0); // true
| Метод | Возвращает | Останавливается |
|---|---|---|
map |
новый массив | нет |
filter |
новый массив | нет |
reduce |
аккумулятор | нет |
forEach |
undefined |
нет |
find |
элемент / undefined |
на первом true |
some |
boolean |
на первом true |
every |
boolean |
на первом false |
Ключевое: map/filter/reduce НЕ мутируют исходный массив, возвращают новый. forEach нельзя прервать через break/return (используйте for...of или some).
⚠️ Ловушка: Использовать map ради побочных эффектов (без использования результата) — антипаттерн, для этого есть forEach. И forEach не дождётся await внутри колбэка — для асинхронного перебора нужен for...of.
2828. reduce подробно?
middle
Короткий ответ: reduce(callback(accumulator, current, index, array), initialValue) сводит массив к одному значению, прогоняя каждый элемент через колбэк и накапливая результат в аккумуляторе.
Подробно:
// Сумма
[1, 2, 3, 4].reduce((acc, cur) => acc + cur, 0); // 10
// acc: 0→1→3→6→10
// Без начального значения — первый элемент становится acc
[1, 2, 3].reduce((acc, cur) => acc + cur); // 6, acc стартует с 1
// Максимум
[3, 7, 2].reduce((max, n) => (n > max ? n : max)); // 7
// Группировка по ключу
const people = [
{ name: "Аня", city: "Мск" },
{ name: "Боб", city: "Спб" },
{ name: "Вера", city: "Мск" },
];
people.reduce((groups, p) => {
(groups[p.city] ||= []).push(p.name);
return groups;
}, {});
// { Мск: ["Аня", "Вера"], Спб: ["Боб"] }
// Подсчёт частот
["a", "b", "a", "c", "a"].reduce((acc, x) => {
acc[x] = (acc[x] || 0) + 1;
return acc;
}, {});
// { a: 3, b: 1, c: 1 }
// Композиция функций (pipe)
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
const addThenDouble = pipe((n) => n + 1, (n) => n * 2);
addThenDouble(5); // 12
// «Развернуть» вложенные массивы (flat вручную)
[[1, 2], [3, 4]].reduce((acc, arr) => acc.concat(arr), []); // [1,2,3,4]
⚠️ Ловушка: reduce БЕЗ начального значения на пустом массиве кидает TypeError: Reduce of empty array with no initial value. Всегда указывайте initialValue (особенно когда тип аккумулятора отличается от элементов — объект, число при массиве объектов).
2929. Копирование объектов: shallow vs deep?
middle
Короткий ответ: Поверхностное (shallow) копирование копирует верхний уровень, вложенные объекты остаются по ссылке. Глубокое (deep) копирует всё рекурсивно. Современный способ deep — structuredClone.
Подробно:
const original = {
name: "Аня",
address: { city: "Москва" },
};
// SHALLOW — spread / Object.assign
const shallow1 = { ...original };
const shallow2 = Object.assign({}, original);
shallow1.address.city = "Спб";
console.log(original.address.city); // "Спб" — вложенный объект общий!
// DEEP — structuredClone (современный, встроенный)
const deep = structuredClone(original);
deep.address.city = "Казань";
console.log(original.address.city); // не изменилось
// DEEP — JSON-хак (с оговорками)
const jsonClone = JSON.parse(JSON.stringify(original));
Проблемы JSON-хака:
const obj = {
date: new Date(), // → строка
fn: () => {}, // → теряется
undef: undefined, // → теряется
sym: Symbol(), // → теряется
inf: Infinity, // → null
nan: NaN, // → null
map: new Map(), // → {} (пустой объект)
};
JSON.parse(JSON.stringify(obj));
// { date: "2026-...строка", inf: null, nan: null, map: {} }
// циклические ссылки → TypeError
structuredClone поддерживает Date, Map, Set, RegExp, ArrayBuffer, циклические ссылки, но НЕ функции, DOM-узлы, прототипы (теряет класс) — кинет ошибку на функциях.
| Способ | Глубина | Функции | Циклы | Date/Map/Set |
|---|---|---|---|---|
| spread / assign | shallow | — | — | — |
JSON |
deep | теряет | падает | ломает |
structuredClone |
deep | падает | OK | OK |
⚠️ Ловушка: structuredClone теряет прототип — клон обычного объекта {}, экземпляр класса перестаёт быть экземпляром (instanceof → false), методы теряются.
3030. Мутабельность и передача по ссылке/значению?
middle
Короткий ответ: Примитивы передаются по значению (копия), объекты — по ссылке (копия ссылки). Технически JS всегда «pass by value», но для объектов значением является ссылка.
Подробно:
// Примитив — копия значения
function changePrimitive(x) {
x = 100;
}
let a = 5;
changePrimitive(a);
console.log(a); // 5 — не изменилось
// Объект — копия ССЫЛКИ (обе указывают на один объект)
function mutateObject(obj) {
obj.value = 100; // мутация общего объекта
}
const o = { value: 5 };
mutateObject(o);
console.log(o.value); // 100 — изменилось!
// Но переприсваивание ссылки внутри функции НЕ влияет снаружи
function reassign(obj) {
obj = { value: 999 }; // новая ссылка, локальная
}
const o2 = { value: 5 };
reassign(o2);
console.log(o2.value); // 5 — снаружи ссылка прежняя
// Поэтому методы массивов делятся на мутирующие и нет
const arr = [3, 1, 2];
arr.sort(); // МУТИРУЕТ исходный
arr.push(4); // МУТИРУЕТ
const sorted = [...arr].sort(); // безопасно — копия
// Иммутабельные: map, filter, slice, concat, [...spread]
// Мутирующие: push, pop, splice, sort, reverse, shift, unshift, fill
⚠️ Ловушка: sort() и reverse() мутируют массив И возвращают его же (одну ссылку). const b = a.sort() — a и b это один массив. Новые методы toSorted, toReversed, toSpliced, with возвращают копию.
3131. Модули: ES Modules vs CommonJS?
middle
Короткий ответ: ESM (import/export) — стандарт, статический анализ, асинхронная загрузка, работает в браузере и Node. CommonJS (require/module.exports) — старый формат Node, синхронный, динамический.
Подробно:
// ===== ES Modules =====
// export
export const name = "Аня"; // named export
export function greet() {} // named
export default class User {} // default (один на модуль)
// import
import User from "./user.js"; // default
import { name, greet } from "./utils.js"; // named
import { name as userName } from "./u.js"; // переименование
import * as utils from "./utils.js"; // всё
import User, { name } from "./user.js"; // default + named
// Динамический импорт — возвращает промис
const module = await import("./heavy.js"); // ленивая загрузка
// ===== CommonJS (Node) =====
const fs = require("fs"); // импорт
const { readFile } = require("fs"); // деструктуризация
module.exports = { name, greet }; // экспорт
module.exports.foo = 1; // или по одному
exports.bar = 2; // ссылка на module.exports
| ESM | CommonJS | |
|---|---|---|
| Синтаксис | import/export |
require/module.exports |
| Загрузка | асинхронная, статическая | синхронная, динамическая |
| Анализ | статический (tree-shaking) | динамический |
this верхнего уровня |
undefined |
module.exports |
| Когда резолвится | до выполнения (hoisted) | в момент require |
⚠️ Ловушка: В ESM import всплывает и резолвится статически — нельзя import внутри if (только import()). А импорты ESM — это «живые» биндинги (изменение экспорта видно импортёру), тогда как CommonJS require отдаёт копию-снимок значения на момент вызова.
3232. IIFE?
middle
Короткий ответ: Immediately Invoked Function Expression — функция, которая объявляется и сразу вызывается. Создаёт изолированную область видимости. До ES6 — основной способ инкапсуляции (паттерн «модуль»).
Подробно:
// Базовый синтаксис
(function () {
const secret = "приватно";
console.log(secret);
})(); // сразу вызывается
// Со стрелкой
(() => {
console.log("выполнено сразу");
})();
// Передача аргументов
(function (global) {
// global = window, защита от переопределения
})(window);
// Module pattern — приватное состояние через замыкание
const counter = (function () {
let count = 0; // приватная
return {
increment() { return ++count; },
get() { return count; },
};
})();
counter.increment(); // 1
console.log(counter.count); // undefined — недоступно
// Зачем нужны скобки: превратить declaration в expression
// function(){}() — SyntaxError
// (function(){})() — OK
В современном коде роль IIFE взяли на себя ES-модули (своя область видимости) и блочная видимость let/const. Но IIFE всё ещё встречается, в т.ч. с async ((async () => { await ... })()).
⚠️ Ловушка: Без скобок function(){}() парсится как объявление функции и кидает ошибку. Альтернативы скобкам: !function(){}(), +function(){}() (используют унарные операторы, чтобы получить expression).
3333. debounce и throttle?
middle
Короткий ответ: Обе ограничивают частоту вызова функции. Debounce откладывает вызов до паузы (выполнит после того как перестали дёргать). Throttle гарантирует вызов не чаще раза в интервал.
Подробно:
// DEBOUNCE — вызов через delay после ПОСЛЕДНЕГО события
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// Применение: поиск при вводе (ждём, пока пользователь допечатает)
const onSearch = debounce((q) => console.log("Поиск:", q), 300);
// THROTTLE — вызов не чаще раза в limit мс
function throttle(fn, limit) {
let inThrottle = false;
return function (...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// Применение: scroll, resize, mousemove (равномерно, не каждый кадр)
const onScroll = throttle(() => console.log("scroll"), 200);
Разница на временной шкале (события идут плотно):
События: | | | | | | | | |
Debounce: X (один раз, после паузы)
Throttle: X X X X (равномерно, раз в интервал)
Применение:
- debounce: автокомплит/поиск, валидация формы, авто-сохранение.
- throttle: обработка scroll/resize/mousemove, ограничение запросов к API.
⚠️ Ловушка: В реализациях важно сохранять this и args через fn.apply(this, args), иначе обработчик события потеряет контекст и данные события. Также для debounce часто нужен флаг immediate (вызвать сразу на первом событии) и метод cancel.
3434. Symbol?
middle
Короткий ответ: Symbol — примитивный тип, создающий уникальные неперечисляемые идентификаторы. Используется для «скрытых» свойств объектов и системных хуков (well-known symbols).
Подробно:
// Каждый символ уникален
const s1 = Symbol("desc");
const s2 = Symbol("desc");
console.log(s1 === s2); // false — даже с одним описанием
// Как ключ объекта — не пересекается с обычными ключами
const id = Symbol("id");
const user = { name: "Аня", [id]: 123 };
console.log(user[id]); // 123
console.log(Object.keys(user)); // ["name"] — символ не виден
for (const k in user) console.log(k); // только "name"
// Глобальный реестр символов
const g1 = Symbol.for("app.id"); // создаёт или берёт существующий
const g2 = Symbol.for("app.id");
console.log(g1 === g2); // true
// Well-known symbols — кастомизация поведения
class Range {
constructor(start, end) { this.start = start; this.end = end; }
[Symbol.iterator]() { // делает объект итерируемым
let cur = this.start;
const end = this.end;
return {
next() {
return cur <= end
? { value: cur++, done: false }
: { value: undefined, done: true };
},
};
}
}
console.log([...new Range(1, 3)]); // [1, 2, 3]
Символы НЕ попадают в Object.keys, for...in, JSON.stringify. Получить их можно через Object.getOwnPropertySymbols.
⚠️ Ловушка: Символы из Symbol() и Symbol.for() — РАЗНЫЕ: первый всегда уникален, второй берётся из глобального реестра. Также символ нельзя неявно привести к строке ("" + Symbol() → TypeError), только .toString() или String(sym).
3535. Итераторы и генераторы?
senior
Короткий ответ: Итератор — объект с методом next(), возвращающим { value, done }. Генератор (function*) — функция, способная приостанавливаться через yield и автоматически реализующая протокол итератора.
Подробно:
// Генератор — function* и yield
function* gen() {
yield 1;
yield 2;
yield 3;
}
const it = gen();
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }
// Генератор итерируем
for (const v of gen()) console.log(v); // 1 2 3
console.log([...gen()]); // [1, 2, 3]
// Бесконечная последовательность (ленивая)
function* naturals() {
let n = 1;
while (true) yield n++;
}
const nums = naturals();
nums.next().value; // 1
nums.next().value; // 2 — генерируется по требованию
// Двусторонняя связь — yield возвращает значение из next(arg)
function* dialog() {
const name = yield "Как вас зовут?";
yield `Привет, ${name}!`;
}
const d = dialog();
d.next(); // { value: "Как вас зовут?", done: false }
d.next("Аня"); // { value: "Привет, Аня!", done: false }
// Делегирование yield*
function* combined() {
yield* [1, 2];
yield* gen();
}
Применение генераторов: ленивые/бесконечные последовательности, кастомная итерация, корутины, библиотеки (redux-saga).
⚠️ Ловушка: Генератор-объект одноразовый — после done: true его нельзя «перемотать», нужно создавать новый вызовом gen(). И первый next() запускает код только до первого yield; аргумент в самый первый next() игнорируется.
3636. Optional chaining (?.) и nullish coalescing (??)?
junior
Короткий ответ: ?. безопасно обращается к свойству — возвращает undefined вместо ошибки, если левая часть null/undefined. ?? возвращает правый операнд только если левый null/undefined (в отличие от ||).
Подробно:
// Optional chaining ?.
const user = { profile: { name: "Аня" } };
console.log(user.profile?.name); // "Аня"
console.log(user.settings?.theme); // undefined (не ошибка!)
console.log(user.address?.city); // undefined
// Без ?. была бы ошибка
// user.address.city → TypeError: Cannot read properties of undefined
// С вызовами методов и массивами
user.getName?.(); // вызовет, если метод есть, иначе undefined
user.list?.[0]; // безопасный доступ по индексу
// Nullish coalescing ??
const a = 0 ?? 10; // 0 (0 не nullish)
const b = null ?? 10; // 10
const c = undefined ?? 10; // 10
const d = "" ?? "default"; // "" (пустая строка не nullish)
// Разница с ||
const count1 = 0 || 5; // 5 (|| считает 0 falsy → заменяет)
const count2 = 0 ?? 5; // 0 (?? заменяет только null/undefined)
// Комбинация
const theme = user.settings?.theme ?? "light"; // "light"
|| срабатывает на всех falsy (0, "", false, NaN), а ?? — только на null/undefined. Поэтому ?? правильнее для значений по умолчанию, где 0 и "" валидны.
⚠️ Ловушка: Нельзя смешивать ?? с ||/&& без скобок — синтаксическая ошибка: a ?? b || c → SyntaxError. Нужно (a ?? b) || c. Также ?. только «короткозамыкает» доступ, но не защищает от ошибки в следующем вне-цепочечном выражении.
3737. Garbage collection и утечки памяти?
senior
Короткий ответ: JS управляет памятью автоматически через сборщик мусора по алгоритму mark-and-sweep: объекты, недостижимые из корней (root), удаляются. Утечки — когда ненужные объекты остаются достижимыми.
Подробно:
Алгоритм mark-and-sweep:
- Сборщик начинает с корней (global object, текущий стек вызовов).
- Mark: помечает все объекты, достижимые по ссылкам от корней.
- Sweep: освобождает память непомеченных (недостижимых) объектов.
Достижимость, а не подсчёт ссылок — поэтому циклические ссылки не вызывают утечки (если весь цикл недостижим от корней).
let obj = { data: "большой объект" };
obj = null; // объект стал недостижим → будет собран
// Частые утечки:
// 1. Забытые таймеры/интервалы
const id = setInterval(() => { /* держит замыкание */ }, 1000);
// нужно clearInterval(id) когда не нужен
// 2. Забытые обработчики событий
element.addEventListener("click", handler);
// держит element и handler; нужно removeEventListener
// 3. Замыкания, удерживающие большие объекты
function leak() {
const huge = new Array(1000000);
return () => huge[0]; // замыкание держит весь huge
}
// 4. Случайные глобальные переменные (без объявления, non-strict)
function bad() { leaked = "глобальная"; } // window.leaked живёт вечно
// 5. Растущие кэши/Map без очистки — используйте WeakMap
const cache = new WeakMap(); // ключи-объекты собираются автоматически
WeakMap/WeakSet держат «слабые» ссылки — не мешают сборке ключей-объектов, что предотвращает утечки в кэшах.
⚠️ Ловушка: Detached DOM nodes — узел удалён из DOM, но на него ссылается JS-переменная или замыкание обработчика → не собирается. Также console.log(obj) в DevTools может удерживать ссылку. Профилируйте через Memory snapshot в DevTools.
3838. Object: keys/values/entries, freeze, getters/setters?
junior
Короткий ответ: Object.keys/values/entries извлекают ключи/значения/пары. Object.freeze делает объект иммутабельным (поверхностно). Геттеры/сеттеры — методы, выглядящие как свойства.
Подробно:
const user = { name: "Аня", age: 30 };
Object.keys(user); // ["name", "age"]
Object.values(user); // ["Аня", 30]
Object.entries(user); // [["name","Аня"], ["age",30]]
// entries удобен для итерации и преобразований
for (const [key, value] of Object.entries(user)) {
console.log(`${key}: ${value}`);
}
// Объект из пар обратно
Object.fromEntries([["a", 1], ["b", 2]]); // { a: 1, b: 2 }
// freeze — запрет изменений (поверхностный!)
const frozen = Object.freeze({ x: 1, nested: { y: 2 } });
frozen.x = 99; // молча игнорируется (TypeError в strict)
frozen.nested.y = 99; // ИЗМЕНИТСЯ — freeze не рекурсивен
console.log(Object.isFrozen(frozen)); // true
// Getters / Setters
const temp = {
_celsius: 0,
get celsius() { return this._celsius; },
set celsius(val) { this._celsius = val; },
get fahrenheit() { return this._celsius * 1.8 + 32; },
set fahrenheit(val) { this._celsius = (val - 32) / 1.8; },
};
temp.celsius = 25;
console.log(temp.fahrenheit); // 77 — вычисляется при доступе
temp.fahrenheit = 212;
console.log(temp.celsius); // 100
⚠️ Ловушка: Object.freeze поверхностный — вложенные объекты остаются мутабельными. Для глубокой заморозки нужна рекурсия (deepFreeze). Object.keys НЕ обходит унаследованные и символьные свойства (в отличие от for...in, который обходит унаследованные перечисляемые).
3939. Map / Set vs объект / массив?
middle
Короткий ответ: Map — коллекция пар ключ-значение с любыми ключами и сохранением порядка. Set — коллекция уникальных значений. Они лучше объекта/массива для частых добавлений/удалений и поиска.
Подробно:
// MAP — ключи любого типа, есть .size, сохраняет порядок
const map = new Map();
map.set("str", 1);
map.set(42, 2);
const objKey = {};
map.set(objKey, 3); // объект как ключ!
map.get(objKey); // 3
map.has("str"); // true
map.size; // 3
map.delete(42);
for (const [k, v] of map) console.log(k, v); // итерируемый
// SET — уникальные значения
const set = new Set([1, 2, 2, 3, 3]);
console.log([...set]); // [1, 2, 3] — дубли убраны
set.add(4);
set.has(2); // true
set.size; // 4
// Дедупликация массива
const unique = [...new Set([1, 1, 2, 3])]; // [1, 2, 3]
| Map | Object | |
|---|---|---|
| Ключи | любой тип | string / symbol |
| Размер | .size |
Object.keys().length |
| Порядок | гарантирован | в основном да |
| Итерация | напрямую (for...of) |
через Object.entries |
| Производительность вставки/удаления | оптимизирована | хуже при частых изменениях |
| Прототип | нет «мусорных» ключей | есть (риск коллизий) |
Когда что: Map — динамический словарь с произвольными ключами; объект — фиксированная структура/запись; Set — уникальность/проверка членства; массив — упорядоченный список с индексами.
⚠️ Ловушка: Map нельзя сериализовать через JSON.stringify (даст {}). И Set/Map сравнивают объекты-ключи по ссылке, а не по содержимому: set.add({a:1}); set.has({a:1}) → false.
4040. Strict mode?
junior
Короткий ответ: "use strict" включает строгий режим: запрещает небезопасные конструкции, превращает молчаливые ошибки в исключения, меняет this в обычных вызовах на undefined. В ES-модулях и классах включён автоматически.
Подробно:
"use strict"; // в начале файла или функции
// 1. Нельзя создавать необъявленные глобальные переменные
function f() {
undeclared = 5; // ReferenceError (без strict — создало бы window.undeclared)
}
// 2. this в обычном вызове = undefined (не window)
function g() {
console.log(this); // undefined (без strict — window)
}
g();
// 3. Ошибка при записи в нередактируемое
const obj = Object.freeze({ x: 1 });
obj.x = 2; // TypeError (без strict — молча игнорируется)
// 4. Запрет дублей параметров
// function h(a, a) {} // SyntaxError
// 5. Запрет восьмеричных литералов 0123, with, delete переменной
Где strict включён автоматически: тело class, ES-модули (import/export), поэтому современный код по умолчанию строгий.
⚠️ Ловушка: "use strict" должен быть ПЕРВОЙ инструкцией в файле/функции — иначе игнорируется молча. Директива на уровне функции делает строгой только эту функцию. В модулях писать её не нужно.
4141. Зачем нужны замыкания?
concept
Короткий ответ: Замыкания обеспечивают инкапсуляцию (приватное состояние), сохранение данных между вызовами, фабрики функций и каррирование, а также лежат в основе колбэков, обработчиков и модулей.
Подробно:
// 1. Инкапсуляция приватного состояния (нет доступа снаружи)
function createStore(initial) {
let state = initial;
return {
getState: () => state,
setState: (next) => { state = next; },
};
}
// 2. Память между вызовами (мемоизация)
function memoize(fn) {
const cache = new Map(); // замыкание хранит кэш
return (arg) => {
if (cache.has(arg)) return cache.get(arg);
const result = fn(arg);
cache.set(arg, result);
return result;
};
}
// 3. Каррирование / частичное применение
const add = (a) => (b) => a + b; // b замыкается на a
const add5 = add(5);
add5(3); // 8
// 4. Фиксация конфигурации (фабрики)
const createLogger = (prefix) => (msg) => console.log(`[${prefix}] ${msg}`);
const errorLog = createLogger("ERROR");
По сути почти весь функциональный JS, React-хуки (useState хранит состояние в замыкании), обработчики событий, debounce/throttle — всё построено на замыканиях.
⚠️ Ловушка: Замыкания удерживают всё лексическое окружение в памяти. Если замыкание ссылается хотя бы на одну переменную из большой области, движок может удерживать её → потенциальная утечка.
4242. Почему JS однопоточный, но неблокирующий?
concept
Короткий ответ: JS выполняет код в одном потоке (один call stack), но длительные операции (I/O, таймеры, сеть) делегируются окружению (браузер/Node), которое выполняет их асинхронно и кладёт колбэки в очередь. Event loop забирает их, когда стек свободен.
Подробно:
// Синхронный блокирующий код заморозил бы всё
// while (true) {} // повесит вкладку — поток один
// Асинхронность: операция уходит в Web API, поток свободен
console.log("1");
setTimeout(() => console.log("3 — позже"), 0); // уходит в браузер
fetch("/data").then(() => console.log("данные")); // сеть в фоне
console.log("2");
// Вывод: 1, 2, 3 — поток не ждал таймер
// Тяжёлые вычисления всё же блокируют (нет I/O делегата)
function heavy() {
let sum = 0;
for (let i = 0; i < 1e9; i++) sum += i; // заморозит UI
return sum;
}
// Решение для CPU-задач — Web Workers (отдельный поток)
Ключ: сам движок JS однопоточный, но окружение (браузер, Node/libuv) многопоточное и берёт на себя I/O. JS-поток не ждёт — он регистрирует колбэк и продолжает. Когда операция завершилась, колбэк попадает в очередь, и event loop запускает его при свободном стеке.
⚠️ Ловушка: «Неблокирующий» относится только к асинхронным I/O-операциям. Тяжёлые синхронные вычисления (большие циклы, сложные регулярки, JSON.parse гигантского объекта) блокируют единственный поток и замораживают UI. Для них нужны Web Workers.
4343. Что не так с this в JS?
concept
Короткий ответ: this определяется в момент ВЫЗОВА, а не объявления, и зависит от способа вызова — это контринтуитивно и приводит к частой «потере контекста». Стрелочные функции (лексический this) во многом решают проблему.
Подробно:
const obj = {
name: "Аня",
greet() {
console.log(this.name); // "Аня" при obj.greet()
},
};
// 1. Потеря this при «отрыве» метода
const fn = obj.greet;
fn(); // undefined/ошибка — this уже не obj
// 2. this в колбэке теряется
[1].forEach(function () {
// this здесь не obj, а undefined/window
});
// 3. this во вложенной обычной функции
const obj2 = {
items: [1, 2],
process() {
this.items.forEach(function () {
// this.items здесь недоступно — другой this
});
},
};
// Решение — стрелки (лексический this)
const obj3 = {
items: [1, 2],
process() {
this.items.forEach(() => console.log(this.items)); // работает
},
};
Проблема в том, что this — динамический и зависит от 4 правил вызова. В большинстве языков this (или self) ссылается на экземпляр стабильно. В JS один и тот же метод даёт разный this в зависимости от того, как его вызвали.
⚠️ Ловушка: Не используйте стрелочные функции там, где this ДОЛЖЕН быть динамическим: методы объекта/прототипа, обработчики DOM (где ожидается this = element). Стрелка «жёстко» лексична, и call/apply/bind на ней не переопределят this.
4444. Зачем Promise, если есть колбэки?
concept
Короткий ответ: Промисы решают проблемы колбэков: callback hell, дублирование обработки ошибок, инверсию контроля. Они дают плоские цепочки, единый catch, композицию (all/race) и базу для async/await.
Подробно:
// Проблема колбэков: вложенность + ошибки на каждом уровне
loadA((errA, a) => {
if (errA) return handle(errA);
loadB(a, (errB, b) => {
if (errB) return handle(errB); // дубликат обработки
// ...пирамида растёт
});
});
// Промисы: плоско, единый catch, гарантии
loadA()
.then((a) => loadB(a))
.then((b) => loadC(b))
.catch(handle); // одна точка обработки ошибок
// Композиция — недостижима на колбэках элегантно
const [a, b] = await Promise.all([loadA(), loadB()]);
Дополнительные гарантии промисов:
- Колбэк
thenвызовется ровно один раз (колбэк могут вызвать 0, 1 или много раз — баг чужого кода). - Колбэк всегда асинхронен (даже на уже зарезолвленном промисе), что предотвращает race conditions.
- Решают инверсию контроля: с колбэком вы доверяете чужому коду вызвать ваш правильно; промис — это объект-обещание, которым управляете вы.
⚠️ Ловушка: Промисы не отменяемы (Promise.all не остановит остальные при ошибке), и если забыть .catch, rejected-промис вызовет unhandledrejection. Не оборачивайте уже-промис-возвращающие API руками в new Promise (антипаттерн «promise constructor»).
4545. Почему 0.1 + 0.2 !== 0.3?
concept
Короткий ответ: Числа в JS — это double (IEEE 754, 64 бита). Дроби 0.1 и 0.2 не имеют точного двоичного представления (как 1/3 в десятичной), накапливается ошибка округления.
Подробно:
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
// Почему: 0.1 в двоичной — бесконечная дробь
// 0.1₁₀ = 0.0001100110011...₂ (периодическая)
// Хранится с округлением в 64 битах → крошечная погрешность
// Правильное сравнение — через эпсилон
function nearlyEqual(a, b, eps = Number.EPSILON) {
return Math.abs(a - b) < eps;
}
nearlyEqual(0.1 + 0.2, 0.3); // true
// Округление до нужной точности
(0.1 + 0.2).toFixed(2); // "0.30" (строка)
Math.round((0.1 + 0.2) * 100) / 100; // 0.3
// Для денег — храните в копейках (целые числа) или BigInt
const cents = 10 + 20; // 30 копеек, без погрешностей
Number.EPSILON — наименьшая разница между 1 и следующим представимым числом (~2.22e-16). Используется как порог сравнения с плавающей точкой.
⚠️ Ловушка: Это НЕ баг JS — так работает IEEE 754 во всех языках (Python, Java, C). Никогда не сравнивайте дробные числа через ===; для денег используйте целые (минимальные единицы) или специальные библиотеки (decimal.js). 0.1 + 0.2 даёт лишнюю погрешность, а 0.5 + 0.5 === 1 — true (степени двойки представимы точно).
Источники
Источники и редакционная политика
Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.