Mock Собеседование для Junior Frontend разработчика
Сегодня мы разберём прохождение кандидатом Олегом тренировочного собеседования на позицию фронтенд-разработчика: он демонстрирует базовое понимание 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, инициализируетсяundefinedlet b→ регистрируется в Lexical Environment, остаётся неинициализированной (uninitialized)
Фаза выполнения:
- До строки
let b = 20переменнаяbнаходится в TDZ — любое обращение вызываетReferenceError - После строки
let b = 20TDZ заканчивается, переменная доступна
Практические примеры 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:
| Характеристика | var | let | const |
|---|---|---|---|
| Hoisting | Да, с undefined | Да, с TDZ | Да, с TDZ |
| Обращение до объявления | undefined | ReferenceError | ReferenceError |
| Переобъявление | Да | Нет | Нет |
| Переопределение | Да | Да | Нет |
| Область видимости | Функция | Блок | Блок |
Ключевой вывод: 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);
}
Сравнительная таблица:
| Критерий | in | hasOwnProperty | Object.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илиcomponentDidMountcomponentWillReceiveProps→ заменить наgetDerivedStateFromPropscomponentWillUpdate→ заменить наgetSnapshotBeforeUpdate
Порядок вызовов методов:
Монтирование:
constructor → getDerivedStateFromProps → render → componentDidMount
Обновление:
getDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate → componentDidUpdate
Размонтирование:
componentWillUnmount
Сопоставление с хуками (для перехода на функциональные компоненты):
| Классовый метод | Эквивалентный хук |
|---|---|
componentDidMount | useEffect(() => { ... }, []) |
componentDidUpdate | useEffect(() => { ... }) или с зависимостями |
componentWillUnmount | useEffect(() => { return cleanup }, []) |
shouldComponentUpdate | React.memo + кастомный comparator |
getDerivedStateFromProps | useEffect с зависимостью от пропса |
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;
Сравнительная таблица:
| Критерий | useEffect | useLayoutEffect |
|---|---|---|
| Выполнение | Асинхронно, после 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 prop | Legacy-код без 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, принимает два аргумента:
props— обычные пропсы компонента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 DevToolsforwardRefработает только с 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 (аккумулятор первым), и выбрасывать ошибку при пустом массиве без начального значения.
