Перейти к содержанию
Фронтенд

45 вопросов по теме «JavaScript» на собеседовании

В этом материале — 45 вопросов из русской колоды RecallDeck по теме «JavaScript». Сначала сформулируйте короткий ответ сами, затем откройте подробный разбор и проверьте примеры, ограничения и отказные случаи.

51 мин чтения45 подробных ответовПроверено 24 августа 2026
Главная мысль

Связывайте ответ с браузером, доступностью, производительностью и пользовательским состоянием — именно там видна зрелость frontend-инженера.

Вопросы и ответы

45 подробных ответов

01

1. Типы данных: примитивы vs объекты?

Короткий ответ: В 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([]).

02

2. typeof и баг с typeof null?

Короткий ответ: 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'. Это единственный безопасный способ проверить необъявленную переменную.

03

3. null vs undefined?

Короткий ответ: 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 не применился!
04

4. == vs ===?

Короткий ответ: === (строгое) сравнивает без приведения типов — типы должны совпадать. == (нестрогое) приводит типы перед сравнением, что порождает много неочевидных результатов.

Подробно:

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). Поэтому полагаться на == для цепочек нельзя.

05

5. Приведение типов и truthy/falsy?

Короткий ответ: 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 в некоторых контекстах (если {} трактуется как блок кода). Избегайте арифметики с объектами/массивами.

06

6. var vs let vs const?

Короткий ответ: 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.

07

7. Hoisting (всплытие)?

Короткий ответ: Объявления переменных и функций «поднимаются» в начало своей области видимости на этапе компиляции. 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.

08

8. Замыкания (closures)?

Короткий ответ: Замыкание — это функция вместе с лексическим окружением, в котором она была создана. Функция «запоминает» переменные внешней области, даже когда внешняя функция уже завершилась.

Подробно:

// Счётчик — классический пример
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 создаёт отдельное лексическое связывание на каждой итерации.

09

9. Ловушка var в цикле и setTimeout?

Короткий ответ: При 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.

10

10. this: 4 правила привязки?

Короткий ответ: Значение 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
// Метод оторван от объекта → правило обычного вызова.
11

11. call / apply / bind?

Короткий ответ: Все три задают 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 с забинженным обработчиком не сработает, если бинд делался заново.

12

12. Стрелочные функции vs обычные?

Короткий ответ: Стрелочные не имеют собственного 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!
};
13

13. Потеря this в колбэках?

Короткий ответ: Когда метод передаётся как колбэк (в 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).

14

14. Прототипы и прототипное наследование?

Короткий ответ: Каждый объект имеет внутреннюю ссылку [[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) сильно бьёт по производительности.

15

15. Оператор new под капотом?

Короткий ответ: 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(...).

16

16. class — сахар над прототипами?

Короткий ответ: 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.

17

17. Event loop?

Короткий ответ: JS однопоточный. Event loop постоянно проверяет: если call stack пуст — берёт задачу из очереди. Сначала опустошается ВСЯ очередь микрозадач (промисы), потом берётся одна макрозадача (setTimeout, события), затем снова все микрозадачи.

Подробно:

Компоненты:

  • Call Stack — стек вызовов синхронного кода.
  • Web APIs — браузерные API (таймеры, fetch, DOM-события) работают вне движка.
  • Callback Queue (macrotask)setTimeout, setInterval, события, setImmediate.
  • Microtask QueuePromise.then/catch/finally, queueMicrotask, MutationObserver.

Алгоритм одного «тика»:

  1. Выполнить весь синхронный код (опустошить стек).
  2. Опустошить ВСЮ очередь микрозадач (включая те, что добавились по ходу).
  3. Отрисовать (в браузере, при необходимости).
  4. Взять ОДНУ макрозадачу, выполнить.
  5. Повторить с шага 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) заблокирует рендеринг и макрозадачи — очередь микрозадач опустошается полностью перед любой макрозадачей.

18

18. Microtask vs macrotask — порядок вывода?

Короткий ответ: Микрозадачи (промисы) всегда выполняются перед макрозадачами (таймеры) после каждого синхронного блока. Это частый вопрос «угадай порядок».

Подробно:

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, и только продолжение откладывается.

19

19. Промисы: состояния и методы?

Короткий ответ: Промис — объект, представляющий результат асинхронной операции. Три состояния: pendingfulfilled (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-промис автоматически.

20

20. Promise.all / allSettled / race / any?

Короткий ответ: 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.

21

21. async/await?

Короткий ответ: 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.

22

22. Параллельное vs последовательное выполнение await?

Короткий ответ: Несколько 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), но чаще это случайное замедление.

23

23. Колбэки и callback hell?

Короткий ответ: Колбэк — функция, переданная в другую функцию для вызова позже. Вложение множества асинхронных колбэков порождает «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 снаружи (асинхронность).

24

24. Деструктуризация?

Короткий ответ: Синтаксис для извлечения значений из массивов/объектов в переменные. Поддерживает значения по умолчанию, переименование, вложенность, 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 } = nullTypeError.

25

25. Spread / rest?

Короткий ответ: Один и тот же синтаксис .... 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 — вложенный объект не скопирован!
26

26. Шаблонные строки?

Короткий ответ: Строки в обратных кавычках с интерполяцией ${...}, многострочностью и тегированными шаблонами.

Подробно:

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).

⚠️ Ловушка: Многострочный шаблон сохраняет ВСЕ отступы и переводы строк как есть — отступы кода попадут в строку. Это влияет на сравнение строк и вывод.

27

27. map / filter / reduce / forEach / find / some / every?

Короткий ответ: Методы перебора массивов. 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.

28

28. reduce подробно?

Короткий ответ: 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 (особенно когда тип аккумулятора отличается от элементов — объект, число при массиве объектов).

29

29. Копирование объектов: shallow vs deep?

Короткий ответ: Поверхностное (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), методы теряются.

30

30. Мутабельность и передача по ссылке/значению?

Короткий ответ: Примитивы передаются по значению (копия), объекты — по ссылке (копия ссылки). Технически 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 возвращают копию.

31

31. Модули: ES Modules vs CommonJS?

Короткий ответ: 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 отдаёт копию-снимок значения на момент вызова.

32

32. IIFE?

Короткий ответ: 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).

33

33. debounce и throttle?

Короткий ответ: Обе ограничивают частоту вызова функции. 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.

34

34. Symbol?

Короткий ответ: 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).

35

35. Итераторы и генераторы?

Короткий ответ: Итератор — объект с методом 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() игнорируется.

36

36. Optional chaining (?.) и nullish coalescing (??)?

Короткий ответ: ?. безопасно обращается к свойству — возвращает 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. Также ?. только «короткозамыкает» доступ, но не защищает от ошибки в следующем вне-цепочечном выражении.

37

37. Garbage collection и утечки памяти?

Короткий ответ: JS управляет памятью автоматически через сборщик мусора по алгоритму mark-and-sweep: объекты, недостижимые из корней (root), удаляются. Утечки — когда ненужные объекты остаются достижимыми.

Подробно:

Алгоритм mark-and-sweep:

  1. Сборщик начинает с корней (global object, текущий стек вызовов).
  2. Mark: помечает все объекты, достижимые по ссылкам от корней.
  3. 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.

38

38. Object: keys/values/entries, freeze, getters/setters?

Короткий ответ: 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, который обходит унаследованные перечисляемые).

39

39. Map / Set vs объект / массив?

Короткий ответ: 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.

40

40. Strict mode?

Короткий ответ: "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" должен быть ПЕРВОЙ инструкцией в файле/функции — иначе игнорируется молча. Директива на уровне функции делает строгой только эту функцию. В модулях писать её не нужно.

41

41. Зачем нужны замыкания?

Короткий ответ: Замыкания обеспечивают инкапсуляцию (приватное состояние), сохранение данных между вызовами, фабрики функций и каррирование, а также лежат в основе колбэков, обработчиков и модулей.

Подробно:

// 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 — всё построено на замыканиях.

⚠️ Ловушка: Замыкания удерживают всё лексическое окружение в памяти. Если замыкание ссылается хотя бы на одну переменную из большой области, движок может удерживать её → потенциальная утечка.

42

42. Почему JS однопоточный, но неблокирующий?

Короткий ответ: 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.

43

43. Что не так с this в JS?

Короткий ответ: 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.

44

44. Зачем Promise, если есть колбэки?

Короткий ответ: Промисы решают проблемы колбэков: 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»).

45

45. Почему 0.1 + 0.2 !== 0.3?

Короткий ответ: Числа в 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, архитектуру и поведенческие истории.

Начать подготовку

Продолжить подготовку

Библиотека собеседований RecallDeck

Подробные русские ответы, разборы этапов найма и планы подготовки для российского IT-рынка.

RSS