Перейти к основному содержимому

Mock Собеседование для Junior Frontend разработчика

· 44 мин. чтения

Сегодня мы разберём прохождение кандидатом Олегом тренировочного собеседования на позицию фронтенд-разработчика: он демонстрирует базовое понимание JavaScript (контекст, замыкания, методы массивов) и React (хуки, жизненный цикл компонентов), успешно решает задачи на рекурсию и reduce, однако испытывает трудности с некоторыми темами (Temporal Dead Zone, статические методы промисов, forwardRef) и признаёт недостаток практики и проектного опыта. Интервьюер Евгений даёт обратную связь, рекомендуя углубить знания по ряду тем и активнее решать алгоритмические задачи для перехода на уровень middle.

Вопрос 1. Что такое контекст выполнения в JavaScript и как он определяется?

Таймкод: 00:01:41

Ответ собеседника: Неполный. Контекст — это то, что доступно в рамках определённого блока кода: набор переменных, функций, глобальные методы и переменные. Определяется ключевым словом this.

Правильный ответ:

Контекст выполнения (Execution Context) — это абстрактная среда, в которой JavaScript-код оценивается и выполняется. Это не просто «набор доступных переменных», а сложная структура данных, которая движок V8 (или другой) создаёт при запуске кода или вызове функции.

Из чего состоит контекст выполнения:

1. Variable Environment (Lexical Environment) Содержит все переменные, объявления функций и аргументы функции, объявленные через var, let, const, function. Также хранит ссылку на внешнее лексическое окружение (для реализации замыканий).

2. Scope Chain (Цепочка областей видимости) Механизм, позволяющий текущему контексту обращаться к переменным из внешних контекстов. При обращении к переменной движок ищет её сначала в текущем Lexical Environment, затем во внешнем, и так далее до глобального.

3. this binding Значение this определяется в момент вызова функции, а не в момент объявления. Это ключевое отличие от лексического окружения.

Типы контекстов выполнения:

А. Глобальный контекст (Global Execution Context) Создаётся автоматически при загрузке скрипта. В браузере this ссылается на window, в Node.js — на global (или globalThis). В глобальном контексте Variable Environment совпадает с Lexical Environment.

Б. Контекст функции (Function Execution Context) Создаётся при каждом вызове функции. Содержит собственный Lexical Environment с локальными переменными и аргументами.

В. Контекст eval (Eval Execution Context) Создаётся при вызове eval() — используется редко и не рекомендуется.

Как создаётся контекст (двухфазный процесс):

Фаза создания (Creation Phase):

  • Создаётся Lexical Environment
  • Создаётся Variable Environment
  • Определяется значение this
  • Устанавливается ссылка на внешнее окружение (outer environment reference)

Фаза выполнения (Execution Phase):

  • Код интерпретируется и выполняется построчно
  • Переменным присваиваются значения
  • Вызываются функции (создавая новые контексты)

Как определяется this — четыре правила:

1. Глобальный контекст: this указывает на глобальный объект (window в браузере, globalThis универсально).

2. Вызов как метод объекта: this указывает на объект, которому принадлежит метод.

const obj = {
name: 'Alice',
greet() {
console.log(this.name); // 'Alice' — this указывает на obj
}
};
obj.greet();

3. Вызов через call/apply/bind: this привязывается явно.

function greet() {
console.log(this.name);
}
const person = { name: 'Bob' };
greet.call(person); // 'Bob'

4. Вызов через new (constructor call): this указывает на вновь созданный экземпляр.

function Person(name) {
this.name = name;
}
const p = new Person('Charlie');
console.log(p.name); // 'Charlie'

5. Стрелочные функции (arrow functions): Не имеют собственного this. Значение this берётся из внешнего лексического окружения — это фиксируется на момент объявления функции.

const obj = {
name: 'Dave',
greet: () => {
console.log(this.name); // undefined — this унаследован из внешнего контекста (глобального)
}
};

Стек вызовов (Call Stack):

Контексты выполнения организованы в стек. Глобальный контекст всегда внизу. При вызове функции её контекст помещается на вершину стека. При возврате — извлекается.

function first() {
console.log('first');
second();
console.log('first end');
}

function second() {
console.log('second');
}

first();
// Стек: [Global] → [Global, first] → [Global, first, second] → [Global, first] → [Global]

Важные нюансы:

  • var всплывает (hoisting) в начало функции/глобального контекста с undefined, let/const попадают в Temporal Dead Zone
  • Каждый вызов функции создаёт новый контекст, даже если функция вызывается рекурсивно
  • Замыкания работают благодаря сохранению ссылки на Lexical Environment внешней функции
  • Строгое режим ('use strict') меняет поведение this в обычных вызовах функций — вместо глобального объекта будет undefined

Вопрос 2. Что делают методы bind, call и apply в JavaScript и чем они отличаются?

Таймкод: 00:02:05

Ответ собеседника: Неполный. Методы bind, call и apply позволяют прикрепить контекст к вызываемой функции. Call вызывает функцию с переданным контекстом сразу, bind привязывает контекст и возвращает новую функцию для последующего вызова. Apply также привязывает контекст, но аргументы передаются массивом.

Правильный ответ:

Все три метода определены на Function.prototype и служат для управления значением this при вызове функции. Ответ собеседника в целом верен по сути, но требует углубления в детали и практические сценарии использования.

Сигнатуры методов:

func.call(thisArg, arg1, arg2, ...)
func.apply(thisArg, [argsArray])
func.bind(thisArg, arg1, arg2, ...)

call — немедленный вызов с явным this и аргументами через запятую:

function introduce(greeting, punctuation) {
console.log(`${greeting}, I'm ${this.name}${punctuation}`);
}

const person = { name: 'Alice' };
introduce.call(person, 'Hello', '!'); // "Hello, I'm Alice!"

apply — немедленный вызов с явным this и аргументами массивом:

introduce.apply(person, ['Hi', '.']); // "Hi, I'm Alice."

Исторически apply был полезен, когда аргументы уже были в виде массива или их количество неизвестно заранее. С появлением spread-оператора (...args) в ES6 разница между call и apply практически исчезла:

const args = ['Hey', '?'];
introduce.call(person, ...args); // эквивалент apply

bind — создание новой функции с привязанным this (и частичными аргументами):

const aliceIntroduce = introduce.bind(person);
aliceIntroduce('Greetings', '!'); // "Greetings, I'm Alice!"

// bind с частичным применением аргументов (partial application)
const aliceHello = introduce.bind(person, 'Hello');
aliceHello('!!!'); // "Hello, I'm Alice!!!"

Ключевые различия в поведении:

1. Время выполнения:

  • call и apply вызывают функцию немедленно
  • bind возвращает новую функцию без выполнения

2. Возвращаемое значение:

  • call/apply возвращают результат выполнения функции
  • bind возвращает новую функцию-обёртку

3. Привязанная функция через bind теряет возможность переопределить this:

const boundFunc = introduce.bind(person);
boundFunc.call({ name: 'Bob' }, 'Hi', '!'); // всё равно "Hi, I'm Alice!"
// this привязан жёстко, call/apply не могут его переопределить

Практические сценарии использования:

А. Использование методов объектов с другим контекстом (метод заимствование):

function Animal(name) {
this.name = name;
}

function Dog(name, breed) {
Animal.call(this, name); // вызов конструктора-родителя
this.breed = breed;
}

Б. Работу с псевдомассивами (arguments, NodeList):

function sum() {
// arguments — псевдомассив, у него нет метода reduce
return Array.prototype.reduce.call(arguments, (acc, val) => acc + val, 0);
}
sum(1, 2, 3, 4); // 10

В. Обработка событий с сохранением контекста (до появления стрелочных функций):

class Counter {
constructor() {
this.count = 0;
// bind для сохранения контекста при передаче метода как callback
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.count++;
console.log(this.count);
}
}

Г. Карринг и частичное применение аргументов:

function multiply(a, b) {
return a * b;
}

const double = multiply.bind(null, 2);
const triple = multiply.bind(null, 3);

double(5); // 10
triple(5); // 15

Д. Нахождение максимума/минимума в массиве (классический пример apply):

const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers); // 7
// С ES6: Math.max(...numbers)

Важные нюансы:

  • Стрелочные функции нельзя переопределить через call/apply/bind — их this зафиксирован лексически при объявлении
  • bind можно вызывать повторно, но повторный bind не переопределит уже привязанный this
  • При использовании new на функции, созданной через bind, привязанный this игнорируется — создаётся новый объект

Вопрос 3. Что такое Temporal Dead Zone (TDZ) в JavaScript и почему она возникает?

Таймкод: 00:02:54

Ответ собеседника: Неправильный. Кандидат не смог ответить на вопрос.

Правильный ответ:

Temporal Dead Zone (TDZ) — это период времени между началом области видимости, в которой объявлена переменная, и строкой, где эта переменная фактически инициализирована. В течение этого периода обращение к переменной выбрасывает ReferenceError.

Суть проблемы:

Переменные, объявленные через let и const, всплывают (hoist) в начало своей области видимости, в отличие от распространённого заблуждения о том, что они не подвержены поднятию. Однако в отличие от var, который при всплытии получает значение undefined, переменные let/const остаются в «мёртвой зоне» — движок знает о существовании переменной, но не позволяет к ней обращаться до строки объявления.

Механизм работы:

console.log(a); // undefined — var всплыл и инициализирован undefined
var a = 10;

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 20;

Что происходит внутри движка:

Фаза создания контекста:

  • var a → регистрируется в Variable Environment, инициализируется undefined
  • let b → регистрируется в Lexical Environment, остаётся неинициализированной (uninitialized)

Фаза выполнения:

  • До строки let b = 20 переменная b находится в TDZ — любое обращение вызывает ReferenceError
  • После строки let b = 20 TDZ заканчивается, переменная доступна

Практические примеры TDZ:

// Пример 1: TDZ в блочной области видимости
{
console.log(typeof myVar); // "undefined" — var вне TDZ
console.log(typeof myLet); // ReferenceError — myLet в TDZ
let myLet = 'hello';
var myVar = 'world';
}

// Пример 2: TDZ с const (обязательно инициализация)
const PI = 3.14159;
// const FOO; // SyntaxError — const без инициализации недопустим

// Пример 3: TDZ в параметрах по умолчанию
function foo(x = y, y = 2) {
return x + y;
}
foo(); // ReferenceError: y is not defined — y в TDZ при вычислении x

function bar(y = 2, x = y) {
return x + y;
}
bar(); // 4 — y уже инициализирован к моменту вычисления x

// Пример 4: TDZ с typeof
// Обычно typeof для несуществующей переменной возвращает "undefined"
console.log(typeof undeclaredVar); // "undefined" — безопасно
console.log(typeof tdzVar); // ReferenceError — tdzVar объявлена, но в TDZ
let tdzVar = 42;

Почему TDZ существует — причины введения:

1. Раннее обнаружение ошибок: Код, который обращается к переменной до её объявления, почти всегда содержит логическую ошибку. TDZ превращает молчаливое поведение (undefined) в явную ошибку.

2. Предсказуемость const: Без TDZ переменная const имела бы значение undefined до инициализации, что противоречило бы её семантике константы.

3. Устранение путаницы с hoisting: TDZ делает поведение let/const более консистентным и менее подверженным ошибкам по сравнению с var.

TDZ в различных конструкциях:

// Цикл for с let — отдельная область видимости на каждой итерации
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 0, 1, 2
}

// С var было бы 3, 3, 3 — одна область видимости на весь цикл
for (var j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 100); // 3, 3, 3
}

// TDZ в switch-case
switch (x) {
case 1:
let value = 'one';
break;
case 2:
// value находится в TDZ здесь, если x === 2
// let value всплыл на весь switch-блок
break;
}

Сравнение поведения var, let, const:

Характеристикаvarletconst
HoistingДа, с undefinedДа, с TDZДа, с TDZ
Обращение до объявленияundefinedReferenceErrorReferenceError
ПереобъявлениеДаНетНет
ПереопределениеДаДаНет
Область видимостиФункцияБлокБлок

Ключевой вывод: TDZ — это не отсутствие hoisting, а особое состояние переменной после регистрации в области видимости, но до её инициализации. Это одна из важнейших причин, по которой рекомендуется отдавать предпочтение const и let вместо var.

Вопрос 4. Как работает деструктурирующее присваивание (Destructuring Assignment) в JavaScript?

Таймкод: 00:03:01

Ответ собеседника: Правильный. Деструктуризация позволяет распаковать массив или объект. Для массива используются квадратные скобки, для объекта — фигурные скобки с указанием ключей.

Правильный ответ:

Ответ собеседника верен в основе, но тема значительно глубже. Деструктурирующее присваивание — это синтаксис, позволяющий извлекать значения из массивов или свойства из объектов в отдельные переменные с помощью специального паттерна, который «зеркально» отражает структуру данных.

Деструктуризация массивов (по позиции):

const [first, second, third] = [1, 2, 3];
console.log(first); // 1

// Пропуск элементов
const [a, , c] = [1, 2, 3]; // a=1, c=3

// Значения по умолчанию
const [x = 10, y = 20] = [undefined, 5]; // x=10, y=5

// Rest-оператор
const [head, ...tail] = [1, 2, 3, 4]; // head=1, tail=[2, 3, 4]

// Вложенная деструктуризация
const [p, [q, r]] = [1, [2, 3]]; // p=1, q=2, r=3

// Обмен переменных без временной
let m = 1, n = 2;
[m, n] = [n, m]; // m=2, n=1

Деструктуризация объектов (по именам свойств):

const { name, age } = { name: 'Alice', age: 30, city: 'NYC' };

// Переименование переменных
const { name: userName, age: userAge } = { name: 'Bob', age: 25 };
console.log(userName); // 'Bob'

// Значения по умолчанию
const { role = 'guest' } = {}; // role='guest'

// Переименование + значение по умолчанию
const { status: userStatus = 'active' } = {};

// Rest-оператор для оставшихся свойств
const { id, ...rest } = { id: 1, name: 'Charlie', age: 35, role: 'admin' };
// id=1, rest={ name: 'Charlie', age: 35, role: 'admin' }

// Вложенная деструктуризация
const { address: { city } } = { name: 'Dave', address: { city: 'London' } };
console.log(city); // 'London'

Деструктуризация в параметрах функций:

// Объект как параметр — извлечение нужных полей
function greet({ name, age = 18 }) {
return `${name} is ${age} years old`;
}
greet({ name: 'Eve' }); // "Eve is 18 years old"

// Массив как параметр
function getFirst([first]) {
return first;
}
getFirst([10, 20, 30]); // 10

// Комбинированный подход
function processUser({ id, ...data }, [action, ...args]) {
console.log(id, data, action, args);
}
processUser(
{ id: 1, name: 'Frank', role: 'user' },
['update', 'name', 'Frank Jr.']
);

Деструктуризация из итерируемых объектов:

// Строки
const [ch1, ch2] = 'AB'; // ch1='A', ch2='B'

// Map
const map = new Map([['key1', 'val1'], ['key2', 'val2']]);
for (const [key, value] of map) {
console.log(key, value);
}

// Set
const [first, ...rest] = new Set([1, 2, 3]); // first=1, rest=[2, 3]

Важные нюансы:

  • При деструктуризации объекта имена переменных должны совпадать с именами свойств (если не используется переименование)
  • Деструктуризация работает «по значению» для примитивов и «по ссылке» для объектов
  • Если правая часть undefined или null — будет TypeError
  • Можно комбинировать массивную и объектную деструктуризацию:
const [{ name }, [x, y]] = [{ name: 'Grace' }, [10, 20]];
// name='Grace', x=10, y=20

Примечание по терминологии: В вопросе упоминается boxing/unboxing — это терминология из языков вроде C#/Java, где boxing — обёртка примитива в объект, unboxing — извлечение примитива. В JavaScript более корректно говорить именно о «деструктурирующем присваивании» (destructuring assignment).

Вопрос 5. В чём разница между оператором in и методом hasOwnProperty в JavaScript?

Таймкод: 00:04:05

Ответ собеседника: Неправильный. Кандидат не смог ответить на вопрос.

Правильный ответ:

Оба способа проверяют наличие свойства в объекте, но делают это на разных уровнях — это одно из фундаментальных различий, которое напрямую влияет на корректность работы с объектами.

Оператор in — проверяет свойство во всей цепочке прототипов:

const parent = { inherited: 'from parent' };
const child = Object.create(parent);
child.own = 'belongs to child';

console.log('own' in child); // true — собственное свойство
console.log('inherited' in child); // true — унаследованное свойство
console.log('toString' in child); // true — из Object.prototype

hasOwnProperty — проверяет только собственные свойства объекта:

console.log(child.hasOwnProperty('own')); // true
console.log(child.hasOwnProperty('inherited')); // false
console.log(child.hasOwnProperty('toString')); // false

Ключевое отличие наглядно:

const obj = { a: 1 };

console.log('a' in obj); // true — собственное
console.log('constructor' in obj); // true — из прототипа
console.log(obj.hasOwnProperty('a')); // true
console.log(obj.hasOwnProperty('constructor')); // false

Проблема безопасности hasOwnProperty:

Собственный метод hasOwnProperty объекта может быть переопределён или объект может быть создан без прототипа:

// Объект с переопределённым методом
const obj = { hasOwnProperty: () => false };
obj.a = 1;
obj.hasOwnProperty('a'); // false — метод переопределён!

// Объект без прототипа
const bareObj = Object.create(null);
bareObj.key = 'value';
bareObj.hasOwnProperty('key'); // TypeError: not a function

Безопасные альтернативы:

// Вызов метода напрямую из прототипа
Object.prototype.hasOwnProperty.call(obj, 'key');

// Современный способ (ES2022+)
Object.hasOwn(obj, 'key'); // true — рекомендуемый подход

Практические сценарии использования:

А. Итерация по объекту без унаследованных свойств:

const config = { host: 'localhost', port: 3000 };

for (const key in config) {
if (config.hasOwnProperty(key)) { // фильтрация унаследованных
console.log(key, config[key]);
}
}

// Современная альтернатива
Object.keys(config).forEach(key => {
console.log(key, config[key]);
});

Б. Проверка наличия свойства с учётом наследования:

function processResponse(response) {
if ('status' in response) {
// работает даже если status унаследован от базового класса
console.log(response.status);
}
}

В. Работа с объектами, имеющими null-прототип:

const mapLike = Object.create(null);
mapLike.key = 'value';

// Безопасная проверка
if (Object.hasOwn(mapLike, 'key')) {
console.log(mapLike.key);
}

Сравнительная таблица:

КритерийinhasOwnPropertyObject.hasOwn
Собственные свойстваДаДаДа
Унаследованные свойстваДаНетНет
Цепочка прототиповПроверяет всюТолько свойТолько свой
Уязвимость к переопределениюНетДаНет
Работа с Object.create(null)ДаTypeErrorДа

Рекомендация: Используйте Object.hasOwn() (ES2022) как наиболее безопасный способ проверки собственных свойств. Оператор in применяйте, когда нужно учитывать всю цепочку прототипов.

Вопрос 6. Что такое сборщик мусора (Garbage Collector) в JavaScript и как он работает?

Таймкод: 00:04:19

Ответ собеседника: Правильный. Garbage Collector — это механизм JavaScript, который автоматически удаляет из памяти объекты, на которые больше нет ссылок.

Правильный ответ:

Ответ собеседника верен в основе, но описывает лишь одну стратегию. Современные движки JavaScript (V8, SpiderMonkey, JavaScriptCore) используют значительно более сложные и многоуровневые алгоритмы сборки мусора.

Базовый принцип — достижимость (Reachability):

Объект считается «живым», если до него можно добраться от корневых ссылок (roots). Корни — это глобальный объект, текущий стек вызовов (локальные переменные), замыкания. Всё, что недостижимо из корней — мусор.

Основные алгоритмы:

1. Mark-and-Sweep (Пометь и сотри) — основной алгоритм:

Движок проходит два этапа:

  • Mark (пометка): начиная с корней, рекурсивно обходит все достижимые объекты и помечает их как «живые»
  • Sweep (очистка): все непомеченные объекты считаются мусором и их память освобождается
let obj = { data: new Array(1000000) };
obj = null; // объект становится недостижимым → будет собран GC

2. Reference Counting (подсчёт ссылок) — устаревший подход:

Каждый объект хранит счётчик ссылок на него. Когда счётчик достигает нуля — объект удаляется. Проблема — циклические ссылки:

function createCycle() {
const a = {};
const b = {};
a.ref = b;
b.ref = a;
// Даже после выхода из функции a и b ссылаются друг на друга
// Reference counting не может их собрать
// Mark-and-Sweep справится — они недостижимы из корней
}

Поколенческая сборка (Generational Collection) — ключевая оптимизация V8:

Основано на наблюдении: большинство объектов «умирают молодыми» (weak generational hypothesis).

Young Generation (Scavenge) — младшее поколение:

  • Новые объекты попадают в область From-space
  • При заполнении запускается быстрая сборка (Scavenge)
  • Живые объекты копируются в To-space, мусор просто забывается
  • Выжившие объекты с каждой сборкой получают «возраст»

Old Generation (Mark-Sweep-Compact) — старшее поколение:

  • Объекты, пережившие несколько сборок молодого поколения, перемещаются сюда
  • Сборка реже, но медленнее — используется Mark-Sweep и Mark-Compact
  • Большие объекты сразу попадают в Old Generation

Incremental GC и Concurrent GC:

Для минимизации пауз (stop-the-world):

  • Incremental marking: пометка разбивается на маленькие порции, выполняемые между кадрами выполнения кода
  • Concurrent marking: часть работы GC выполняется в фоновых потоках параллельно с основным кодом
  • Parallel scavenging: копирование в молодом поколении распараллеливается по потокам

Пространства памяти в V8:

  • New Space (From/To): для новых объектов, быстрая сборка
  • Old Space: для долгоживущих объектов
  • Large Object Space: для объектов, превышающих размер New Space
  • Code Space: для скомпилированного кода
  • Map Space: для метаданных объектов (hidden classes)

Типичные утечки памяти, которые не решаются GC:

// 1. Забытые обработчики событий
class Component {
constructor() {
this.data = new Array(1000000).fill('x');
document.addEventListener('click', this.handleClick);
}
handleClick = () => console.log(this.data.length);
}
// Даже после удаления DOM-элемента, обработчик держит весь объект

// 2. Забытые таймеры
const id = setInterval(() => {
const hugeData = fetchHugeData();
process(hugeData);
}, 1000);
// clearInterval(id) забыт — hugeData накапливается

// 3. Замыкания, удерживающие большие объектов
function outer() {
const hugeArray = new Array(1000000);
return function inner() {
// даже если hugeArray не используется в inner,
// он может оставаться в памяти (зависит от оптимизации движка)
return 42;
};
}

// 4. Глобальные переменные и Map/Set без очистки
const cache = new Map();
function processItem(id, data) {
cache.set(id, data); // кэш растёт бесконечно без эвакуации
}

Мониторинг и отладка:

// Chrome DevTools → Memory tab
// Heap Snapshot — снимок кучи
// Allocation Timeline — распределение памяти во времени
// Allocation Sampling — профилирование аллокаций

// Node.js — флаги для анализа GC
// node --trace-gc --trace-gc-verbose app.js
// node --inspect app.js → Chrome DevTools

Ключевой вывод: Современный GC в V8 — это сложная многоуровневая система с поколенческой сборкой, инкрементальным и параллельным выполнением, которая минимизирует паузы, но не может предотвратить утечки памяти, вызванные программистом (забытые ссылки, замыкания, кэши без ограничений).

Вопрос 7. Какие статические методы объекта Promise существуют в JavaScript и как они работают?

Таймкод: 00:05:08

Ответ собеседния: Неправный. Кандидат не смог назвать статические методы промисов.

Правильный ответ:

Статические методы Promise — это инструменты для композиции и управления группами асинхронных операций. Они позволяют координировать выполнение нескольких промисов с различными стратегиями обработки результатов.

Promise.all — ждёт все, падает при первой ошибке:

const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json()),
]);

// Если хотя бы один промис отклонён — весь Promise.all отклоняется
// с ошибкой первого отклонённого промиса

Поведение: принимает итерируемый объект промисов, возвращает промис, который выполнится, когда все входные промисы выполнены, или отклонится при первом отклонении. Порядок результатов сохраняется независимо от порядка завершения.

Promise.allSettled — ждёт все, никогда не падает:

const results = await Promise.allSettled([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/broken-endpoint'),
]);

// results = [
// { status: 'fulfilled', value: Response },
// { status: 'fulfilled', value: Response },
// { status: 'rejected', reason: Error }
// ]

const successful = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);

Поведение: всегда выполняется успешно, возвращая массив объектов с описанием результата каждого промиса. Полезен, когда нужно дождаться завершения всех операций и обработать результаты индивидуально.

Promise.race — возвращает результат первого завершённого (успех или ошибка):

// Таймаут для запроса
function fetchWithTimeout(url, ms) {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
),
]);
}

const response = await fetchWithTimeout('/api/data', 5000);

Поведение: возвращает результат (или ошибку) первого завершённого промиса, остальные игнорируются (хотя продолжают выполняться).

Promise.any — возвращает первый успешный, падает если все отклонены:

// Запрос к нескольким зеркалам, используем первый ответ
const mirrors = [
'https://mirror1.example.com/data',
'https://mirror2.example.com/data',
'https://mirror3.example.com/data',
];

try {
const response = await Promise.any(
mirrors.map(url => fetch(url))
);
const data = await response.json();
} catch (error) {
// error — это AggregateError, содержащий все ошибки
console.log(error.errors); // [Error, Error, Error]
}

Поведение: возвращает результат первого успешно завершённого промиса. Отклоняется AggregateError только если все промисы отклонены.

Promise.resolve и Promise.reject — создание уже разрешённых промисов:

// Завернуть значение в промис
const cached = Promise.resolve({ data: 'cached' });
await cached; // { data: 'cached' }

// Создать отклонённый промис
const failure = Promise.reject(new Error('Failed'));
await failure; // throws Error: Failed

// Полезно для кэширования в цепочках
function getData(id) {
if (cache.has(id)) {
return Promise.resolve(cache.get(id)); // синхронное → асинхронное
}
return fetch(`/api/data/${id}`).then(r => r.json());
}

Сравнительная таблица стратегий:

МетодВыполняется когдаОтклоняется когдаИспользование
Promise.allВсе успешныХотя бы один отклонёнПараллельные зависимые запросы
Promise.allSettledВсе завершеныНикогдаПараллельные независимые запросы
Promise.raceПервый завершёнПервый отклонёнТаймауты
Promise.anyПервый успешенВсе отклоненыFallback-запросы к зеркалам

Продвинутые паттерны:

// Параллельное выполнение с ограничением concurrency
async function asyncPool(poolLimit, array, iteratorFn) {
const ret = [];
const executing = new Set();

for (const item of array) {
const p = Promise.resolve().then(() => iteratorFn(item));
ret.push(p);
executing.add(p);

const clean = () => executing.delete(p);
p.then(clean, clean);

if (executing.size >= poolLimit) {
await Promise.race(executing);
}
}

return Promise.all(ret);
}

// Использование: максимум 3 параллельных запроса
const urls = ['url1', 'url2', 'url3', 'url4', 'url5'];
const results = await asyncPool(3, urls, url => fetch(url));

Обработка ошибок в Promise.all:

// Проблема: одна ошибка ломает всё
const results = await Promise.all([
fetch('/api/users'), // OK
fetch('/api/posts'), // 500 ошибка — всё падает
fetch('/api/comments'), // даже не успевает вернуть результат
]);

// Решение 1: оборачивать каждый промис
const results = await Promise.all([
fetch('/api/users').then(r => ({ status: 'ok', data: r }))
.catch(e => ({ status: 'error', error: e })),
fetch('/api/posts').then(r => ({ status: 'ok', data: r }))
.catch(e => ({ status: 'error', error: e })),
]);

// Решение 2: использовать Promise.allSettled
const results = await Promise.allSettled([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments'),
]);

Вопрос 8. Какие методы жизненного цикла классовых компонентов в React существуют?

Таймкод: 00:06:01

Ответ собеседника: Неполный. Кандидат назвал три этапа: монтирование, обновление, размонтирование. Не упомянул четвёртый этап — обработку ошибок (componentDidCatch).

Правильный ответ:

Жизненный цикл классового компонента в React делится на четыре фазы, каждая из которых имеет свои методы. Понимание этих методов критически важно для правильной работы с ресурсами, побочными эффектами и оптимизацией.

1. Mounting (Монтирование) — создание и вставка компонента в DOM:

class Example extends React.Component {
constructor(props) {
super(props);
// Инициализация состояния, привязка методов
this.state = { count: 0 };
}

static getDerivedStateFromProps(props, state) {
// Вызывается перед render, позволяет обновить состояние
// на основе изменений пропсов. Должен вернуть объект или null.
if (props.initialCount !== state.prevInitialCount) {
return {
count: props.initialCount,
prevInitialCount: props.initialCount,
};
}
return null;
}

render() {
// Единственный обязательный метод — возвращает JSX
return <div>{this.state.count}</div>;
}

componentDidMount() {
// Вызывается после вставки в DOM
// Идеально для: запросов к API, подписок, работы с DOM
this.fetchData();
this.subscription = eventBus.subscribe(this.handleEvent);
}
}

2. Updating (Обновление) — повторный рендер из-за изменений пропсов или состояния:

class Example extends React.Component {
static getDerivedStateFromProps(props, state) {
// Вызывается при каждом обновлении, не только при монтировании
return null;
}

shouldComponentUpdate(nextProps, nextState) {
// Возврат false предотвращает ре-рендер
// Используется для оптимизации производительности
return nextState.count !== this.state.count;
}

getSnapshotBeforeUpdate(prevProps, prevState) {
// Вызывается непосредственно перед фиксацией изменений в DOM
// Возвращаемое значение передаётся в componentDidUpdate
// Пример: сохранение позиции скролла
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}

componentDidUpdate(prevProps, prevState, snapshot) {
// Вызывается после обновления DOM
// snapshot — значение из getSnapshotBeforeUpdate
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}

// Условный запрос при изменении пропса
if (prevProps.userId !== this.props.userId) {
this.fetchUser(this.props.userId);
}
}
}

3. Unmounting (Размонтирование) — удаление компонента из DOM:

class Example extends React.Component {
componentWillUnmount() {
// Единственный метод этой фазы
// Очистка: отмена таймеров, отписка от событий, отмена запросов
clearInterval(this.timer);
this.subscription.unsubscribe();
this.controller.abort(); // AbortController для fetch
}
}

4. Error Handling (Обработка ошибок) — перехват ошибок в дереве компонентов:

class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}

static getDerivedStateFromError(error) {
// Вызывается при перехвате ошибки в дочернем компоненте
// Обновляет состояние для отображения fallback UI
return { hasError: true, error };
}

componentDidCatch(error, errorInfo) {
// Логирование ошибки в сервис мониторинга
errorReportingService.log({
error: error.toString(),
componentStack: errorInfo.componentStack,
});
}

render() {
if (this.state.hasError) {
return <FallbackUI error={this.state.error} />;
}
return this.props.children;
}
}

// Использование
<ErrorBoundary>
<App />
</ErrorBoundary>

Устаревшие методы (deprecated в React 16.3+, удалены в React 19):

  • componentWillMount → заменить на constructor или componentDidMount
  • componentWillReceiveProps → заменить на getDerivedStateFromProps
  • componentWillUpdate → заменить на getSnapshotBeforeUpdate

Порядок вызовов методов:

Монтирование: constructorgetDerivedStateFromPropsrendercomponentDidMount

Обновление: getDerivedStateFromPropsshouldComponentUpdaterendergetSnapshotBeforeUpdatecomponentDidUpdate

Размонтирование: componentWillUnmount

Сопоставление с хуками (для перехода на функциональные компоненты):

Классовый методЭквивалентный хук
componentDidMountuseEffect(() => { ... }, [])
componentDidUpdateuseEffect(() => { ... }) или с зависимостями
componentWillUnmountuseEffect(() => { return cleanup }, [])
shouldComponentUpdateReact.memo + кастомный comparator
getDerivedStateFromPropsuseEffect с зависимостью от пропса
getSnapshotBeforeUpdateНет прямого аналога (useLayoutEffect частично)
componentDidCatchНет аналога — классовые компоненты всё ещё нужны для Error Boundaries

Практический пример — DataFetcher с полным жизненным циклом:

class DataFetcher extends React.Component {
constructor(props) {
super(props);
this.state = { data: null, loading: true, error: null };
this.controller = null;
}

componentDidMount() {
this.fetchData();
}

componentDidUpdate(prevProps) {
if (prevProps.url !== this.props.url) {
this.fetchData();
}
}

componentWillUnmount() {
if (this.controller) {
this.controller.abort();
}
}

fetchData() {
this.controller = new AbortController();
this.setState({ loading: true });

fetch(this.props.url, { signal: this.controller.signal })
.then(r => r.json())
.then(data => this.setState({ data, loading: false }))
.catch(error => {
if (error.name !== 'AbortError') {
this.setState({ error, loading: false });
}
});
}

render() {
if (this.state.loading) return <Spinner />;
if (this.state.error) return <Error message={this.state.error.message} />;
return this.props.render(this.state.data);
}
}

Вопрос 9. Что такое componentWillUnmount и для чего он используется?

Таймкод: 00:06:15

Ответ собеседника: Неправильный. Кандидат не слышал о методе componentWillUnmount.

Правильный ответ:

componentWillUnmount — это метод жизненного цикла классового компонента React, который вызывается непосредственно перед удалением компонента из DOM. Это единственный метод фазы Unmounting (размонтирования).

Главное назначение — очистка ресурсов:

Когда компонент удаляется из DOM, он больше не нужен, но побочные эффекты, созданные в componentDidMount или componentDidUpdate, могут продолжать работать и удерживать ссылки в памяти. componentWillUnmount — единственное место, где гарантированно можно отменить все побочные эффекты.

Типичные сценарии использования:

1. Отмена таймеров и интервалов:

class Timer extends React.Component {
componentDidMount() {
this.intervalId = setInterval(() => {
this.setState(prev => ({ seconds: prev.seconds + 1 }));
}, 1000);
}

componentWillUnmount() {
clearInterval(this.intervalId); // без этого — утечка памяти
}

render() {
return <div>{this.state.seconds}s</div>;
}
}

2. Отписка от событий и подписок:

class LiveFeed extends React.Component {
componentDidMount() {
this.unsubscribe = eventBus.subscribe('update', this.handleUpdate);
window.addEventListener('resize', this.handleResize);
}

componentWillUnmount() {
this.unsubscribe();
window.removeEventListener('resize', this.handleResize);
}
}

3. Отмена HTTP-запросов (AbortController):

class DataLoader extends React.Component {
componentDidMount() {
this.controller = new AbortController();
fetch(this.props.url, { signal: this.controller.signal })
.then(r => r.json())
.then(data => this.setState({ data }));
}

componentWillUnmount() {
this.controller.abort(); // отмена запроса при размонтировании
}
}

4. Отмена WebSocket-соединений:

class Chat extends React.Component {
componentDidMount() {
this.ws = new WebSocket('wss://chat.example.com');
this.ws.onmessage = this.handleMessage;
}

componentWillUnmount() {
this.ws.close();
}
}

5. Очистка сторонних библиотек (карты, редакторы, видео):

class MapView extends React.Component {
componentDidMount() {
this.map = new MapLib(this.mapRef.current, {
center: this.props.center,
zoom: 12,
});
}

componentWillUnmount() {
this.map.destroy(); // освобождение ресурсов библиотеки карт
}
}

Чего нельзя делать в componentWillUnmount:

componentWillUnmount() {
// НЕЛЬЗЯ: обновлять состояние — компонент уже будет удалён
this.setState({ data: null }); // предупреждение React

// НЕЛЬЗЯ: вызывать forceUpdate
this.forceUpdate(); // бессмысленно

// МОЖНО: очищать, отменять, отписываться
clearTimeout(this.timeoutId);
}

Эквивалент в функциональных компонентах (useEffect cleanup):

function Timer() {
const [seconds, setSeconds] = useState(0);

useEffect(() => {
const intervalId = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);

// Функция возврата = componentWillUnmount
return () => clearInterval(intervalId);
}, []);

return <div>{seconds}s</div>;
}

Более сложный пример — несколько подписок:

class Dashboard extends React.Component {
componentDidMount() {
// Несколько подписок
this.unsubData = dataStream.subscribe(this.props.dashboardId, this.updateData);
this.unsubAlerts = alertService.subscribe(this.updateAlerts);
this.visibilityHandler = () => this.handleVisibilityChange();
document.addEventListener('visibilitychange', this.visibilityHandler);

this.pollInterval = setInterval(this.pollUpdates, 30000);
}

componentWillUnmount() {
// Очистка ВСЕХ подписок в одном месте
this.unsubData();
this.unsubAlerts();
document.removeEventListener('visibilitychange', this.visibilityHandler);
clearInterval(this.pollInterval);
}

render() {
return (
<div>
<DataPanel data={this.state.data} />
<AlertsPanel alerts={this.state.alerts} />
</div>
);
}
}

Почему это критически важно:

Без componentWillUnmount возникают:

  • Утечки памяти (таймеры, подписки продолжают работать)
  • Ошибки при попытке обновить состояние размонтированного компонента (React выдаст предупреждение)
  • Ненужные сетевые запросы
  • Неожиданное поведение из-за срабатывания обработчиков событий на несуществующем компоненте

Ключевой вывод: componentWillUnmount — это «деструктор» компонента React. Всё, что было создано в componentDidMount или componentDidUpdate и имеет побочные эффекты (таймеры, подписки, соединения, слушатели событий), должно быть очищено в componentWillUnmount.

Вопрос 10. Какие хуки React существуют и для чего они используются?

Таймкод: 00:06:24

Ответ собеседника: Неполный. Кандидат упомянул useState, useEffect и React Hook Form для форм.

Правильный ответ:

Хуки — это функции, позволяющие функциональным компонентам использовать состояние, побочные эффекты и другие возможности React без классов. Ответ собеседника охватил лишь базовые встроенные хуки, но экосистема значительно шире.

Встроенные (built-in) хуки React:

useState — локальное состояние компонента:

const [count, setCount] = useState(0);
const [user, setUser] = useState(null);
const [items, setItems] = useState(() => {
// ленивая инициализация — функция вызывается только при первом рендере
return JSON.parse(localStorage.getItem('items') || '[]');
});

// Функциональное обновление при зависимости от предыдущего состояния
setCount(prev => prev + 1);
setItems(prev => [...prev, newItem]);

useEffect — побочные эффекты:

// Выполняется после каждого рендера (без массива зависимостей)
useEffect(() => {
document.title = `Count: ${count}`;
});

// Выполняется один раз при монтировании (пустой массив)
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(r => r.json())
.then(setData);
return () => controller.abort(); // cleanup = componentWillUnmount
}, []);

// Выполняется при изменении зависимостей
useEffect(() => {
const subscription = eventBus.subscribe(id, handler);
return () => subscription.unsubscribe();
}, [id]);

useContext — доступ к контексту без обёрток:

const ThemeContext = React.createContext('light');

function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click</button>;
}

useReducer — сложное состояние (альтернатива useState):

const initialState = { count: 0, step: 1 };

function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step };
case 'decrement':
return { ...state, count: state.count - state.step };
case 'setStep':
return { ...state, step: action.payload };
default:
return state;
}
}

function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</>
);
}

useMemo — мемоизация вычислений:

const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]); // пересчитывается только при изменении items

useCallback — мемоизация функций:

const handleSubmit = useCallback((data) => {
api.submitForm(id, data);
}, [id]); // функция пересоздаётся только при изменении id

// Без useCallback функция пересоздаётся при каждом рендере,
// что вызывает лишние ре-рендеры дочерних компонентов с React.memo

useRef — изменяемая ссылка, не вызывающая ре-рендер:

const inputRef = useRef(null);
const prevValueRef = useRef();
const renderCount = useRef(0);

useEffect(() => {
prevValueRef.current = value;
renderCount.current += 1;
});

const focusInput = () => inputRef.current.focus();

useLayoutEffect — синхронный эффект до отрисовки браузером:

// Выполняется синхронно после мутаций DOM, но до отрисовки
// Используется для: измерения DOM, синхронных изменений видимого UI
useLayoutEffect(() => {
const rect = tooltipRef.current.getBoundingClientRect();
if (rect.bottom > window.innerHeight) {
setPosition('top');
}
}, [content]);

useId — уникальный ID для доступности (React 18+):

function Form() {
const id = useId();
return (
<>
<label htmlFor={`${id}-name`}>Name</label>
<input id={`${id}-name`} />
<label htmlFor={`${id}-email`}>Email</label>
<input id={`${id}-email`} />
</>
);
}

useDeferredValue — отложенное значение (React 18+):

const deferredQuery = useDeferredQuery(query);
// Позволяет продолжить отвечать на ввод, пока тяжёлый рендер ждёт

useTransition — неблокирующее обновление состояния (React 18+):

const [isPending, startTransition] = useTransition();

function handleSearch(input) {
setInputValue(input); // срочное обновление — инпут отвечает сразу
startTransition(() => {
setSearchQuery(input); // отложенное — фильтрация может подождать
});
}

Пользовательские хуки (Custom Hooks):

Ключевая сила хуков — возможность извлекать логику в переиспользуемые функции:

function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
return JSON.parse(localStorage.getItem(key)) ?? initialValue;
} catch {
return initialValue;
}
});

useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);

return [value, setValue];
}

function useDebounce(value, delay) {
const [debounced, setDebounced] = useState(value);

useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);

return debounced;
}

function useFetch(url) {
const [state, dispatch] = useReducer(fetchReducer, {
data: null,
loading: true,
error: null,
});

useEffect(() => {
const controller = new AbortController();
dispatch({ type: 'FETCH_INIT' });

fetch(url, { signal: controller.signal })
.then(r => r.json())
.then(data => dispatch({ type: 'FETCH_SUCCESS', payload: data }))
.catch(error => {
if (error.name !== 'AbortError') {
dispatch({ type: 'FETCH_ERROR', payload: error });
}
});

return () => controller.abort();
}, [url]);

return state;
}

Популярные сторонние хуки (библиотеки):

  • react-hook-form — производительная работа с формами без лишних ре-рендеров
  • react-query / TanStack Query — серверное состояние, кэширование, рефетчинг
  • zustand / jotai — альтернативные менеджеры состояния
  • usehooks-ts — коллекция готовых типобезопасных хуков

Правила хуков (Rules of Hooks):

  • Вызывайте хуки только на верхнем уровне компонента или пользовательского хука
  • Не вызывайте хуки внутри условий, циклов или вложенных функций
  • Имя пользовательского хука должно начинаться с use

Вопрос 11. Чем отличается useEffect от useLayoutEffect в React?

Таймкод: 00:06:38

Ответ собеседника: Неправильный. Кандидат не знает разницы между useEffect и useLayoutEffect.

Правильный ответ:

Оба хука выполняют побочные эффекты, но принципиально отличаются моментом выполнения относительно отрисовки браузером. Это различие напрямую влияет на пользовательский опыт и производительность.

Порядок выполнения в жизненном цикле:

Рендер (React вычисляет изменения)

Commit (React применяет изменения к DOM)

useLayoutEffect ← выполняется СИНХРОННО здесь, блокирует отрисовку

Браузер отрисовывает пиксели на экране (paint)

useEffect ← выполняется АСИНХРОННО здесь, не блокирует отрисовку

useEffect — асинхронный, не блокирует отрисовку:

useEffect(() => {
// Выполняется ПОСЛЕ того, как браузер отрисовал изменения
// Пользователь уже видит обновлённый экран
console.log('useEffect fired');
}, [dependency]);

useLayoutEffect — синхронный, блокирует отрисовку:

useLayoutEffect(() => {
// Выполняется ДО отрисовки браузером
// Блокирует показ пользователю до завершения
// Позволяет изменить DOM до того, как пользователь увидит промежуточное состояние
console.log('useLayoutEffect fired');
}, [dependency]);

Наглядный пример — мерцание при использовании useEffect:

// ПЛОХО: пользователь видит мерцание
function Tooltip({ content, children }) {
const [position, setPosition] = useState('bottom');
const ref = useRef(null);

useEffect(() => {
// Выполняется ПОСЛЕ отрисовки — пользователь видит tooltip внизу,
// а затем он мгновенно прыгает наверх
const rect = ref.current.getBoundingClientRect();
if (rect.bottom > window.innerHeight) {
setPosition('top'); // вызывает второй рендер → видимый скачок
}
}, [content]);

return <div ref={ref} className={`tooltip-${position}`}>{content}</div>;
}

// ХОРОШО: пользователь никогда не видит промежуточное состояние
function Tooltip({ content, children }) {
const [position, setPosition] = useState('bottom');
const ref = useRef(null);

useLayoutEffect(() => {
// Выполняется ДО отрисовки — пользователь сразу видит правильную позицию
const rect = ref.current.getBoundingClientRect();
if (rect.bottom > window.innerHeight) {
setPosition('top');
}
}, [content]);

return <div ref={ref} className={`tooltip-${position}`}>{content}</div>;
}

Когда использовать useLayoutEffect:

1. Измерения DOM и последующие изменения видимого UI:

function AutoSizeTextarea() {
const textareaRef = useRef(null);
const [height, setHeight] = useState('auto');

useLayoutEffect(() => {
// Измеряем и устанавливаем высоту до отрисовки
setHeight(`${textareaRef.current.scrollHeight}px`);
}, []);

return <textarea ref={textareaRef} style={{ height }} />;
}

2. Синхронные анимации и позиционирование:

function Popover({ anchorRef }) {
const popoverRef = useRef(null);
const [coords, setCoords] = useState({ top: 0, left: 0 });

useLayoutEffect(() => {
const anchorRect = anchorRef.current.getBoundingClientRect();
const popoverRect = popoverRef.current.getBoundingClientRect();
setCoords({
top: anchorRect.bottom,
left: anchorRect.left + anchorRect.width / 2 - popoverRect.width / 2,
});
}, [anchorRef]);

return <div ref={popoverRef} style={{ position: 'absolute', ...coords }} />;
}

3. Предотвращение видимого мерцания (flash of content):

function Modal({ isOpen, children }) {
const [shouldRender, setShouldRender] = useState(false);

useLayoutEffect(() => {
if (isOpen) {
setShouldRender(true); // синхронно до отрисовки — нет мерцания
}
}, [isOpen]);

// cleanup через useEffect (асинхронный — не критично)
useEffect(() => {
if (!isOpen) {
const timer = setTimeout(() => setShouldRender(false), 300);
return () => clearTimeout(timer);
}
}, [isOpen]);

return shouldRender ? <div className="modal">{children}</div> : null;
}

Когда использовать useEffect (в большинстве случаев):

  • Запросы к API
  • Подписки на события
  • Логирование и аналитика
  • Работа с таймерами
  • Любые эффекты, не влияющие на видимый макет

Предупреждение в SSR:

useLayoutEffect выдаёт предупреждение при серверном рендеринге, так как на сервере нет DOM. Решение — использовать useIsomorphicLayoutEffect:

import { useEffect, useLayoutEffect } from 'react';

const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;

Сравнительная таблица:

КритерийuseEffectuseLayoutEffect
ВыполнениеАсинхронно, после paintСинхронно, до paint
Блокирует отрисовкуНетДа
МерцаниеВозможноНевозможно
ПроизводительностьЛучшеХуже (блокирует)
Использование по умолчаниюДа (95% случаев)Только при необходимости
SSR warningНетДа

Ключевой вывод: Используйте useEffect по умолчанию. Переключайтесь на useLayoutEffect только когда видите визуальное мерцание или вам нужно измерить DOM и синхронно обновить состояние до отрисовки браузером. Правило: если эффект не влияет на видимый макет — используйте useEffect.

Вопрос 12. Как передать ref в дочерний компонент в React?

Таймкод: 00:07:08

Ответ собеседника: Неполный. Кандидат описал передачу через props, но не упомянул forwardRef для правильной передачи ref.

Правильный ответ:

Передача ref в дочерний компонент — распространённая задача, для которой React предлагает несколько подходов. Ответ собеседника частично верен, но не охватывает основной механизм.

Проблема: ref не проходит через props напрямую:

// НЕ РАБОТАЕТ как ожидается — ref не является обычным prop
function Parent() {
const inputRef = useRef(null);
return <Child ref={inputRef} />; // ref не попадёт в props компонента Child
}

function Child(props) {
console.log(props.ref); // undefined — ref зарезервирован React
return <div />;
}

React обрабатывает ref особым образом — он не передаётся в компонент как обычный prop.

Решение 1: React.forwardRef — стандартный подход:

const Child = React.forwardRef((props, ref) => {
// ref приходит как второй аргумент
return <input ref={ref} {...props} />;
});

// Использование
function Parent() {
const inputRef = useRef(null);

const focusInput = () => {
inputRef.current.focus();
};

return (
<>
<Child ref={inputRef} placeholder="Enter text" />
<button onClick={focusInput}>Focus</button>
</>
);
}

Решение 2: Передача через именованный prop (обходной путь):

// Если нельзя использовать forwardRef (редкий случай)
function Parent() {
const inputRef = useRef(null);
return <Child innerRef={inputRef} />;
}

function Child({ innerRef }) {
return <input ref={innerRef} />;
}

Этот подход работает, но не рекомендуется, так как нарушает стандартный паттерн React и не поддерживается автоматически TypeScript-типами для ref.

Решение 3: useImperativeHandle — кастомный API компонента:

const FancyInput = React.forwardRef((props, ref) => {
const inputRef = useRef(null);
const [value, setValue] = useState('');

// Ограничиваем и контролируем, что доступно извне
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => setValue(''),
getValue: () => value,
// inputRef.current НЕ доступен — контроль над API
}), [value]);

return (
<input
ref={inputRef}
value={value}
onChange={e => setValue(e.target.value)}
/>
);
});

// Использование
function Parent() {
const fancyRef = useRef(null);

return (
<>
<FancyInput ref={fancyRef} />
<button onClick={() => fancyRef.current.focus()}>Focus</button>
<button onClick={() => fancyRef.current.clear()}>Clear</button>
<button onClick={() => console.log(fancyRef.current.getValue())}>
Log Value
</button>
</>
);
}

Решение 4: Callback ref — функция вместо объекта ref:

function Parent() {
const [element, setElement] = useState(null);

// Функция вызывается при монтировании (с DOM-элементом)
// и при размонтировании (с null)
const measureRef = useCallback((node) => {
if (node !== null) {
setElement(node);
const rect = node.getBoundingClientRect();
console.log('Element size:', rect.width, rect.height);
}
}, []);

return <Child ref={measureRef} />;
}

const Child = React.forwardRef((props, ref) => {
return <div ref={ref}>Measurable content</div>;
});

Решение 5: HOC с forwardRef:

function withLogging(WrappedComponent) {
const Enhanced = React.forwardRef((props, ref) => {
useEffect(() => {
console.log('Component mounted');
return () => console.log('Component unmounted');
}, []);

return <WrappedComponent ref={ref} {...props} />;
});

Enhanced.displayName = `withLogging(${
WrappedComponent.displayName || WrappedComponent.name
})`;

return Enhanced;
}

const EnhancedInput = withLogging(
React.forwardRef((props, ref) => <input ref={ref} {...props} />)
);

Типизация в TypeScript:

// forwardRef с типами
interface FancyInputProps {
placeholder?: string;
defaultValue?: string;
}

interface FancyInputHandle {
focus: () => void;
clear: () => void;
getValue: () => string;
}

const FancyInput = React.forwardRef<FancyInputHandle, FancyInputProps>(
(props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState(props.defaultValue || '');

useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => setValue(''),
getValue: () => value,
}), [value]);

return (
<input
ref={inputRef}
value={value}
onChange={e => setValue(e.target.value)}
placeholder={props.placeholder}
/>
);
}
);

// Использование с типизацией
function Parent() {
const inputRef = useRef<FancyInputHandle>(null);

return (
<FancyInput
ref={inputRef}
placeholder="Type here"
defaultValue="Hello"
/>
);
}

Вложенные forwardRef — передача через несколько уровней:

const Inner = React.forwardRef((props, ref) => (
<input ref={ref} {...props} />
));

const Middle = React.forwardRef((props, ref) => (
<div className="wrapper">
<Inner ref={ref} {...props} />
</div>
));

function Outer() {
const ref = useRef(null);
return <Middle ref={ref} />;
}

Сравнение подходов:

ПодходКогда использоватьМинусы
forwardRefСтандартный случай — доступ к DOM-элементуНет
innerRef propLegacy-код без forwardRefНе стандартный, нет типизации ref
useImperativeHandleНужно ограничить API компонентаБольше кода
callback refНужно реагировать на монтирование/размонтированиеВызывается дважды в strict mode

Ключевой вывод: Основной способ передачи ref в дочерний компонент — React.forwardRef. Он позволяет принимать ref как второй аргумент и привязывать его к нужному DOM-элементу. Для контроля над тем, что доступно извне, комбинируйте с useImperativeHandle.

Вопрос 13. Что такое React.forwardRef и для чего он используется?

Таймкод: 00:07:46

Ответ собеседника: Неправильный. Кандидат не слышал о forwardRef.

Правильный ответ:

React.forwardRef — это функция высшего порядка (Higher-Order Component), которая позволяет компоненту принимать ref от родителя и передавать его дочернему DOM-элементу или другому компоненту. Это основной механизм для получения доступа к DOM-узлам дочерних компонентов.

Проблема, которую решает forwardRef:

В React ref — это специальный зарезервированный prop. Он не передаётся в компонент через props, в отличие от обычных пропсов. Это означает, что обычный функциональный компонент не может принять ref и привязать его к своему DOM-элементу.

// НЕ РАБОТАЕТ — ref не попадает в props
function MyInput(props) {
console.log(props.ref); // undefined
return <input {...props} />;
}

function Parent() {
const ref = useRef(null);
return <MyInput ref={ref} />; // React выдаст предупреждение
}

Решение с forwardRef:

const MyInput = React.forwardRef((props, ref) => {
// ref приходит как ВТОРОЙ аргумент, не через props
return <input ref={ref} {...props} />;
});

function Parent() {
const inputRef = useRef(null);

useEffect(() => {
inputRef.current.focus(); // работает!
}, []);

return <MyInput ref={inputRef} placeholder="Auto-focused" />;
}

Сигнатура:

React.forwardRef((props, ref) => ReactElement)

Компонент, обёрнутый в forwardRef, принимает два аргумента:

  1. props — обычные пропсы компонента
  2. ref — ссылка от родительского компонента

Практические сценарии использования:

1. Обёртки над нативными элементами (UI-библиотеки):

const Button = React.forwardRef(({ variant, size, children, ...props }, ref) => {
return (
<button
ref={ref}
className={`btn btn-${variant} btn-${size}`}
{...props}
>
{children}
</button>
);
});

Button.displayName = 'Button';

// Использование в библиотеке компонентов
function Form() {
const submitRef = useRef(null);

return (
<>
<input name="email" />
<Button ref={submitRef} variant="primary" size="large">
Submit
</Button>
</>
);
}

2. Композиция с useImperativeHandle — контролируемый API:

const Modal = React.forwardRef(({ children, ...props }, ref) => {
const dialogRef = useRef(null);

useImperativeHandle(ref, () => ({
open: () => dialogRef.current.showModal(),
close: () => dialogRef.current.close(),
}), []);

return (
<dialog ref={dialogRef} {...props}>
{children}
</dialog>
);
});

// Родитель управляет модалкой через ограниченный API
function Page() {
const modalRef = useRef(null);

return (
<>
<button onClick={() => modalRef.current.open()}>Open Modal</button>
<Modal ref={modalRef}>
<p>Modal content</p>
<button onClick={() => modalRef.current.close()}>Close</button>
</Modal>
</>
);
}

3. Интеграция с библиотеками (react-hook-form, react-spring):

// react-hook-form ожидает ref для регистрации поля
const FormInput = React.forwardRef(({ label, error, ...props }, ref) => (
<div className="form-group">
<label>{label}</label>
<input ref={ref} {...props} />
{error && <span className="error">{error}</span>}
</div>
));

function MyForm() {
const { register, handleSubmit } = useForm();

return (
<form onSubmit={handleSubmit(onSubmit)}>
<FormInput label="Email" {...register('email')} />
<FormInput label="Password" type="password" {...register('password')} />
</form>
);
}

4. Передача ref через несколько уровней (ref forwarding chain):

const BaseInput = React.forwardRef((props, ref) => (
<input ref={ref} {...props} />
));

const FancyInput = React.forwardRef(({ icon, ...props }, ref) => (
<div className="fancy-input">
{icon && <span className="icon">{icon}</span>}
<BaseInput ref={ref} {...props} />
</div>
));

function App() {
const ref = useRef(null);
return <FancyInput ref={ref} icon="🔍" placeholder="Search" />;
}

Типизация в TypeScript:

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
}

// Типизированный forwardRef
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ label, error, ...props }, ref) => (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
{error && <span>{error}</span>}
</div>
)
);

// Тип для imperative handle
interface InputHandle {
focus: () => void;
getValue: () => string;
}

const TypedInput = React.forwardRef<InputHandle, InputProps>(
(props, ref) => {
const innerRef = useRef<HTMLInputElement>(null);

useImperativeHandle(ref, () => ({
focus: () => innerRef.current?.focus(),
getValue: () => innerRef.current?.value || '',
}));

return <input ref={innerRef} {...props} />;
}
);

Важные нюансы:

  • displayName рекомендуется устанавливать для отладки в React DevTools
  • forwardRef работает только с DOM-элементами или другими forwardRef компонентами — нельзя передать ref в обычный функциональный компонент без forwardRef
  • В React 19+ появилась возможность принимать ref как обычный prop без forwardRef (экспериментально)

Ключевой вывод: React.forwardRef — это стандартный механизм для передачи ref через компонент к DOM-элементу. Он необходим при создании переиспользуемых UI-компонентов, интеграции с библиотеками форм, анимаций и другими инструментами, которым требуется прямой доступ к DOM-узлам.

Вопрос 14. Как вывести массив объектов с полями name и children в виде вложенного списка в React?

Таймкод: 00:10:35

Ответ собеседника: Правильный. Кандидат использовал рекурсивный подход с map, реализовал рекурсивную функцию с проверкой наличия children.

Правильный ответ:

Задача на рекурсивный рендеринг — классическая для интервью. Решение кандидата верно по сути. Рассмотрим полную реализацию с различными подходами и улучшениями.

Базовое решение — рекурсивный компонент:

const data = [
{
name: 'Fruits',
children: [
{ name: 'Apple' },
{ name: 'Banana' },
{
name: 'Citrus',
children: [
{ name: 'Orange' },
{ name: 'Lemon' },
],
},
],
},
{
name: 'Vegetables',
children: [
{ name: 'Carrot' },
{ name: 'Potato' },
],
},
];

function NestedList({ items }) {
return (
<ul>
{items.map((item, index) => (
<li key={item.name || index}>
{item.name}
{item.children && item.children.length > 0 && (
<NestedList items={item.children} />
)}
</li>
))}
</ul>
);
}

function App() {
return <NestedList items={data} />;
}

Улучшенная версия с отступами (лесенка):

function NestedList({ items, depth = 0 }) {
return (
<ul style={{ listStyleType: depth === 0 ? 'disc' : 'circle' }}>
{items.map((item, index) => (
<li
key={item.name || index}
style={{ marginLeft: `${depth * 20}px` }}
>
{item.name}
{item.children?.length > 0 && (
<NestedList items={item.children} depth={depth + 1} />
)}
</li>
))}
</ul>
);
}

Версия с раскрытием/сворачиванием (collapsible tree):

function TreeNode({ item, depth = 0 }) {
const [isOpen, setIsOpen] = useState(depth < 1); // первый уровень открыт по умолчанию
const hasChildren = item.children?.length > 0;

return (
<div style={{ marginLeft: `${depth * 20}px` }}>
<div
onClick={() => hasChildren && setIsOpen(!isOpen)}
style={{
cursor: hasChildren ? 'pointer' : 'default',
fontWeight: hasChildren ? 'bold' : 'normal',
}}
>
{hasChildren && (isOpen ? '▼ ' : '▶ ')}
{item.name}
</div>
{isOpen && hasChildren && (
<NestedList items={item.children} depth={depth + 1} />
)}
</div>
);
}

function NestedList({ items, depth }) {
return (
<>
{items.map((item, index) => (
<TreeNode key={item.name || index} item={item} depth={depth} />
))}
</>
);
}

Версия с отдельным компонентом TreeNode:

function TreeNode({ node, depth = 0 }) {
const [expanded, setExpanded] = useState(true);
const hasChildren = node.children?.length > 0;

return (
<div className="tree-node">
<div
className="tree-node__label"
style={{ paddingLeft: `${depth * 16}px` }}
onClick={() => hasChildren && setExpanded(!expanded)}
>
{hasChildren ? (
<span className="tree-node__toggle">
{expanded ? '−' : '+'}
</span>
) : (
<span className="tree-node__spacer"></span>
)}
<span>{node.name}</span>
</div>

{expanded && hasChildren && (
<div className="tree-node__children">
{node.children.map((child, i) => (
<TreeNode key={child.name || i} node={child} depth={depth + 1} />
))}
</div>
)}
</div>
);
}

function TreeView({ data }) {
return (
<div className="tree-view">
{data.map((node, i) => (
<TreeNode key={node.name || i} node={node} />
))}
</div>
);
}

Версия на TypeScript:

interface TreeNode {
name: string;
children?: TreeNode[];
}

interface TreeViewProps {
data: TreeNode[];
}

interface TreeNodeProps {
node: TreeNode;
depth?: number;
}

const TreeNode: React.FC<TreeNodeProps> = ({ node, depth = 0 }) => {
const [expanded, setExpanded] = useState<boolean>(true);
const hasChildren = node.children !== undefined && node.children.length > 0;

return (
<div style={{ marginLeft: `${depth * 20}px` }}>
<div
onClick={() => hasChildren && setExpanded(!expanded)}
style={{ cursor: hasChildren ? 'pointer' : 'default' }}
>
{hasChildren ? (expanded ? '▼' : '▶') : '•'} {node.name}
</div>
{expanded && hasChildren && (
<TreeView data={node.children!} />
)}
</div>
);
};

const TreeView: React.FC<TreeViewProps> = ({ data }) => (
<>
{data.map((node, index) => (
<TreeNode key={node.name || index} node={node} />
))}
</>
);

Оптимизация с memo для больших деревьев:

const TreeNode = React.memo(({ node, depth = 0 }: TreeNodeProps) => {
const [expanded, setExpanded] = useState(true);
const hasChildren = node.children?.length > 0;

return (
<div style={{ marginLeft: `${depth * 20}px` }}>
<span onClick={() => hasChildren && setExpanded(!expanded)}>
{hasChildren && (expanded ? '▼' : '▶')} {node.name}
</span>
{expanded && hasChildren && <TreeView data={node.children!} />}
</div>
);
});

CSS для визуального оформления лесенки:

.tree-view {
font-family: monospace;
}

.tree-node__label {
padding: 2px 0;
}

.tree-node__label:hover {
background-color: #f0f0f0;
}

.tree-node__toggle {
display: inline-block;
width: 16px;
text-align: center;
font-size: 12px;
}

.tree-node__spacer {
display: inline-block;
width: 16px;
text-align: center;
color: #999;
}

Ключевые моменты:

  • Рекурсия — естественный подход для вложенных структур данных
  • key обязателен при рендере списков через map
  • Проверка item.children?.length > 0 предотвращает рендер пустых <ul>
  • depth параметр позволяет визуально отображать вложенность
  • React.memo полезен для оптимизации при большом количестве узлов
  • Для очень глубоких деревьев (тысячи узлов) стоит рассмотреть виртуализацию (react-window, react-virtuoso)

Вопрос 15. Как реализовать функцию, которая при последовательном вызове выводит в консоль 1, 2, 3?

Таймкод: 00:22:46

Ответ собеседника: Правильный. Кандидат использовал замыкание для хранения счётчика. Задача выполнена корректно.

Правильный ответ:

Задача на замыкание (closure) — одна из классических задач на собеседованиях. Суть: функция должна «запоминать» состояние между вызовами без использования внешних переменных.

Решение 1: Замыкание с самовызывающейся функцией (IIFE):

const printNumber = (function () {
let counter = 0;

return function () {
counter += 1;
console.log(counter);
return counter;
};
})();

printNumber(); // 1
printNumber(); // 2
printNumber(); // 3

Решение 2: Замыкание через фабричную функцию:

function createCounter(start = 0, step = 1) {
let counter = start;

return function () {
counter += step;
console.log(counter);
return counter;
};
}

const printNumber = createCounter(0, 1);
printNumber(); // 1
printNumber(); // 2
printNumber(); // 3

// Можно создать несколько независимых счётчиков
const counterFromTen = createCounter(10, 2);
counterFromTen(); // 12
counterFromTen(); // 14

Решение 3: С ограничением до 3 (как в условии задачи):

const printNumber = (function () {
let counter = 0;

return function () {
if (counter >= 3) {
console.log('Limit reached');
return counter;
}
counter += 1;
console.log(counter);
return counter;
};
})();

printNumber(); // 1
printNumber(); // 2
printNumber(); // 3
printNumber(); // "Limit reached"
printNumber(); // "Limit reached"

Решение 4: Сброс счётчика после достижения лимита (циклический):

const createCyclicPrinter = (max) => {
let counter = 0;

return function () {
counter = (counter % max) + 1;
console.log(counter);
return counter;
};
};

const printNumber = createCyclicPrinter(3);
printNumber(); // 1
printNumber(); // 2
printNumber(); // 3
printNumber(); // 1 — цикл начинается заново
printNumber(); // 2

Решение 5: С использованием объекта с методом:

const printer = {
counter: 0,
print() {
this.counter += 1;
console.log(this.counter);
return this.counter;
},
reset() {
this.counter = 0;
},
};

printer.print(); // 1
printer.print(); // 2
printer.print(); // 3
printer.reset();
printer.print(); // 1

Решение 6: Генераторная функция:

function* numberGenerator() {
let counter = 0;
while (counter < 3) {
counter += 1;
yield counter;
}
}

const gen = numberGenerator();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
console.log(gen.next().value); // { value: undefined, done: true }

Решение 7: Карринг с цепочкой вызовов (альтернативная интерпретация задачи):

Если задача подразумевает цепочку вызовов вида printNumber()()():

function printNumber(n = 1) {
console.log(n);
if (n < 3) {
return function () {
return printNumber(n + 1);
};
}
return null;
}

printNumber()()(); // 1, 2, 3

Решение 8: Класс (для сравнения):

class Counter {
#counter = 0; // приватное поле

next() {
this.#counter += 1;
console.log(this.#counter);
return this.#counter;
}

get value() {
return this.#counter;
}

reset() {
this.#counter = 0;
}
}

const counter = new Counter();
counter.next(); // 1
counter.next(); // 2
counter.next(); // 3

Как это работает — механизм замыкания:

// Пошагово:
const printNumber = (function () {
// Эта область видимости создаётся один раз при определении printNumber
let counter = 0; // ← переменная «заперта» в замыкании

return function () {
// Эта функция имеет доступ к counter даже после того,
// как внешняя IIFE завершила выполнение
counter += 1;
console.log(counter);
};
})();

// Каждый вызов printNumber() обращается к одной и той же переменной counter

Почему это работает:

  • IIFE выполняется один раз, создавая лексическое окружение с переменной counter
  • Возвращаемая функция «захватывает» ссылку на это окружение — это и есть замыкание
  • Между вызовами переменная counter сохраняет своё значение
  • Переменная недоступна извне — инкапсуляция состояния

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

Вопрос 16. Как реализовать функцию reduce для массива с использованием замыканий?

Таймкод: 00:25:59

Ответ собеседования: Неполный. Кандидат допустил ошибку в порядке параметров и получил неверный результат. Задача не завершена корректно.

Правильный ответ:

reduce — это функция высшего порядка, которая последовательно обрабатывает элементы массива, аккумулируя результат в одном значении. Реализация требует понимания замыканий, итерации и работы с аккумулятором.

Стандартная реализация Array.prototype.reduce:

function reduce(array, callback, initialValue) {
// Определяем начальный индекс и начальное значение аккумулятора
let accumulator;
let startIndex;

if (arguments.length >= 3) {
// Если initialValue передан — начинаем с индекса 0
accumulator = initialValue;
startIndex = 0;
} else {
// Если initialValue НЕ передан — используем первый элемент массива
if (array.length === 0) {
throw new TypeError('Reduce of empty array with no initial value');
}
accumulator = array[0];
startIndex = 1;
}

for (let i = startIndex; i < array.length; i++) {
// callback(accumulator, currentValue, index, array)
accumulator = callback(accumulator, array[i], i, array);
}

return accumulator;
}

// Примеры использования
reduce([1, 2, 3, 4], (acc, val) => acc + val, 0); // 10
reduce([1, 2, 3, 4], (acc, val) => acc + val); // 10
reduce([1, 2, 3, 4], (acc, val) => acc * val, 1); // 24
reduce(['a', 'b', 'c'], (acc, val) => acc + val, ''); // 'abc'

Реализация как метода массива (полифилл):

if (!Array.prototype.myReduce) {
Array.prototype.myReduce = function (callback, initialValue) {
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}

const array = this;
const length = array.length;
let accumulator;
let startIndex;

if (arguments.length >= 2) {
accumulator = initialValue;
startIndex = 0;
} else {
if (length === 0) {
throw new TypeError('Reduce of empty array with no initial value');
}
accumulator = array[0];
startIndex = 1;
}

for (let i = startIndex; i < length; i++) {
if (Object.prototype.hasOwnProperty.call(array, i)) {
accumulator = callback(accumulator, array[i], i, array);
}
}

return accumulator;
};
}

Реализация с использованием замыкания (фабрика):

function createReducer(callback, initialValue) {
// Замыкание: accumulator сохраняется между вызовами
let accumulator = initialValue;
let isFirstCall = arguments.length < 2;

return function (currentValue, index, array) {
if (isFirstCall) {
accumulator = currentValue;
isFirstCall = false;
return accumulator;
}
accumulator = callback(accumulator, currentValue, index, array);
return accumulator;
};
}

// Использование
const sumReducer = createReducer((acc, val) => acc + val, 0);
[1, 2, 3, 4].forEach(val => sumReducer(val)); // последний вызов вернёт 10

Реализация через рекурсию с замыканием:

function reduceRecursive(array, callback, initialValue) {
let startIndex = 0;
let accumulator;

if (arguments.length >= 3) {
accumulator = initialValue;
} else {
if (array.length === 0) {
throw new TypeError('Reduce of empty array with no initial value');
}
accumulator = array[0];
startIndex = 1;
}

function helper(index, acc) {
if (index >= array.length) {
return acc;
}
const newAcc = callback(acc, array[index], index, array);
return helper(index + 1, newAcc); // рекурсивный вызов
}

return helper(startIndex, accumulator);
}

console.log(reduceRecursive([1, 2, 3, 4], (acc, val) => acc + val, 0)); // 10

Реализация через замыкание с накоплением (curried reduce):

function reduceWithClosure(callback, initialValue) {
let accumulator = initialValue;
let hasInitial = arguments.length >= 2;

function reducer(value) {
if (!hasInitial) {
accumulator = value;
hasInitial = true;
} else {
accumulator = callback(accumulator, value);
}
return reducer; // возвращаем себя для цепочки вызовов
}

reducer.valueOf = () => accumulator;
reducer.toString = () => String(accumulator);
reducer.getResult = () => accumulator;

return reducer;
}

// Цепочка вызовов
const result = reduceWithClosure((a, b) => a + b, 0)(1)(2)(3)(4).getResult();
console.log(result); // 10

Практические примеры использования reduce:

// Сумма чисел
[1, 2, 3, 4].reduce((acc, val) => acc + val, 0); // 10

// Группировка по свойству
const people = [
{ name: 'Alice', age: 25, city: 'NYC' },
{ name: 'Bob', age: 30, city: 'LA' },
{ name: 'Charlie', age: 25, city: 'NYC' },
];

const groupedByCity = people.reduce((acc, person) => {
const key = person.city;
if (!acc[key]) acc[key] = [];
acc[key].push(person);
return acc;
}, {});
// { NYC: [Alice, Charlie], LA: [Bob] }

// Плоский массив из вложенного
const nested = [[1, 2], [3, 4], [5, 6]];
nested.reduce((acc, val) => acc.concat(val), []); // [1, 2, 3, 4, 5, 6]

// Подсчёт вхождений
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const count = fruits.reduce((acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
}, {});
// { apple: 3, banana: 2, orange: 1 }

// Пайплайн функций
const pipe = (...fns) => (initialValue) =>
fns.reduce((value, fn) => fn(value), initialValue);

const add5 = x => x + 5;
const multiply2 = x => x * 2;
const toString = x => String(x);

const transform = pipe(add5, multiply2, toString);
transform(10); // "30" → (10 + 5) * 2 = 30 → "30"

// Максимум с индексом
const numbers = [3, 7, 2, 9, 4];
const maxIndex = numbers.reduce((maxIdx, val, idx, arr) => {
return val > arr[maxIdx] ? idx : maxIdx;
}, 0);
// 3 (индекс числа 9)

Типичные ошибки при реализации:

// ОШИБКА 1: Неправильный порядок параметров в callback
// Правильно: callback(accumulator, currentValue, index, array)
// Неправильно: callback(currentValue, accumulator, ...)

// ОШИБКА 2: Необработанный случай пустого массива без initialValue
function badReduce(arr, cb, init) {
let acc = init; // если init не передан — acc = undefined
for (let i = 0; i < arr.length; i++) {
acc = cb(acc, arr[i]);
// undefined + 1 = NaN
}
return acc;
}
badReduce([1, 2, 3], (a, b) => a + b); // NaN!

// ОШИБКА 3: Неправильный начальный индекс при отсутствии initialValue
// Нужно начинать с индекса 1, а не 0

// ОШИБКА 4: Мутация аккумулятора для объектов
const result = [1, 2, 3].reduce((acc, val) => {
acc.push(val); // мутируем исходный массив!
return acc;
}, []);

Сигнатура callback:

callback(accumulator, currentValue, currentIndex, sourceArray)
ПараметрОписание
accumulatorНакопленный результат предыдущих вызовов
currentValueТекущий обрабатываемый элемент
currentIndexИндекс текущего элемента
sourceArrayИсходный массив

Ключевой вывод: При реализации reduce критически важно: правильно обрабатывать случай отсутствия initialValue (брать первый элемент и начинать с индекса 1), правильный порядок параметров в callback (аккумулятор первым), и выбрасывать ошибку при пустом массиве без начального значения.