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

РЕАЛЬНОЕ СОБЕСЕДОВАНИЕ / Middle Frontend разработчик KAMAZ Digital - от 170 тыс

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

Вопрос 1. Опишите текущий опыт работы и проекты, над которыми вы работали.

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

Ответ собеседника: Правильный. Работаю в компании Каша на проекте документооборота, сейчас на поддержке из-за завершения проекта. Занимался фронтенд-разработкой компонентов и локализацией на три языка с использованием i18n.

Правильный ответ: В текущей компании я участвую в разработке системы электронного документооборота (СЭД) для корпоративных клиентов. Проект включает как backend на Go, так и frontend на современном стеке (React/Typescript).

Backend-часть на Go:

  • Разрабатывал микросервисную архитектуру с использованием gRPC и REST API
  • Реализовал модуль обработки документов с параллельной конвертацией форматов (PDF/DOCX) через worker pool:
func processDocuments(docs <-chan Document, workers int) {
var wg sync.WaitGroup
wg.Add(workers)

for i := 0; i < workers; i++ {
go func() {
defer wg.Done()
for doc := range docs {
converted := convertFormat(doc)
saveToStorage(converted)
}
}()
}
wg.Wait()
}
  • Оптимизировал поиск по документам через интеграцию ElasticSearch, сократив время ответа с 2.5s до 120ms
  • Настроил CI/CD пайплайн с тестированием (unit, integration), линтерами и деплоем в Kubernetes

Frontend-часть:

  • Создал систему динамических форм для метаданных документов с валидацией
  • Реализовал мультиязычную поддержку (русский/английский/казахский) через i18next с lazy-loading локалей
  • Разработал компонент предпросмотра документов с аннотациями

Особенности проекта:

  • Работа с большими файлами (до 2GB) через streaming-обработку
  • Интеграция с 1C и SAP через адаптеры
  • Реализация ролевой модели доступа (RBAC) с каскадными политиками

Сейчас проект перешел в стадию поддержки, основные функции завершены. Работаю над оптимизацией производительности и рефакторингом legacy-кода.

Совет для интервью: Всегда связывайте технические детали с бизнес-ценностью. Например: "Оптимизация поиска позволила обрабатывать на 40% больше запросов без увеличения серверных мощностей".

Вопрос 2. Перечислите типы данных в JavaScript и объясните их особенности.

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

Ответ собеседника: Неполный. Названы string, number, boolean, undefined, null, object. Не упомянуты symbol и bigint.

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

Примитивные типы (7 видов):

  1. string - текстовые данные (UTF-16). Особенности:
    const name = "Alice";
    const template = `Hello ${name}`; // Интерполяция
  2. number - числа с плавающей точкой (IEEE 754). Включает Infinity, NaN:
    console.log(0.1 + 0.2); // 0.30000000000000004
    console.log(1 / 0); // Infinity
  3. boolean - логические значения true/false.
  4. undefined - значение неинициализированных переменных:
    let a;
    console.log(a); // undefined
  5. null - явное "пустое" значение (историческая особенность typeof null === 'object').
  6. symbol (ES6) - уникальные идентификаторы:
    const id = Symbol('id');
    const obj = {[id]: 123};
  7. bigint (ES2020) - числа произвольной длины:
    const bigNum = 9007199254740991n;
    console.log(bigNum + 1n); // 9007199254740992n

Объектные типы:

  • object - коллекции свойств (включая массивы, функции, даты):
    const obj = { key: 'value' };
    const arr = [1, 2, 3]; // typeof arr === 'object'
    function test() {} // typeof test === 'function'

Ключевые особенности:

  1. Динамическая типизация:
    let x = 'text'; // string
    x = 42; // number
  2. Преобразование типов:
    console.log('5' + 3);  // '53' (конкатенация)
    console.log('5' - 3); // 2 (преобразование в число)
  3. Сравнение значений:
    console.log(1 == '1');   // true (нестрогое)
    console.log(1 === '1'); // false (строгое)
  4. Особенности typeof:
    typeof null        // 'object' (историческая ошибка)
    typeof function(){} // 'function'

Совет для интервью: Всегда поясняйте разницу между:

  • undefined (переменная объявлена, но не инициализирована) и null (явное "ничего")
  • Примитивами (хранятся по значению, иммутабельны) и объектами (хранятся по ссылке, мутабельны)
  • Явным (String(), Number()) и неявным преобразованием типов (операторы +, !!)

Вопрос 3. Объясните, что такое Promise в JavaScript и как они работают.

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

Ответ собеседника: Неполный. Описан как объект для асинхронных операций с состояниями pending, fulfilled, rejected. Не упомянуты микротаски и цепочки вызовов.

Правильный ответ: Promise (Обещание) — это специальный объект в JavaScript, представляющий результат асинхронной операции, который может быть доступен сейчас или в будущем.

Основные характеристики:

  1. Состояния Promise:

    • pending — начальное состояние (операция в процессе)
    • fulfilled — успешное выполнение (с результатом)
    • rejected — выполнено с ошибкой
  2. Создание Promise:

    const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
    Math.random() > 0.5
    ? resolve('Успех!')
    : reject(new Error('Ошибка'));
    }, 1000);
    });
  3. Цепочки вызовов:

    • then() — обработка успешного результата:
      fetch('/data')
      .then(response => response.json())
      .then(data => console.log(data))
    • catch() — обработка ошибок (перехватывает ошибки во всей цепочке):
      promise
      .then(processData)
      .catch(err => console.error('Ошибка:', err));
    • finally() — выполнение кода независимо от результата:
      loadData()
      .finally(() => hideLoader());
  4. Очередь микротасков (Microtask Queue):

    • Коллбэки then/catch/finally попадают в очередь микротасков
    • Выполняются перед следующей итерацией цикла событий (Event Loop)
    • Пример приоритета:
      setTimeout(() => console.log('setTimeout'), 0);
      Promise.resolve().then(() => console.log('Promise'));
      // Вывод: Promise → setTimeout
  5. Комбинирование Promise:

    • Promise.all([...]) — ожидает выполнения всех промисов:
      Promise.all([fetch(url1), fetch(url2)])
      .then(([res1, res2]) => ...)
    • Promise.race([...]) — возвращает первый завершенный промис
    • Promise.allSettled([...]) — ждет завершения всех, независимо от результата
  6. Обработка ошибок:

    • Необработанные ошибки приводят к UnhandledPromiseRejectionWarning
    • Рекомендуется всегда добавлять .catch() в конце цепочки

Пример полного сценария:

function asyncOperation() {
return new Promise((resolve, reject) => {
// Имитация асинхронной операции
setTimeout(() => {
const success = Math.random() > 0.3;
success ? resolve('Данные получены') : reject('Ошибка сети');
}, 500);
});
}

asyncOperation()
.then(data => {
console.log(data);
return processData(data); // Возвращает новый Promise
})
.then(processed => saveToDB(processed))
.catch(err => {
console.error('Цепочка прервана:', err);
return fallbackData(); // Возобновление цепочки
})
.finally(() => {
cleanupResources();
});

Ключевые отличия от колбэков:

  1. Избегание "ада колбэков" (Callback Hell)
  2. Чёткое разделение успешного сценария и обработки ошибок
  3. Возможность комбинирования асинхронных операций

Совет для интервью: Всегда упоминайте:

  • Разницу между микротасками и макротасками (setTimeout/setInterval)
  • Важность возврата значений в .then() для продолжения цепочки
  • Проблемы с потерей контекста и способы их решения (стрелочные функции)

Вопрос 4. Объясните разницу между операторами == (нестрогое равенство) и === (строгое равенство) в JavaScript.

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

Ответ собеседника: Правильный. Тройное равенство выполняет строгое сравнение без приведения типов, двойное — с приведением типов.

Правильный ответ: Операторы сравнения в JavaScript имеют принципиальные различия в механике работы, которые критичны для понимания при написании надежного кода.

=== (Строгое равенство):

  1. Без приведения типов: Сравнивает значение и тип данных.
  2. Примеры:
    5 === 5;     // true (number === number)
    '5' === 5; // false (string !== number)
    null === undefined; // false (разные типы)
  3. Особые случаи:
    NaN === NaN;  // false (единственное значение, не равное себе)
    +0 === -0; // true

== (Нестрогое равенство):

  1. С приведением типов: Алгоритм выполняет неочевидные преобразования по правилам ECMAScript:

  2. Правила преобразований:

    • Если типы одинаковы → сравнивает как ===
    • null == undefined → true (специальное правило)
    • Число vs Строка → строка преобразуется в число
    • Булево значение → преобразуется в число (true → 1, false → 0)
    • Объект → вызывается valueOf() или toString()
  3. Примеры с неявными преобразованиями:

    '5' == 5;     // true (строка '5' → число 5)
    false == 0; // true (false → 0)
    '' == 0; // true (пустая строка → 0)
    [] == 0; // true (массив → '' → 0)
    [] == ''; // true (массив → '')
    [] == ![]; // true (правый массив → false → 0, левый → 0)

Где это приводит к ошибкам:

const userId = '123';
if (userId == 123) { // true! Возможно нежелательное поведение
grantAccess();
}

Рекомендации для профессиональной разработки:

  1. Всегда используйте === если явно не требуется преобразование типов.
  2. Опасные сценарии для ==:
    • Сравнение с false/true:
      if ([] == false) // true ([] → '' → 0, false → 0)
    • Сравнение объектов и примитивов:
      new String('foo') === 'foo' // false (объект vs примитив)
  3. Используйте линтеры (ESLint с правилом eqeqeq) для автоматической проверки.

Как запомнить сложные преобразования:

  • При сравнении объекта с примитивом объект преобразуется в примитив через valueOf()/toString()
  • Булевы значения всегда приводятся к числам в сравнениях
  • Массивы → преобразуются в строки через join(',')

Пример для продвинутых:

const a = {
valueOf: () => 42,
toString: () => 'Hello'
};

console.log(a == 42); // true (вызывается valueOf())
console.log(a == 'Hello'); // false (valueOf имеет приоритет)

Итоговое правило: В 99% случаев используйте ===. Исключения — только при явной необходимости проверить null/undefined:

if (value == null) {
// Поймает и null, и undefined (без сравнения с 0/false)
}

Вопрос 5. Какие существуют способы проверить наличие свойства у объекта в JavaScript?

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

Ответ собеседника: Правильный. Использовать метод hasOwnProperty.

Правильный ответ: В JavaScript есть несколько методов проверки свойств объекта, каждый с уникальными особенностями:

  1. hasOwnProperty() (Базовый метод) Проверяет собственные свойства объекта (не учитывает прототипы):
const user = { name: 'Alice' };

console.log(user.hasOwnProperty('name')); // true
console.log(user.hasOwnProperty('age')); // false

Ограничения:

  • Не проверяет унаследованные свойства:
    function Person() {}
    Person.prototype.age = 30;

    const bob = new Person();
    console.log(bob.hasOwnProperty('age')); // false (свойство в прототипе)
  • Может быть переопределен:
    const obj = { hasOwnProperty: () => true };
    console.log(obj.hasOwnProperty('any')); // true (ложный результат)
  1. Оператор in (Проверка в цепочке прототипов) Проверяет свойства во всей цепочке прототипов:
console.log('name' in user);   // true
console.log('age' in bob); // true (из прототипа)
console.log('toString' in {}); // true (унаследовано от Object.prototype)
  1. Object.hasOwn() (ES2022, современная замена) Безопасная альтернатива hasOwnProperty:
console.log(Object.hasOwn(user, 'name'));  // true
console.log(Object.hasOwn(bob, 'age')); // false

Преимущества:

  • Работает с объектами без прототипа (Object.create(null)):
    const protoLess = Object.create(null);
    protoLess.key = 'value';

    console.log(Object.hasOwn(protoLess, 'key')); // true
  • Защищен от переопределения:
    const trickyObj = { hasOwnProperty: 'hacked' };
    console.log(Object.hasOwn(trickyObj, 'hasOwnProperty')); // true (корректная проверка)
  1. Прямое сравнение с undefined (Осторожно!) Ненадежный метод для некоторых случаев:
console.log(user.name !== undefined); // true
console.log('toString' in {}); // true
console.log({}.toString !== undefined); // true (но свойство унаследовано!)
  1. Object.keys()/Object.getOwnPropertyNames() Для проверки через список свойств:
const keys = Object.keys(user);
console.log(keys.includes('name')); // true

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

МетодСобственные свойстваЦепочка прототиповРаботает с Object.create(null)Защита от переопределения
hasOwnProperty
in
Object.hasOwn()
property !== undefined⚠️ (условно)

Рекомендации:

  1. Для собственных свойствObject.hasOwn() (ES2022+) или Object.prototype.hasOwnProperty.call(obj, prop)
  2. Для проверки включая прототипы → оператор in
  3. Избегайте obj.hasOwnProperty() напрямую из-за риска переопределения
  4. Для безопасной работы с legacy-кодом:
    const safeHasOwn = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);

Пример продвинутого использования:

function hasProperty(obj, prop, checkPrototype = false) {
if (checkPrototype) {
return prop in obj;
}
return Object.hasOwn ? Object.hasOwn(obj, prop) : Object.prototype.hasOwnProperty.call(obj, prop);
}

console.log(hasProperty(user, 'name')); // true (own)
console.log(hasProperty(bob, 'age', true)); // true (prototype)

Вопрос 6. Как правильно проверить, является ли значение массивом в JavaScript?

Таймкод: 00:11:51

Ответ собеседника: Правильный. Использовать метод Array.isArray().

Правильный ответ: В JavaScript существует несколько подходов к определению типа массива, но не все они безопасны и надежны. Рассмотрим детально:

  1. Array.isArray(value) (Рекомендуемый способ) Стандартный метод, появившийся в ES5:
console.log(Array.isArray([]));          // true
console.log(Array.isArray(new Array())); // true
console.log(Array.isArray({})); // false
console.log(Array.isArray('array')); // false

Преимущества:

  • Корректно работает с кросс-фреймовыми массивами (iframe):
    const iframe = document.createElement('iframe');
    document.body.appendChild(iframe);
    const frameArray = iframe.contentWindow.Array;
    const arr = new frameArray();

    console.log(arr instanceof Array); // false (разные конструкторы)
    console.log(Array.isArray(arr)); // true
  • Поддерживается во всех современных браузерах и Node.js (включая старые версии с полифилом)
  1. Оператор instanceof (Ненадежный в некоторых случаях) Проверяет цепочку прототипов:
console.log([] instanceof Array); // true
console.log({} instanceof Array); // false

Проблемы:

  • Не работает с массивами из других контекстов (например, iframe)
  • Ложно-положительные сценарии при модификации прототипа:
    const obj = {};
    Object.setPrototypeOf(obj, Array.prototype);
    console.log(obj instanceof Array); // true (хотя это не массив)
  1. Проверка через Object.prototype.toString() (До ES5) Универсальный, но более многословный метод:
function isArray(value) {
return Object.prototype.toString.call(value) === '[object Array]';
}

console.log(isArray([])); // true
console.log(isArray({})); // false

Особенности:

  • Работает во всех окружениях, включая старые браузеры
  • Корректно определяет типы для любых объектов:
    console.log(Object.prototype.toString.call(null));      // '[object Null]'
    console.log(Object.prototype.toString.call(undefined)); // '[object Undefined]'
  1. Ненадежные методы (избегать!):
  • Проверка свойства .length:
    function fakeArray() {}
    fakeArray.prototype = { length: 0 };
    console.log(new fakeArray().length === 0); // true, но это не массив
  • Проверка метода .push:
    const arrayLike = { push: () => {}, length: 0 };
    console.log(!!arrayLike.push); // true (ложное срабатывание)
  1. Проверка для TypedArray: Для Uint8Array, Float32Array и других:
const buffer = new ArrayBuffer(16);
const uintArray = new Uint8Array(buffer);

console.log(Array.isArray(uintArray)); // false
console.log(uintArray instanceof Uint8Array); // true

Полифил для старых сред (pre-ES5):

if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}

Рекомендации:

  • Всегда используйте Array.isArray() в современном коде
  • Для объектов, похожих на массив (arguments, NodeList), преобразуйте их:
    function example() {
    const argsArray = Array.from(arguments); // или [...arguments]
    console.log(Array.isArray(argsArray)); // true
    }
  • Избегайте typeof для проверки массивов (возвращает 'object')

Пример продвинутого использования:

function deepContainsArray(obj) {
if (Array.isArray(obj)) return true;

if (typeof obj === 'object' && obj !== null) {
return Object.values(obj).some(deepContainsArray);
}

return false;
}

console.log(deepContainsArray({ a: [1, 2] })); // true
console.log(deepContainsArray({ b: { c: 3 } })); // false

Вопрос 7. Что представляют собой интерфейсы в TypeScript и как они используются?

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

Ответ собеседника: Неправильный. Не смог дать точного определения, предположил использование для типизации объектов и ответов бэкенда без конкретики.

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

Ключевые возможности интерфейсов:

  1. Описание формы объекта:

    interface User {
    id: number;
    name: string;
    email?: string; // Опциональное свойство
    readonly createdAt: Date; // Неизменяемое после создания
    }

    const alice: User = {
    id: 1,
    name: "Alice",
    createdAt: new Date()
    };
  2. Определение методов:

    interface Calculator {
    add(x: number, y: number): number;
    subtract: (x: number, y: number) => number; // Альтернативный синтаксис
    }

    const calc: Calculator = {
    add: (a, b) => a + b,
    subtract(a, b) { return a - b; }
    };
  3. Реализация в классах (через implements):

    interface Animal {
    name: string;
    makeSound(): void;
    }

    class Dog implements Animal {
    constructor(public name: string) {}

    makeSound() {
    console.log("Woof!");
    }
    }
  4. Наследование интерфейсов:

    interface Vehicle {
    speed: number;
    }

    interface Car extends Vehicle {
    wheels: number;
    }

    const tesla: Car = {
    speed: 250,
    wheels: 4
    };
  5. Интерфейсы для функций:

    interface StringTransformer {
    (input: string): string;
    }

    const toUpper: StringTransformer = (str) => str.toUpperCase();
  6. Индексируемые типы:

    interface StringArray {
    [index: number]: string; // Массив строк
    }

    const arr: StringArray = ["a", "b"];

Расширенные возможности:

  1. Объединение интерфейсов (Declaration Merging):

    interface Box {
    width: number;
    }

    interface Box {
    height: number;
    }

    // Автоматически объединяются:
    const box: Box = { width: 10, height: 20 };
  2. Генерики в интерфейсах:

    interface ApiResponse<T> {
    data: T;
    status: number;
    }

    const userResponse: ApiResponse<User> = {
    data: { id: 1, name: "Alice" },
    status: 200
    };

Интерфейсы vs Псевдонимы типов (type aliases):

ОсобенностьИнтерфейсыType Aliases
Расширениеextends& (intersection)
ОбъединениеDeclaration MergingНе поддерживается
ПримитивыТолько объектные типыЛюбые типы (включая union)
Реализация в классеimplementsНе поддерживается

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

  1. Валидация API-ответов:

    interface ApiError {
    code: number;
    message: string;
    details?: Record<string, string>;
    }

    async function fetchData() {
    const response = await fetch('/api/data');
    const data: ApiResponse<User> | ApiError = await response.json();
    // Автодополнение полей на основе типа
    }
  2. Паттерн Стратегия:

    interface PaymentStrategy {
    pay(amount: number): void;
    }

    class CreditCardStrategy implements PaymentStrategy { ... }
    class PayPalStrategy implements PaymentStrategy { ... }

    function processPayment(strategy: PaymentStrategy, amount: number) {
    strategy.pay(amount);
    }
  3. Работа с DOM:

    interface CustomElement extends HTMLElement {
    customMethod(): void;
    }

    const el = document.getElementById('my-el') as CustomElement;
    el.customMethod(); // Безопасный вызов

Совет для интервью: Всегда упоминайте:

  • Интерфейсы как инструмент для декларативного программирования
  • Разницу между компиляционной (TypeScript) и рантайм (JavaScript) проверкой типов
  • Возможность расширения сторонних типов через Declaration Merging

Вопрос 8. Какие существуют способы добавления типизации в React-компоненты при использовании JavaScript (без TypeScript)?

Таймкод: 00:12:42

Ответ собеседника: Неправильный. Предположил использование JSDoc, но не смог точно ответить. Позже упомянул PropTypes, но без уверенности.

Правильный ответ: В экосистеме React существует несколько подходов к добавлению типизации в JavaScript-компоненты:

  1. PropTypes (Базовый подход) Библиотека prop-types для проверки типов props в рантайме:
import PropTypes from 'prop-types';

function Button({ text, onClick }) {
return <button onClick={onClick}>{text}</button>;
}

Button.propTypes = {
text: PropTypes.string.isRequired,
onClick: PropTypes.func,
size: PropTypes.oneOf(['small', 'medium', 'large']),
style: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
})
};

Button.defaultProps = {
size: 'medium'
};

Особенности:

  • Проверка происходит только в режиме разработки
  • Включает базовые типы: string, number, bool, func, object, array
  • Специальные валидаторы:
    • PropTypes.instanceOf(Class)
    • PropTypes.oneOf(['val1', 'val2'])
    • PropTypes.oneOfType([PropTypes.string, PropTypes.number])
  1. JSDoc с TypeScript-проверкой (Статический анализ) Использование комментариев JSDoc для типизации в обычном JavaScript:
/**
* @typedef {Object} ButtonProps
* @property {string} text
* @property {() => void} [onClick]
* @property {'small' | 'medium' | 'large'} [size]
*/

/**
* @param {ButtonProps} props
*/
function Button({ text, onClick, size = 'medium' }) {
return <button onClick={onClick}>{text}</button>;
}

Настройка:

  1. Установить TypeScript как devDependency: npm install --save-dev typescript
  2. Создать tsconfig.json с флагом checkJs:
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"noEmit": true
}
}

Преимущества:

  • Статическая проверка типов без перехода на TypeScript
  • Поддержка автодополнения в IDE (VSCode, WebStorm)
  • Возможность импортировать типы из .d.ts файлов
  1. Flow (Альтернативная система типов) Статический анализатор типов от Facebook:
// @flow
import * as React from 'react';

type Props = {
text: string,
onClick?: () => void,
size?: 'small' | 'medium' | 'large'
};

function Button({ text, onClick, size = 'medium' }: Props) {
return <button onClick={onClick}>{text}</button>;
}

Настройка:

  1. Установить Flow: npm install --save-dev flow-bin

  2. Добавить скрипт в package.json: "flow": "flow"

  3. Создать .flowconfig файл

  4. TypeScript в JS файлах через JSDoc (Продвинутый способ) Полноценная типизация с TypeScript-синтаксисом в .js файлах:

// @ts-check

/**
* @template T
* @typedef {Object} ApiResponse
* @property {T} data
* @property {number} status
*/

/** @type {ApiResponse<{ id: number, name: string }>} */
const response = {
data: { id: 1, name: 'Alice' },
status: 200
};

function Button(/** @type {{ text: string }} */ props) {
return <button>{props.text}</button>;
}

Преимущества:

  • Использование TypeScript-дженериков и утилит типов
  • Поддержка import type через JSDoc:
    /** @type {import('./types').User} */
    const user = { id: 1 };

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

МетодТип проверкиСложностьВозможностиИнтеграция с TS
PropTypesРантаймНизкаяБазовые типыЧастичная
JSDocСтатическаяСредняяУмеренныеПолная
FlowСтатическаяВысокаяПродвинутыеНет
TypeScript JSDocСтатическаяВысокаяПродвинутыеПолная

Рекомендации:

  • Для новых проектов — используйте TypeScript
  • Для существующих JS-проектов:
    • Старт с PropTypes для базовой защиты
    • Постепенно добавляйте JSDoc с @ts-check для сложных компонентов
  • Избегайте смешивания Flow и TypeScript в одном проекте

Пример продвинутой типизации с JSDoc:

// Компонент с дженериком
/**
* @template T
* @typedef {Object} ListProps
* @property {T[]} items
* @property {(item: T) => React.ReactNode} renderItem
*/

/**
* @template T
* @param {ListProps<T>} props
*/
function GenericList({ items, renderItem }) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{renderItem(item)}</li>
))}
</ul>
);
}

// Использование
<GenericList
items={users}
renderItem={user => <div>{user.name}</div>}
/>

Вопрос 9. Что такое пропсы (props) в React и как они используются?

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

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

Правильный ответ: Пропсы (props) — это основной механизм передачи данных между React-компонентами, работающий по принципу однонаправленного потока данных (parent → child). Рассмотрим ключевые аспекты:

  1. Основные характеристики:
  • Иммутабельность: Пропсы доступны только для чтения внутри компонента-получателя
  • Типизация: Рекомендуется использовать PropTypes или TypeScript для валидации
  • Динамичность: Могут содержать любые данные — примитивы, объекты, функции, JSX-элементы
  1. Передача пропсов:
// Родительский компонент
<UserProfile
name="Alice"
age={28}
isAdmin
onUpdate={handleUpdate}
avatar={<Avatar src="alice.jpg" />}
/>
  1. Получение в компонентах: Функциональные компоненты (через параметр):
function UserProfile({ name, age, onUpdate, children }) {
return (
<div>
<h2>{name}</h2>
<button onClick={onUpdate}>Update</button>
{children}
</div>
);
}

Классовые компоненты (через this.props):

class UserProfile extends React.Component {
render() {
const { name, age } = this.props;
return <div>{name} ({age})</div>;
}
}
  1. Особые виды пропсов:
  • children: Специальный пропс для вложенного контента
    <Modal>
    <h1>Title</h1> {/* Доступно как props.children */}
    </Modal>
  • Spread-оператор: Передача объекта как набора пропсов
    const props = { title: 'Hello', onClick: handleClick };
    <Button {...props} />
  1. Продвинутые паттерны: Render Props:
<DataFetcher
render={(data) => <List items={data} />}
/>

function DataFetcher({ render }) {
const [data, setData] = useState(null);
// Загрузка данных...
return render(data);
}

Пропсы-функции (для взаимодействия родитель↔ребёнок):

function Parent() {
const handleChildEvent = (data) => {
console.log('Данные от ребенка:', data);
};

return <Child onEvent={handleChildEvent} />;
}

function Child({ onEvent }) {
return (
<button onClick={() => onEvent({ time: Date.now() })}>
Отправить данные
</button>
);
}
  1. Оптимизация производительности:
  • Memoization: Предотвращение лишних рендеров
    const MemoizedComponent = React.memo(Component, arePropsEqual?);
  • Коллбэки: Использование useCallback для стабильной идентичности функций
    const handleAction = useCallback(() => {
    // Логика
    }, [dependencies]);
  1. Отличия от состояния (state): | Характеристика | Пропсы (props) | Состояние (state) | |----------------------|-------------------------|--------------------------| | Источник данных | Внешний компонент | Внутренний компонент | | Изменяемость | Только для чтения | Изменяется через setState| | Направление | Parent → Child | Локальное управление | | Обновление | Вызывает ререндер | Вызывает ререндер |

  2. Валидация пропсов: Для TypeScript:

interface UserCardProps {
name: string;
age: number;
isAdmin?: boolean;
}

Для JavaScript (PropTypes):

import PropTypes from 'prop-types';

UserCard.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
isAdmin: PropTypes.bool
};
  1. Антипаттерны:
  • Мутация пропсов: Никогда не изменяйте пропсы напрямую
    // Плохо!
    props.user.name = 'New Name';
  • Избыточная передача: Избегайте "пропс-дриллинга" (используйте Context API)
  • Инлайн-функции: Создают новые ссылки при каждом рендере
    // Потенциальная проблема производительности
    <Button onClick={() => handleClick(id)} />

Итоговые принципы:

  1. Пропсы — это входные параметры компонента, аналогичные аргументам функции
  2. Всегда рассматривайте компоненты как чистые функции относительно их пропсов
  3. Для сложных сценариев используйте композицию компонентов вместо передачи избыточных пропсов

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

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

Ответ собеседника: Правильный. Назвал контекст и стейт-менеджеры (Redux/Zustand).

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

  1. Пропсы (Props) — базовый уровень
  • Сценарий: Родитель ↔ Прямой потомок
  • Пример:
    function Parent() {
    const [data, setData] = useState('Hello');
    return <Child message={data} />;
    }

    function Child({ message }) {
    return <div>{message}</div>;
    }
  • Ограничения: Только для близкородственных компонентов (1-2 уровня вложенности)
  1. Контекст (Context API) — средний уровень
  • Сценарий: Глобальные данные (тема, локализация, аутентификация)
  • Пример:
    const ThemeContext = createContext('light');

    function App() {
    return (
    <ThemeContext.Provider value="dark">
    <Toolbar />
    </ThemeContext.Provider>
    );
    }

    function Toolbar() {
    const theme = useContext(ThemeContext);
    return <div style={{ background: theme === 'dark' ? '#333' : '#FFF' }} />;
    }
  • Особенности:
    • Избегайте частых обновлений (не для высокочастотных данных)
    • Комбинируйте с useMemo для оптимизации
  1. Стейт-менеджеры — продвинутый уровень
  • Библиотеки: Redux, MobX, Zustand, Recoil
  • Сценарий: Сложное кросс-компонентное состояние (кеш API, мультишаговые формы)
  • Пример с Redux Toolkit:
    // store.js
    const store = configureStore({
    reducer: {
    counter: counterReducer
    }
    });

    // Component.js
    function Counter() {
    const count = useSelector(state => state.counter);
    const dispatch = useDispatch();
    return (
    <button onClick={() => dispatch(increment())}>{count}</button>
    );
    }
  1. Событийное взаимодействие — альтернативный подход
  • Сценарий: Независимые компоненты (микросервисная архитектура)
  • Пример с кастомными событиями:
    // Компонент-источник
    function EventEmitter() {
    const emitEvent = () => {
    window.dispatchEvent(new CustomEvent('customEvent', {
    detail: { data: 'payload' }
    }));
    };
    return <button onClick={emitEvent}>Trigger</button>;
    }

    // Компонент-получатель
    function Listener() {
    const [data, setData] = useState(null);

    useEffect(() => {
    const handler = (e) => setData(e.detail);
    window.addEventListener('customEvent', handler);
    return () => window.removeEventListener('customEvent', handler);
    }, []);

    return <div>{data}</div>;
    }
  1. Ссылки (Refs) и императивные методы — для крайних случаев
  • Сценарий: Управление дочерними компонентами (анимации, фокус)
  • Пример:
    function Parent() {
    const childRef = useRef();

    const handleClick = () => {
    childRef.current.scrollToBottom();
    };

    return (
    <>
    <Child ref={childRef} />
    <button onClick={handleClick}>Scroll</button>
    </>
    );
    }

    const Child = forwardRef((props, ref) => {
    const listRef = useRef();

    useImperativeHandle(ref, () => ({
    scrollToBottom: () => {
    listRef.current.scrollTop = listRef.current.scrollHeight;
    }
    }));

    return <div ref={listRef}>...</div>;
    });
  1. URL и роутинг — для сессионных данных
  • Библиотеки: React Router, Next.js Router
  • Сценарий: Фильтры, параметры поиска, идентификаторы сущностей
  • Пример:
    function ProductPage() {
    const { id } = useParams();
    const [product, setProduct] = useState(null);

    useEffect(() => {
    fetchProduct(id).then(setProduct);
    }, [id]);

    return <div>{product?.name}</div>;
    }
  1. Локальное хранилище (Storage API) — сохранение между сессиями
  • Сценарий: Настройки пользователя, кеш данных
  • Пример:
    function usePersistentState(key, defaultValue) {
    const [state, setState] = useState(() => {
    const saved = localStorage.getItem(key);
    return saved ? JSON.parse(saved) : defaultValue;
    });

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

    return [state, setState];
    }

Критерии выбора стратегии:

  1. Объем данных:

    • Малый (пропсы)
    • Средний (контекст)
    • Большой (стейт-менеджер)
  2. Частота обновлений:

    • Редкие (контекст)
    • Частые (специализированные решения: Zustand, Valtio)
  3. Сложность отношений между компонентами:

    • Прямые связи (пропсы)
    • Сложные зависимости (глобальное состояние)

Архитектурная рекомендация: Начинайте с простых решений (пропсы → контекст → стейт-менеджер) и усложняйте только при необходимости.

Вопрос 11. В чём ключевые различия между Context API и стейт-менеджерами (Redux/Zustand) в React?

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

Ответ собеседника: Неполный. Упомянул, что стейт-менеджер - единый источник данных, но не раскрыл различия в масштабируемости и структуре.

Правильный ответ: Context API и стейт-менеджеры решают задачу управления состоянием, но имеют принципиальные архитектурные отличия:

  1. Архитектурные различия | Аспект | Context API | Стейт-менеджеры (Redux/Zustand) | |----------------------|--------------------------------------|---------------------------------------| | Паттерн | Провайдер значений | Единый Store + Flux/Event-ориентированность | | Обновление | Пуш-модель (все подписчики) | Pull-модель (только заинтересованные) | | Структура | Древовидная иерархия | Глобальный плоский Store | | Связность | Жёсткая привязка к React-дереву | Независимый слой (может использоваться без React) |

  2. Производительность Context API:

  • Любое изменение в провайдере вызывает ререндер всех потребителей
  • Пример проблемы:
    <UserContext.Provider value={{ user, setUser }}>
    {/* Компонент A использует только user.name */}
    <ComponentA />
    {/* Компонент B использует setUser */}
    <ComponentB />
    </UserContext.Provider>
    Изменение user вызовет ререндер ComponentB, хотя он использует только сеттер

Стейт-менеджеры:

  • Подписка на конкретные части состояния (селекторы)
  • Пример с Redux Toolkit:
    const count = useSelector(state => state.counter); // Рендер только при изменении counter
  1. Масштабируемость Context API:
  • Подходит для низкочастотных данных (тема, локализация)
  • Сложность при множестве контекстов ("провайдерный ад"):
    <AuthProvider>
    <ThemeProvider>
    <PreferencesProvider>
    <AnalyticsProvider>
    <App />
    </AnalyticsProvider>
    </PreferencesProvider>
    </ThemeProvider>
    </AuthProvider>

Стейт-менеджеры:

  • Поддержка сложных сценариев:
    • Middleware (логирование, асинхронные действия)
    • Девтулзы (Redux DevTools)
    • Код-сплиттинг редьюсеров
  • Пример асинхронного экшена в Redux:
    const fetchUser = createAsyncThunk('user/fetch', async (userId) => {
    const response = await api.getUser(userId);
    return response.data;
    });
  1. Инструменты разработчика | Инструмент | Context API | Redux | Zustand | |----------------------|-------------|----------------|--------------| | Визуализация данных | Нет | Redux DevTools | Zustand DevTools | | Time Travel Debug | Нет | Да | Частично | | Трассировка экшенов | Нет | Да | Да |

  2. Примеры использования Когда выбирать Context API:

  • Статичные данные (тема UI, локаль)
  • Локальное состояние формы (без глубокой вложенности)
  • Провайдеры сервисов (API-клиент, аутентификация)

Когда выбирать стейт-менеджер:

  • Кеширование API-ответов (RTK Query)
  • Сложная бизнес-логика с сайд-эффектами
  • Приложения с > 20 взаимосвязанными компонентами
  • Необходимость предиктивного управления состоянием
  1. Гибридные подходы Оптимизация Context API:
const UserContext = createContext();

function UserProvider({ children }) {
const [state, setState] = useState({});

// Мемоизация значения контекста
const value = useMemo(() => ({
user: state.user,
login: (credentials) => setState(/*...*/)
}), [state.user]); // Перерасчет только при изменении user

return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}

Интеграция Zustand с Context:

const useStore = create((set) => ({
user: null,
setUser: (user) => set({ user })
}));

// Для доступа в классовых компонентах
const UserContext = createContext();

function UserProvider({ children }) {
const store = useStore();
return <UserContext.Provider value={store}>{children}</UserContext.Provider>;
}

Критические отличия на практике:

  1. Сложность тестирования:

    • Redux-экшены/редьюсеры тестируются без рендеринга
    • Контекст требует оборачивания в провайдеры при тестах
  2. Порог вхождения:

    • Context: 10 строк кода для базового использования
    • Redux: ~5 файлов (store, slices, middleware)
  3. Бандл-сайз:

    • Context: 0Kb (встроен в React)
    • Redux: ~2Kb (RTK) + зависимости
    • Zustand: ~1Kb

Итоговое правило: Используйте Context для статичных данных и dependency injection, стейт-менеджеры — для динамичного состояния с частыми обновлениями. Для современных приложений рассмотрите Zustand или Jotai как более легкие альтернативы Redux.

Вопрос 12. Какой у вас опыт работы с библиотеками управления состоянием (стейт-менеджерами) в React и какие из них вы предпочитаете?

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

Ответ собеседника: Правильный. Работал с Redux и Zustand, предпочитает Zustand из-за меньшего бойлерплейта.

Правильный ответ: Опыт работы с различными стейт-менеджерами позволяет выбрать оптимальное решение для конкретных задач. Рассмотрим ключевые аспекты популярных библиотек:

  1. Redux (RTK) — промышленный стандарт Архитектура:
  • Паттерн Flux: Store → Action → Reducer → View
  • Единый источник истины (single source of truth)
  • Неизменяемое состояние (immutability)

Преимущества:

  • Предсказуемость изменений
  • Мощные DevTools с историей изменений
  • Экосистема middleware (thunk, saga, observable)

Пример с Redux Toolkit:

// store.ts
const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: state => state + 1,
decrement: state => state - 1
}
});

export const store = configureStore({
reducer: {
counter: counterSlice.reducer
}
});

// Component.tsx
const Counter = () => {
const count = useSelector((state: RootState) => state.counter);
const dispatch = useDispatch();

return (
<div>
<button onClick={() => dispatch(counterSlice.actions.increment())}>
{count}
</button>
</div>
);
};

Проблемы:

  • Избыточный бойлерплейт (даже с RTK)
  • Сложность для новичков
  • Оверкилл для простых приложений

  1. Zustand — современная альтернатива Архитектура:
  • Атомарное управление состоянием
  • Прямая мутация через set
  • Подписка на части состояния

Преимущества:

  • Минимальный бойлерплейт
  • Встроенная поддержка асинхронности
  • Интеграция с React Concurrent Mode

Пример:

// store.ts
import create from 'zustand';

interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
asyncIncrement: () => Promise<void>;
}

const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
asyncIncrement: async () => {
const response = await fetch('/increment');
set({ count: await response.json() });
}
}));

// Component.tsx
const Counter = () => {
const count = useCounterStore(state => state.count);
const increment = useCounterStore(state => state.increment);

return (
<button onClick={increment}>{count}</button>
);
};

Оптимизации:

  • Селекторное сравнение:
    const user = useStore(state => state.user, shallow);
  • Мемоизация сложных селекторов:
    const expensiveValue = useStore(
    useCallback(state => computeExpensive(state.data), [])
    );

  1. Сравнение производительности | Операция | Redux | Zustand | Контекст | |------------------------|-------|---------|----------| | Инициализация | 150ms | 25ms | 5ms | | Обновление состояния | 5ms | 1.2ms | 0.3ms* | | Подписка на изменение | 3ms | 0.8ms | N/A |

*При условии небольших данных и грамотной мемоизации


  1. Критерии выбора Выбирайте Redux когда:
  • Команда уже имеет экспертизу
  • Требуется time-travel debugging
  • Сложная бизнес-логика с сайд-эффектами
  • Большая кодовая база с множеством разработчиков

Выбирайте Zustand когда:

  • Нужна минимальная настройка
  • Приложение средней сложности
  • Требуется высокая производительность
  • Используются современные фичи React (Suspense, Concurrent Mode)

Выбирайте Context API когда:

  • Состояние используется в 1-2 компонентах
  • Данные обновляются редко (темы, локализация)
  • Не хочется добавлять внешние зависимости

  1. Продвинутые паттерны Redux + RTK Query:
// API слой
const api = createApi({
reducerPath: 'api',
endpoints: (builder) => ({
getUsers: builder.query<User[], void>({
query: () => '/users',
}),
}),
});

// Компонент
const { data, isLoading } = useGetUsersQuery();

Zustand + Immer:

const useStore = create((set) => ({
user: null,
updateUser: produce((draft) => {
draft.user.name = 'New Name';
set(draft);
})
}));

Гибридный подход:

  • Zustand для UI состояния (модалки, формы)
  • Redux для бизнес-логики и кеширования API

Рекомендации:

  1. Для новых проектов начинайте с Zustand — меньше накладных расходов
  2. При миграции с Redux используйте адаптеры:
    const legacyStore = useLegacyReduxStore();
    const newStore = useZustandStore();

    // Синхронизация состояний
    useEffect(() => {
    newStore.setData(legacyStore.data);
    }, [legacyStore.data]);
  3. Всегда используйте TypeScript для стейт-менеджеров
  4. Для микрофронтендов рассмотрите атомарные решения (Jotai, Recoil)

Вопрос 13. Как организовать эффективное взаимодействие с бэкендом в React-приложении?

Таймкод: 00:16:17

Ответ собеседника: Неполный. Упомянул createAsyncThunk из Redux Toolkit, но не описал процесс полностью.

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

  1. Архитектурные подходы #RTK Query (Redux Toolkit) Специализированный слой для API:
// apiSlice.ts
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getUsers: builder.query<User[], void>({
query: () => '/users',
providesTags: ['Users'],
}),
createUser: builder.mutation<User, Partial<User>>({
query: (body) => ({
url: '/users',
method: 'POST',
body,
}),
invalidatesTags: ['Users'],
}),
}),
});

// Компонент
const { data, isLoading, error } = useGetUsersQuery();
const [createUser] = useCreateUserMutation();

Преимущества:

  • Автоматическое кэширование данных
  • Дедупликация запросов
  • Авторегенерация хуков
  • Интеграция с DevTools

#React Query Универсальное решение для управления асинхронным состоянием:

const { data, status } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 5 * 60 * 1000, // 5 минут
});

const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});

#Кастомные хуки Для полного контроля:

function useApi(endpoint) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);

const fetchData = useCallback(async (params) => {
try {
setLoading(true);
const response = await axios.get(endpoint, { params });
setData(response.data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}, [endpoint]);

return { data, error, loading, fetchData };
}
  1. Оптимизации производительности
  • Кэширование:
    // React Query
    cacheTime: 10 * 60 * 1000 // 10 минут
  • Дебаунсинг запросов:
    const searchTerm = useDebounce(query, 300); // Задержка 300ms
  • Префетчинг данных:
    // Предзагрузка при наведении
    const prefetchUser = usePrefetch('getUser');
    <div onMouseEnter={() => prefetchUser(id)}>
  1. Обработка ошибок Глобальный интерсептор (axios):
axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
store.dispatch(logout());
}
return Promise.reject(error);
}
);

Структура ответа:

interface ApiResponse<T> {
data: T;
error?: {
code: string;
message: string;
};
meta?: {
timestamp: string;
version: string;
};
}
  1. Аутентификация и авторизация JWT-токены:
// Автоматическая подстановка токена
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});

Обновление токенов:

const api = axios.create();

api.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const newToken = await refreshToken();
localStorage.setItem('token', newToken);
return api(originalRequest);
}
return Promise.reject(error);
}
);
  1. Тестирование Мокирование API (MSW):
// server.ts
import { rest } from 'msw';

export const handlers = [
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.json([{ id: 1, name: 'Alice' }]),
ctx.delay(150)
);
}),
];

// test.ts
import { setupServer } from 'msw/node';
const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
  1. Оптимизация запросов
  • Пакетные запросы:
    // GraphQL
    query {
    user(id: 1) { name }
    posts(userId: 1) { title }
    }
  • Стратегии загрузки:
    • Lazy-loading: Загрузка по требованию
    • Waterfall: Последовательная загрузка зависимых данных
    • Parallel: Одновременные запросы через Promise.all
  1. Альтернативные библиотеки | Библиотека | Преимущества | Недостатки | |------------------|---------------------------------------|--------------------------| | SWR | Простота, revalidation | Меньше возможностей | | Apollo Client| Интеграция с GraphQL | Оверкилл для REST | | Axios | Перехватчики, отмена запросов | Дополнительный бандл |

Рекомендации:

  1. Для проектов с Redux → RTK Query
  2. Для сложных SPA → React Query
  3. Для микрофронтендов → SWR
  4. Всегда используйте TypeScript для типизации ответов
  5. Внедрите скелетоны загрузки для UX
  6. Реализуйте пагинацию и виртуализацию списков для больших данных

Продвинутый пример с React Query:

const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId, // Автозапуск при наличии userId
});

const { data: posts } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchPosts(userId),
// Зависимый запрос
enabled: !!user?.hasPosts,
});

Вопрос 14. Как корректно использовать предыдущее состояние при обновлении состояния в React?

Таймкод: 00:17:37

Ответ собеседника: Правильный. Опознал callback-форму setState для работы с предыдущим состоянием.

Правильный ответ: Работа с предыдущим состоянием — критически важный аспект реактивного программирования. Рассмотрим профессиональные подходы:

  1. Функциональные обновления (основной метод) Используйте функцию-аргумент в setState для гарантии актуальности состояния:
// Функциональный компонент
const [count, setCount] = useState(0);

// Корректное обновление
const increment = () => {
setCount(prevCount => prevCount + 1);
};

// Классовый компонент
this.setState(prevState => ({
counter: prevState.counter + 1
}));
  1. Когда это необходимо
  • Асинхронные операции:
    const [data, setData] = useState([]);

    const fetchMore = async () => {
    const newData = await api.fetch();
    setData(prev => [...prev, ...newData]); // Гарантированно последняя версия
    };
  • Множественные обновления:
    const doubleIncrement = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1); // +2 в итоге
    };
  • Работа с массивами/объектами:
    const addItem = (newItem) => {
    setItems(prev => [...prev, newItem]);
    };
  1. Опасности прямого использования состояния
// Антипаттерн! Может привести к ошибкам
const brokenIncrement = () => {
setCount(count + 1);
setCount(count + 1); // Все равно +1, а не +2
};
  1. Продвинутые сценарии useReducer для сложной логики:
const [state, dispatch] = useReducer(reducer, { count: 0 });

function reducer(state, action) {
switch (action.type) {
case 'ADD':
return { count: state.count + action.payload };
case 'MULTIPLY':
return { count: state.count * action.payload };
default:
return state;
}
}

// Диспатч экшена
dispatch({ type: 'ADD', payload: 5 });

Сравнение с предыдущим значением через useRef:

const [value, setValue] = useState('');
const prevValueRef = useRef();

useEffect(() => {
prevValueRef.current = value;
}, [value]);

console.log(`Previous: ${prevValueRef.current}, Current: ${value}`);

Оптимизация с useCallback:

const stableUpdater = useCallback(
() => setCount(prev => prev + 1),
[] // Нет зависимостей
);
  1. Правила работы с состоянием

  2. Иммутабельность: Всегда создавайте новые объекты/массивы

    // Плохо!
    setUser(prev => { prev.name = 'New'; return prev; });

    // Хорошо
    setUser(prev => ({ ...prev, name: 'New' }));
  3. Батчинг: React группирует синхронные обновления

    const handleClick = () => {
    setCount(a => a + 1); // 1-й рендер
    setFlag(f => !f); // 1-й рендер (одна группа)
    };
  4. Асинхронный батчинг:

    setTimeout(() => {
    setCount(a => a + 1); // 1-й рендер
    setFlag(f => !f); // 1-й рендер
    }, 1000);
  5. Диагностика проблем Типичные ошибки:

  • Stale closure: Использование устаревшего значения в замыкании
    const incrementAsync = () => {
    setTimeout(() => {
    setCount(count + 1); // Всегда использует начальное значение
    }, 1000);
    };

    // Решение
    setTimeout(() => {
    setCount(prev => prev + 1); // Корректно
    }, 1000);
  • Накопление состояний: В асинхронных операциях
    // Опасный код
    for (let i = 0; i < 5; i++) {
    setCount(count + 1);
    }

    // Решение
    setCount(prev => prev + 5);
  1. Производительность Мемоизация вычислений:
const expensiveValue = useMemo(() => {
return computeExpensiveValue(count);
}, [count]); // Пересчет только при изменении count

Ленивая инициализация:

const [state, setState] = useState(() => {
const initial = heavyComputation(props);
return initial;
});

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

Вопрос 15. В чем разница между useCallback и useMemo и когда их следует использовать?

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

Ответ собеседника: Неполный. Сказал, что useCallback мемоизирует функцию, но не объяснил разницу с useMemo.

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

  1. useCallback Назначение: Мемоизация функций между рендерами.

Синтаксис:

const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b], // Зависимости
);

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

  • Возвращает ту же самую функцию при неизменных зависимостях
  • Предотвращает создание новой функции при каждом рендере

Пример использования:

const Parent = () => {
const [count, setCount] = useState(0);

// Функция сохраняется между рендерами
const increment = useCallback(() => {
setCount(c => c + 1);
}, []); // Нет зависимостей

return <Child onClick={increment} />;
};

const Child = React.memo(({ onClick }) => {
// Рендерится только при изменении onClick
return <button onClick={onClick}>Click</button>;
});
  1. useMemo Назначение: Мемоизация значений между рендерами.

Синтаксис:

const memoizedValue = useMemo(
() => computeExpensiveValue(a, b),
[a, b], // Зависимости
);

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

  • Вычисляет значение при первом рендере и при изменении зависимостей
  • Возвращает кэшированное значение при неизменных зависимостях

Пример использования:

const Component = ({ items }) => {
const sortedItems = useMemo(
() => items.sort(complexComparator),
[items] // Пересортировка только при изменении items
);

return <List items={sortedItems} />;
};
  1. Ключевые различия | Критерий | useCallback | useMemo | |------------------|----------------------------------|--------------------------------| | Возвращает | Функцию | Любое значение (число, массив, объект) | | Оптимизация | Ссылочной идентичности | Дорогих вычислений | | Использование| Передача коллбэков дочерним компонентам | Кэширование результатов сложных вычислений | | Эквивалент | useMemo(() => fn, deps) | useCallback(fn, deps)() |

  2. Глубокий анализ Как работает ссылочная идентичность:

const func1 = () => {};
const func2 = () => {};
console.log(func1 === func2); // false

const memoFunc1 = useCallback(() => {}, []);
const memoFunc2 = useCallback(() => {}, []);
console.log(memoFunc1 === memoFunc2); // true (при одинаковых депсах)

Семантический пример эквивалентности:

// Эквивалентные записи
const callback = useCallback(() => doSomething(a, b), [a, b]);
const memoized = useMemo(() => () => doSomething(a, b), [a, b]);
  1. Правила применения Когда использовать useCallback:
  2. Передача коллбэков оптимизированным компонентам (React.memo)
  3. Зависимости эффектов (useEffect, useLayoutEffect)
  4. Подписки на события (WebSocket, DOM-события)

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

  1. Тяжелые вычисления (фильтрация, сортировка)

  2. Создание сложных объектов/массивов

  3. Оптимизация перерендеров дочерних компонентов

  4. Референциальная целостность в зависимостях эффектов

  5. Антипаттерны Избыточное использование:

// Плохо! Нет реальной оптимизации
const value = useMemo(() => 42, []);

// Лучше
const value = 42;

Некорректные зависимости:

const [state, setState] = useState({});

// Ошибка! Будет пересоздаваться при каждом рендере
const value = useMemo(() => state.value, [state]);

// Решение
const value = useMemo(() => state.value, [state.value]);
  1. Производительность Метрики оптимизации:
  • Память: Кэширование требует дополнительной памяти
  • Время: Проверка зависимостей добавляет накладные расходы

Рекомендации:

  • Профилируйте перед оптимизацией (React DevTools Profiler)
  • Избегайте преждевременной оптимизации
  • Используйте только при доказанных проблемах производительности
  1. Продвинутые сценарии Комбинирование с React.memo:
const ExpensiveComponent = React.memo(({ compute, data }) => {
const result = compute(data);
return <div>{result}</div>;
});

function Parent() {
const compute = useCallback(
(data) => expensiveOperation(data),
[/* deps */]
);

return <ExpensiveComponent compute={compute} data={dataset} />;
}

Кастомные хуки с мемоизацией:

function useSearch(query, items) {
const searchResults = useMemo(
() => items.filter(item => item.includes(query)),
[query, items]
);

const reset = useCallback(() => {
// Логика сброса
}, []);

return { searchResults, reset };
}

Итог:

  • useCallback → для функций
  • useMemo → для значений
  • Всегда проверяйте необходимость оптимизации через профилирование
  • Не злоупотребляйте мемоизацией без необходимости

Вопрос 16. Что такое Virtual DOM в React и как он работает?

Таймкод: 00:18:36

Ответ собеседника: Неполный. Описал как механизм для эффективного обновления DOM с двумя виртуальными деревьями, но не упомянул diffing algorithm.

Правильный ответ: Virtual DOM (VDOM) — это легковесная абстракция реального DOM, используемая в React для оптимизации процесса обновления интерфейса. Рассмотрим его работу поэтапно:

  1. Основные принципы
  • Представление в памяти: VDOM — JavaScript-объект, описывающий структуру UI
  • Сравнение вместо прямых манипуляций: React вычисляет минимальные изменения между состояниями
  • Батчинг обновлений: Группировка множественных изменений в одну операцию
// Пример VDOM-структуры
const vdomElement = {
type: 'div',
props: {
className: 'container',
children: [
{ type: 'h1', props: { children: 'Hello' } },
{ type: 'p', props: { children: 'World' } }
]
}
};
  1. Процесс работы (Reconciliation)

  2. Рендер:

    • При изменении состояния компонента создается новое VDOM-дерево
    function Component() {
    const [count, setCount] = useState(0);
    return <div>Count: {count}</div>;
    }
  3. Diffing Algorithm (Фаза сравнения):

    • Эвристика O(n): Алгоритм сравнивает деревья уровень за уровнем
    • Ключевые правила:
      • Разные типы элементов → полная перестройка поддерева
      // Старое: <div><Component /></div>
      // Новое: <span><Component /></span> → Component unmount/remount
      • Одинаковые DOM-элементы → обновление только измененных атрибутов
      // Старое: <div className="old" />
      // Новое: <div className="new" /> → Только обновление class
      • Списки → использование key для идентификации элементов
      {items.map(item => 
      <li key={item.id}>{item.text}</li>
      )}
  4. Фиксация изменений (Commit Phase):

    • Пакетное применение вычисленных изменений к реальному DOM
    • Вызов эффектов (useEffect, useLayoutEffect)
  5. Оптимизационные механизмы Пример diffing для списков:

// Старый VDOM
<ul>
<li key="a">A</li>
<li key="b">B</li>
</ul>

// Новый VDOM
<ul>
<li key="c">C</li>
<li key="a">A</li>
<li key="b">B</li>
</ul>

// React выполнит:
// 1. Вставка C (перед A)
// 2. Сохранение A и B (без изменений)
  1. Преимущества перед прямыми DOM-операциями | Аспект | Реальный DOM | Virtual DOM | |-----------------------|----------------------------|---------------------------| | Обновление | Медленное (пересчет layout)| Быстрое (в памяти) | | Операции | Поэлементные изменения | Пакетное обновление | | Оптимизация | Ручное управление | Автоматическая | | Порог вхождения | Низкий | Требует понимания React |

  2. Глубокое погружение в Diffing Фазы сравнения компонентов:

  3. Элементы разных типов:

    // Старое: <Button />
    // Новое: <Input /> → Размонтирование Button, монтирование Input
  4. Элементы одного типа:

    // Старое: <div className="old" title="hello" />
    // Новое: <div className="new" title="hello" />
    → Обновит только className
  5. Компоненты того же типа:

    • Сохраняет экземпляр (состояние остается)
    • Обновляет пропсы
    • Вызывает методы жизненного цикла (componentDidUpdate)
  6. Сравнение с альтернативными подходами Svelte (Compiler-based):

  • Генерирует оптимальный DOM-код на этапе сборки
  • Нет runtime-накладок VDOM
  • Сложнее масштабируется в больших приложениях

Solid.js (Fine-grained reactivity):

  • Отслеживает зависимости на уровне отдельных переменных
  • Обновляет только конкретные DOM-узлы
  1. Практические рекомендации
  2. Используйте стабильные key для списков:
    // Плохо: key={index}
    // Хорошо: key={item.id}
  3. Избегайте частых полных перерендеров:
    • Оптимизируйте через React.memo, useMemo, useCallback
  4. Профилируйте производительность:
    • React DevTools → Profiler
    • Chrome Performance Tab
  5. Разделяйте тяжелые компоненты:
    // До
    <HeavyComponent data={data} />

    // После
    <DataProvider>
    <OptimizedComponent />
    </DataProvider>

Продвинутая оптимизация:

function HeavyComponent({ list }) {
const visibleItems = useMemo(
() => list.filter(complexFilter),
[list]
);

return (
<React.Fragment>
{visibleItems.map(item => (
<MemoizedItem key={item.id} item={item} />
))}
</React.Fragment>
);
}

const MemoizedItem = React.memo(Item);

Итог: Virtual DOM — фундаментальная оптимизация React, которая:

  • Минимизирует дорогостоящие операции с реальным DOM
  • Автоматизирует процесс согласования состояний
  • Требует понимания для эффективного использования

Для экстремальной производительности рассматривайте компиляторные решения (Svelte) или fine-grained библиотеки (Solid.js), но для большинства приложений VDOM обеспечивает оптимальный баланс.

Вопрос 17. Как работает хук useState в React и какие особенности важно учитывать?

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

Ответ собеседника: Правильный. Корректно описал создание локального состояния и возврат массива [value, setter].

Правильный ответ: useState — фундаментальный хук React для управления локальным состоянием в функциональных компонентах. Рассмотрим его работу и профессиональные практики использования:

Основной механизм

const [state, setState] = useState(initialValue);
  • initialValue: Стартовое значение (может быть функцией для ленивой инициализации)
  • state: Текущее значение состояния
  • setState: Функция-обновлятель (асинхронная)

Пример:

const Counter = () => {
const [count, setCount] = useState(() => {
// Ленивая инициализация
return Number(localStorage.getItem('count')) || 0;
});

const increment = () => {
setCount(prev => prev + 1); // Функциональное обновление
};

return <button onClick={increment}>{count}</button>;
};

Ключевые особенности

  1. Асинхронность обновлений:

    const handleClick = () => {
    setCount(count + 1);
    console.log(count); // Старое значение! (замыкание)
    };
  2. Батчинг (группировка):

    const update = () => {
    setCount(1); // Одно обновление
    setFlag(true); // с группировкой
    };
  3. Иммутабельность:

    // Антипаттерн для объектов:
    const [user, setUser] = useState({ name: 'Alice' });
    user.name = 'Bob'; // Прямая мутация!
    setUser(user); // Не вызовет ререндер

    // Правильно:
    setUser(prev => ({ ...prev, name: 'Bob' }));

Продвинутые паттерны Управление сложными структурами:

const [form, setForm] = useState({
email: '',
password: '',
preferences: { theme: 'light', notifications: true }
});

// Обновление вложенных полей
const updateTheme = (theme) => {
setForm(prev => ({
...prev,
preferences: {
...prev.preferences,
theme
}
}));
};

Массивы:

const [todos, setTodos] = useState([]);

// Добавление
setTodos(prev => [...prev, newTodo]);

// Удаление
setTodos(prev => prev.filter(todo => todo.id !== id));

// Обновление
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, done: true } : todo
));

Оптимизации производительности

  1. Функциональные обновления:

    // Вместо:
    setCount(count + 1);

    // Лучше (зависит от предыдущего состояния):
    setCount(prev => prev + 1);
  2. Стабильность ссылок:

    // Избегайте создания новых объектов в useState
    const [config] = useState({ mode: 'test' }); // ❌ Новый объект при каждом рендере

    // Решение:
    const [config] = useState(() => ({ mode: 'test' })); // ✅ Одно создание

Ошибки и их решение Зависшие состояния (Stale Closures):

const [count, setCount] = useState(0);

useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // Всегда будет count + 1 = 1
}, 1000);
return () => clearInterval(id);
}, []); // Пустые зависимости

// Решение:
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1); // Актуальное значение
}, 1000);
return () => clearInterval(id);
}, []);

Бесконечные циклы:

const [data, setData] = useState(null);

useEffect(() => {
fetchData().then(setData); // Вызовет эффект снова при изменении data
}, [data]); // Зависимость от data → цикл

// Решение: Убрать зависимость или использовать условие

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

  1. Глобальное состояние → Context/Redux/Zustand
  2. Производные данныеuseMemo
  3. Синхронизация с внешними системамиuseEffect/useSyncExternalStore
  4. Сложная бизнес-логикаuseReducer

Типизация с TypeScript

interface User {
id: string;
name: string;
}

const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<string[]>([]); // Явно типизированный массив

Итоговые рекомендации:

  1. Для примитивов используйте прямое обновление
  2. Для объектов/массивов — иммутабельные обновления
  3. При цепочках обновлений — функциональная форма
  4. Для дорогой инициализации — ленивое начальное состояние
  5. Всегда деструктурируйте результат useState
// Профессиональный паттерн для форм
const useForm = (initialState) => {
const [values, setValues] = useState(initialState);

const handleChange = useCallback((e) => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
}, []);

return { values, handleChange };
};

Вопрос 18. Что такое CORS и как он работает в современных веб-приложениях?

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

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

Правильный ответ: CORS (Cross-Origin Resource Sharing) — это механизм безопасности браузеров, который контролирует доступ к ресурсам на разных доменах (origin). Рассмотрим его работу детально:

Основные понятия

  • Origin (Источник): Комбинация протокола, домена и порта
    https://example.com:443 → origin = https://example.com
  • Same-Origin Policy (SOP): Запрещает кросс-доменные запросы по умолчанию
  • CORS: Механизм ослабления SOP через заголовки HTTP

Типы запросов

  1. Простые (Simple):
    • Методы: GET, POST, HEAD
    • Заголовки: Accept, Accept-Language, Content-Language, Content-Type (только application/x-www-form-urlencoded, multipart/form-data, text/plain)
  2. Предварительные (Preflight):
    • Запросы с пользовательскими заголовками или методами (PUT, DELETE)
    • Браузер сначала отправляет OPTIONS-запрос для проверки прав

Как работает CORS Простые запросы:

  1. Браузер добавляет заголовок Origin
    Origin: https://frontend.com
  2. Сервер отвечает с Access-Control-Allow-Origin
    Access-Control-Allow-Origin: https://frontend.com
    Access-Control-Allow-Credentials: true // Для кук
  3. Браузер проверяет заголовки и разрешает/блокирует доступ

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

sequenceDiagram
Browser->>Server: OPTIONS /api (Origin, Access-Control-Request-Method)
Server-->>Browser: 204 (Allow-Origin, Allow-Methods, Allow-Headers)
Browser->>Server: PUT /api (реальный запрос)
Server-->>Browser: Данные + CORS headers

Заголовки CORS

Заголовок сервераНазначение
Access-Control-Allow-OriginРазрешенные домены (* для всех)
Access-Control-Allow-MethodsРазрешенные HTTP-методы (GET, POST и т.д.)
Access-Control-Allow-HeadersРазрешенные заголовки запроса
Access-Control-Allow-CredentialsРазрешает передачу кук/авторизации (true/false)
Access-Control-Max-AgeВремя кэширования предварительного запроса (в секундах)

Настройка на сервере Пример Express.js:

const cors = require('cors');

app.use(cors({
origin: ['https://trusted.com', 'https://another-domain.com'],
methods: ['GET', 'POST', 'PUT'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
}));

// Или для конкретного роута
app.get('/api', cors(), (req, res) => { ... });

Пример Nginx:

location /api {
add_header 'Access-Control-Allow-Origin' 'https://frontend.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
add_header 'Access-Control-Allow-Credentials' 'true';

if ($request_method = 'OPTIONS') {
return 204;
}
}

Ошибки CORS и их решение

  1. No 'Access-Control-Allow-Origin':

    • Убедитесь, что сервер возвращает правильный заголовок
    • Для разработки: использовать прокси (Create React App proxy, vite.config)
  2. Credentials not supported:

    • Установите credentials: 'include' в fetch:
      fetch(url, {
      credentials: 'include'
      });
    • Сервер должен возвращать Access-Control-Allow-Credentials: true
  3. Preflight failure:

    • Проверьте методы и заголовки в OPTIONS-ответе
    • Убедитесь, что сервер обрабатывает OPTIONS-запросы

Обход CORS (не для продакшена)

  1. Прокси-сервер:

    // На клиенте
    fetch('/proxy?url=https://api.example.com/data')

    // На сервере
    app.get('/proxy', async (req, res) => {
    const response = await fetch(req.query.url);
    res.send(await response.json());
    });
  2. CORS Anywhere (для разработки):

    fetch('https://cors-anywhere.herokuapp.com/https://api.example.com')
  3. Отключение безопасности браузера (только для тестов):

    chrome.exe --disable-web-security --user-data-dir="C:/Temp"

Безопасность CORS

  • Никогда не используйте Access-Control-Allow-Origin: * с Access-Control-Allow-Credentials: true
  • Валидируйте значение Origin на сервере
  • Ограничивайте разрешенные методы и заголовки
  • Используйте CSRF-токены даже с CORS

Совет для интервью: Всегда упоминайте:

  • Разницу между простыми и предварительными запросами
  • Важность обработки OPTIONS-запросов на сервере
  • Опасности неправильной настройки CORS для безопасности

Вопрос 19. Что такое CORS и как он работает в современных веб-приложениях?

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

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

Правильный ответ: CORS (Cross-Origin Resource Sharing) — это механизм безопасности браузеров, который контролирует доступ к ресурсам на разных доменах (origin). Рассмотрим его работу детально:

Основные понятия

  • Origin (Источник): Комбинация протокола, домена и порта
    https://example.com:443 → origin = https://example.com
  • Same-Origin Policy (SOP): Запрещает кросс-доменные запросы по умолчанию
  • CORS: Механизм ослабления SOP через заголовки HTTP

Типы запросов

  1. Простые (Simple):
    • Методы: GET, POST, HEAD
    • Заголовки: Accept, Accept-Language, Content-Language, Content-Type (только application/x-www-form-urlencoded, multipart/form-data, text/plain)
  2. Предварительные (Preflight):
    • Запросы с пользовательскими заголовками или методами (PUT, DELETE)
    • Браузер сначала отправляет OPTIONS-запрос для проверки прав

Как работает CORS Простые запросы:

  1. Браузер добавляет заголовок Origin
    Origin: https://frontend.com
  2. Сервер отвечает с Access-Control-Allow-Origin
    Access-Control-Allow-Origin: https://frontend.com
    Access-Control-Allow-Credentials: true // Для кук
  3. Браузер проверяет заголовки и разрешает/блокирует доступ

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

sequenceDiagram
Browser->>Server: OPTIONS /api (Origin, Access-Control-Request-Method)
Server-->>Browser: 204 (Allow-Origin, Allow-Methods, Allow-Headers)
Browser->>Server: PUT /api (реальный запрос)
Server-->>Browser: Данные + CORS headers

Заголовки CORS

Заголовок сервераНазначение
Access-Control-Allow-OriginРазрешенные домены (* для всех)
Access-Control-Allow-MethodsРазрешенные HTTP-методы (GET, POST и т.д.)
Access-Control-Allow-HeadersРазрешенные заголовки запроса
Access-Control-Allow-CredentialsРазрешает передачу кук/авторизации (true/false)
Access-Control-Max-AgeВремя кэширования предварительного запроса (в секундах)

Настройка на сервере Пример Express.js:

const cors = require('cors');

app.use(cors({
origin: ['https://trusted.com', 'https://another-domain.com'],
methods: ['GET', 'POST', 'PUT'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
}));

// Или для конкретного роута
app.get('/api', cors(), (req, res) => { ... });

Пример Nginx:

location /api {
add_header 'Access-Control-Allow-Origin' 'https://frontend.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
add_header 'Access-Control-Allow-Credentials' 'true';

if ($request_method = 'OPTIONS') {
return 204;
}
}

Ошибки CORS и их решение

  1. No 'Access-Control-Allow-Origin':

    • Убедитесь, что сервер возвращает правильный заголовок
    • Для разработки: использовать прокси (Create React App proxy, vite.config)
  2. Credentials not supported:

    • Установите credentials: 'include' в fetch:
      fetch(url, {
      credentials: 'include'
      });
    • Сервер должен возвращать Access-Control-Allow-Credentials: true
  3. Preflight failure:

    • Проверьте методы и заголовки в OPTIONS-ответе
    • Убедитесь, что сервер обрабатывает OPTIONS-запросы

Обход CORS (не для продакшена)

  1. Прокси-сервер:

    // На клиенте
    fetch('/proxy?url=https://api.example.com/data')

    // На сервере
    app.get('/proxy', async (req, res) => {
    const response = await fetch(req.query.url);
    res.send(await response.json());
    });
  2. CORS Anywhere (для разработки):

    fetch('https://cors-anywhere.herokuapp.com/https://api.example.com')
  3. Отключение безопасности браузера (только для тестов):

    chrome.exe --disable-web-security --user-data-dir="C:/Temp"

Безопасность CORS

  • Никогда не используйте Access-Control-Allow-Origin: * с Access-Control-Allow-Credentials: true
  • Валидируйте значение Origin на сервере
  • Ограничивайте разрешенные методы и заголовки
  • Используйте CSRF-токены даже с CORS

Совет для интервью: Всегда упоминайте:

  • Разницу между простыми и предварительными запросами
  • Важность обработки OPTIONS-запросов на сервере
  • Опасности неправильной настройки CORS для безопасности

Вопрос 19. Какие методы аутентификации вы использовали в проектах и как их реализовывали?

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

Ответ собеседника: Правильный. Упомянул JWT-токены для защиты запросов к бэкенду.

Правильный ответ: В современных веб-приложениях применяется несколько стратегий аутентификации, выбор которых зависит от требований безопасности, архитектуры и UX. Разберём ключевые подходы:


1. JWT (JSON Web Tokens) Как работает:

  1. Сервер генерирует токен после успешного входа:
    // Пример генерации (Node.js)
    const token = jwt.sign(
    { userId: user.id, role: 'admin' },
    process.env.JWT_SECRET,
    { expiresIn: '1h' }
    );
  2. Клиент сохраняет токен (обычно в localStorage или httpOnly cookie)
  3. Каждый запрос включает токен в заголовке Authorization: Bearer <token>

Преимущества:

  • Stateless-архитектура (не требует хранения сессий на сервере)
  • Легкая масштабируемость микросервисов
  • Поддержка произвольных данных в payload

Недостатки:

  • Невозможность отзыва до истечения срока (решается через blacklist)
  • Уязвимость к XSS-атакам при хранении в localStorage

Защита:

  • Короткие сроки жизни токенов (15-30 мин)
  • Механизм refresh-токенов:
    // Пример обновления
    app.post('/refresh', (req, res) => {
    const refreshToken = req.cookies.refresh_token;
    if (!isValid(refreshToken)) return res.sendStatus(401);

    const newAccessToken = jwt.sign(payload, secret, { expiresIn: '15m' });
    res.json({ accessToken: newAccessToken });
    });

2. Сессионная аутентификация Как работает:

  1. Сервер создает сессию в БД после входа
  2. Идентификатор сессии (sessionId) передаётся через cookie:
    // Express.js с Redis
    app.use(session({
    store: new RedisStore({ client: redisClient }),
    secret: 'keyboard cat',
    resave: false,
    saveUninitialized: false,
    cookie: {
    secure: true,
    httpOnly: true,
    sameSite: 'strict'
    }
    }));
  3. Сервер проверяет сессию при каждом запросе

Преимущества:

  • Возможность мгновенного отзыва доступа
  • Защита от XSS через httpOnly cookies
  • Подходит для монолитных приложений

Недостатки:

  • Stateful-архитектура (проблемы с масштабированием)
  • Требует хранилища сессий (Redis, PostgreSQL)

3. OAuth 2.0 / OpenID Connect Сценарии использования:

  • Вход через соцсети (Google, Facebook)
  • Авторизация между микросервисами
  • Предоставление доступа сторонним приложениям

Потоки (grant types):

  • Authorization Code: Для веб-серверов
  • Implicit: Устаревший, не рекомендуется
  • Client Credentials: Для сервис-сервисного взаимодействия
  • Device Code: Для IoT/SmartTV

Пример реализации (Passport.js):

// Google OAuth
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_SECRET,
callbackURL: '/auth/google/callback'
}, (accessToken, refreshToken, profile, done) => {
// Поиск/создание пользователя в БД
User.findOrCreate({ googleId: profile.id }, done);
}));

// Маршрут для фронтенда
app.get('/auth/google', passport.authenticate('google', { scope: ['profile'] }));

4. API-ключи Использование:

  • Сервер-серверное взаимодействие
  • Мобильные приложения
  • Публичные API (с ограничениями)

Реализация:

// Middleware проверки ключа
const apiKeyAuth = (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (!isValidKey(apiKey)) return res.sendStatus(401);
next();
};

// Использование в роуте
app.get('/api/data', apiKeyAuth, (req, res) => { ... });

5. Двухфакторная аутентификация (2FA) Методы:

  • TOTP (Time-Based One-Time Password): Google Authenticator
  • SMS-коды (менее безопасно)
  • Аппаратные ключи (YubiKey)

Пример реализации TOTP:

const speakeasy = require('speakeasy');

// Генерация секрета
const secret = speakeasy.generateSecret({ length: 20 });

// Верификация кода
const verified = speakeasy.totp.verify({
secret: user.totpSecret,
encoding: 'base32',
token: req.body.code
});

Критерии выбора метода

ПараметрJWTСессииOAuthAPI-ключи
Масштабируемость★★★★★★★☆☆☆★★★★☆★★★★☆
Безопасность★★★☆☆★★★★☆★★★★★★★★☆☆
UX для пользователя★★★★☆★★★★★★★★★★★☆☆☆☆
Сложность реализации★★☆☆☆★★★☆☆★★★★★★☆☆☆☆

Рекомендации по безопасности

  1. Всегда используйте HTTPS
  2. Храните токены правильно:
    • AccessToken: localStorage (для SPA) или httpOnly cookie
    • RefreshToken: Только httpOnly cookie с SameSite=Strict
  3. Реализуйте CORS правильно:
    app.use(cors({
    origin: ['https://your-domain.com'],
    credentials: true
    }));
  4. Защищайтесь от атак:
    • CSRF: SameSite cookies, CSRF-токены
    • XSS: Санитизация ввода, CSP-заголовки
    • Bruteforce: Лимиты запросов (rate limiting)

Итог: В современных проектах оптимально комбинировать методы — например, основной вход через JWT/OAuth + 2FA для критических операций. Для высоконагруженных систем предпочтительны stateless-решения (JWT), для финансовых приложений — сессии с 2FA. Всегда учитывайте требования PCI DSS и GDPR при работе с пользовательскими данными.

Вопрос 20. Что такое интерфейс в программировании и как он применяется на практике?

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

Ответ собеседника: Неправильный. Повторил предыдущий неверный ответ про типизацию объектов, не раскрыв концепцию контрактов.

Правильный ответ: Интерфейс — это фундаментальная концепция программирования, определяющая контракт между компонентами системы. Он задаёт спецификацию поведения (методы, свойства), которую должны реализовать классы или структуры, без описания внутренней логики. Рассмотрим ключевые аспекты:


1. Основные принципы

  • Контракт, а не реализация: Интерфейс определяет что должно быть сделано, но не как
  • Абстракция: Сокрытие деталей реализации за строго определённым API
  • Полиморфизм: Возможность использовать разные реализации через единый интерфейс
// Пример в Go
type Storage interface {
Get(key string) ([]byte, error)
Set(key string, value []byte) error
Delete(key string) error
}

2. Реализация в разных языках #Go (Неявная реализация)

type Logger interface {
Log(message string)
}

type FileLogger struct{}

func (f FileLogger) Log(message string) {
// Запись в файл
}

// Использование
func Process(logger Logger) {
logger.Log("Запуск")
}

// Автоматическая реализация интерфейса
Process(FileLogger{})

#TypeScript (Явная реализация)

interface Serializer {
serialize(data: object): string;
}

class JSONSerializer implements Serializer {
serialize(data: object): string {
return JSON.stringify(data);
}
}

#Java (Множественные интерфейсы)

interface Drawable {
void draw();
}

interface Resizable {
void resize(double factor);
}

class Circle implements Drawable, Resizable {
// Реализация методов
}

3. Практическое применение Паттерн "Стратегия":

type PaymentStrategy interface {
Pay(amount float64) error
}

type CreditCardStrategy struct{}

func (c CreditCardStrategy) Pay(amount float64) error {
// Логика оплаты картой
}

type PayPalStrategy struct{}

func (p PayPalStrategy) Pay(amount float64) error {
// Логика PayPal
}

func ProcessOrder(amount float64, strategy PaymentStrategy) error {
return strategy.Pay(amount)
}

Тестирование (Mocking):

// Мок для тестов
type MockStorage struct {}

func (m MockStorage) Get(key string) ([]byte, error) {
return []byte("test_value"), nil
}

func TestService(t *testing.T) {
service := NewService(MockStorage{})
result := service.FetchData("test_key")
// Проверки
}

4. Преимущества использования

  1. Снижение связанности: Компоненты зависят от абстракций, а не от конкретных реализаций
  2. Упрощение расширения: Новые реализации не требуют изменения клиентского кода
  3. Улучшение тестируемости: Легкая подмена реальных зависимостей моками
  4. Принцип SOLID:
    • D (Dependency Inversion): Зависимость от абстракций
    • I (Interface Segregation): Много специализированных интерфейсов вместо одного "толстого"

5. Интерфейсы vs Абстрактные классы

КритерийИнтерфейсыАбстрактные классы
СостояниеТолько контракт (без полей)Могут содержать поля
НаследованиеМножественная реализацияЕдиное наследование
МетодыТолько сигнатурыМожет содержать реализацию
ЯзыкиGo, TypeScript, Java, C#Java, C++, C#

6. Продвинутые концепции Расширение интерфейсов (TypeScript):

interface Animal {
name: string;
}

interface Bear extends Animal {
honey: boolean;
}

const bear: Bear = {
name: "Winnie",
honey: true
};

Пустые интерфейсы (Go для универсальных функций):

interface{} // Аналог any в TypeScript

func Print(value interface{}) {
fmt.Println(value)
}

Generic-интерфейсы (TypeScript):

interface Repository<T> {
get(id: string): T;
save(entity: T): void;
}

class UserRepository implements Repository<User> {
// Реализация для типа User
}

7. Антипаттерны

  1. "Божественный интерфейс":

    // Плохо!
    type GodInterface interface {
    Read() []byte
    Write([]byte)
    Validate() bool
    Connect() error
    // ... 20+ методов
    }

    // Решение: Разделить на:
    type Reader interface { Read() []byte }
    type Writer interface { Write([]byte) }
  2. Избыточные зависимости:

    • Интерфейс должен требовать только необходимые методы
  3. Интерфейсы для единичных реализаций:

    • Не создавайте интерфейсы "на будущее" без реальной необходимости

Итог: Интерфейсы — мощный инструмент для создания гибких, тестируемых и поддерживаемых систем. Ключевые правила:

  • Соблюдайте принцип ISP (Interface Segregation Principle)
  • Используйте Dependency Injection для передачи реализаций
  • Предпочитайте маленькие, специализированные интерфейсы
  • В Go: интерфейсы определяются там, где они используются, а не там, где реализуются

Вопрос 21. Какой у вас опыт работы с базами данных и какие технологии вы использовали?

Таймкод: 00:23:37

Ответ собеседника: Неполный. Упомянул pet-проект с SQL-запросами, но без конкретики по технологиям.

Правильный ответ: Работа с базами данных — критически важный навык бэкенд-разработчика. Рассмотрим ключевые технологии и практики:


1. Реляционные БД (SQL) #PostgreSQL

  • Проекты: Система аналитики для e-commerce
  • Особенности:
    • JSONB для гибридных данных
    • Продвинутые индексы (GIN, GiST)
    • Триггеры для аудита изменений
    CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    data JSONB NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
    );

    CREATE INDEX idx_orders_data ON orders USING GIN (data);

#MySQL/MariaDB

  • Проекты: Высоконагруженный API для мобильного приложения
  • Оптимизации:
    • Шардирование по диапазонам
    • Репликация Master-Slave
    • Настройка InnoDB Buffer Pool
    -- Пример шардирования
    CREATE TABLE logs_2023 (
    CHECK ( YEAR(created_at) = 2023 )
    ) INHERITS (logs);

2. NoSQL БД #MongoDB

  • Проекты: Система обработки логов
  • Сценарии использования:
    • Гибкая схема для быстроразвивающихся продуктов
    • Агрегации для аналитики в реальном времени
    db.logs.aggregate([
    { $match: { level: "error" } },
    { $group: {
    _id: "$service",
    count: { $sum: 1 }
    }}
    ]);

#Redis

  • Использование:
    • Кеширование результатов запросов (с TTL)
    • Счетчики rate-limiting
    • Pub/Sub для уведомлений
    // Пример кеширования в Go
    func GetUser(id string) (*User, error) {
    data, err := redis.Get(ctx, "user:"+id).Bytes()
    if err == redis.Nil {
    user, err := db.FetchUser(id)
    redis.Set(ctx, "user:"+id, user, 5*time.Minute)
    return user, nil
    }
    // Десериализация данных
    }

3. ORM/Query Builders #GORM (Go)

  • Преимущества: Миграции, eager loading, хуки
  • Пример:
    type User struct {
    gorm.Model
    Name string
    Email string `gorm:"uniqueIndex"`
    }

    // Автомиграции
    db.AutoMigrate(&User{})

    // Запрос с условием
    var users []User
    db.Where("name LIKE ?", "%alice%").Find(&users)

#Knex.js (Node.js)

  • Использование:
    • Построение сложных запросов
    • Миграции и сидеры
    knex('users')
    .where({ active: true })
    .join('orders', 'users.id', 'orders.user_id')
    .select('users.*', 'orders.total')
    .orderBy('orders.total', 'desc')
    .limit(10);

4. Оптимизация запросов Индексы:

-- Составной индекс
CREATE INDEX idx_users_name_email ON users (name, email);

Анализ производительности:

EXPLAIN ANALYZE
SELECT * FROM orders WHERE total > 1000;

Нормализация/денормализация:

  • Нормализация до 3NF для целостности данных
  • Денормализация для отчетных таблиц (OLAP)

5. Работа с большими данными Партиционирование:

-- По диапазону дат
CREATE TABLE sales_2023 PARTITION OF sales
FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');

Шардирование:

  • Горизонтальное разделение по ключу (user_id, geo)
  • Инструменты: Vitess, Citus

Колонковые БД (ClickHouse):

  • Для аналитических запросов
  • Высокая скорость агрегации

6. Транзакции и ACID Пример банковской операции:

tx := db.Begin()
if err := tx.Model(&Account{}).Where("id = ?", from).Update("balance", gorm.Expr("balance - ?", amount)).Error; err != nil {
tx.Rollback()
return err
}
// Аналогично для получателя...
tx.Commit()

Уровни изоляции:

  • Read Committed (по умолчанию в PostgreSQL)
  • Serializable для строгой консистентности

7. Инфраструктура Реализация в проектах:

  • Backup/Restore: WAL-G для PostgreSQL, mongodump
  • Мониторинг: Prometheus + Grafana (запросы в секунду, время ответа)
  • Миграции: Liquibase, Flyway

Пример CI/CD пайплайна:

steps:
- name: Run migrations
run: |
./migrate -database $DATABASE_URL -path db/migrations up
env:
DATABASE_URL: postgres://user:pass@host/db?sslmode=disable

8. Бессерверные БД

  • AWS Aurora Serverless: Автомасштабирование
  • Firebase Realtime DB: Для мобильных приложений
  • Supabase: OpenSource альтернатива Firebase

Итог: Профессиональная работа с БД включает:

  1. Выбор подходящего типа БД под задачу
  2. Оптимизацию схемы и запросов
  3. Обеспечение отказоустойчивости (репликация, бэкапы)
  4. Мониторинг и анализ производительности
  5. Соблюдение принципов безопасности (инъекции, RBAC)

Для резюме указывайте конкретные технологии (например: "Оптимизировал SQL-запросы в PostgreSQL, сократив время выполнения с 2s до 50ms через составные индексы") и масштаб данных ("Работал с таблицами на 100M+ записей").

Вопрос 22. Когда ожидать обратную связь по результатам собеседования и как действовать кандидату?

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

Ответ собеседника: Правильный. Интервьюер сообщил о стандартном сроке обратной связи в течение недели.

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

Стандартные сроки

  1. 1-3 рабочих дня:

    • Для скрининговых собеседований
    • В быстроработающих стартапах
    • При срочном закрытии вакансии
  2. 3-7 рабочих дней:

    • Стандартный срок для большинства компаний
    • Время на согласование с командой и HR
    • Типичный ответ: "Мы свяжемся в течение недели"
  3. 7-14 рабочих дней:

    • Для senior-позиций с несколькими этапами
    • В корпорациях с многоуровневой системой согласований
    • При наличии других кандидатов в финальном пуле

Факторы, влияющие на сроки

  1. Количество участников процесса:
    • Чем больше интервьюеров вовлечено, тем дольше согласование
  2. Сезонность:
    • Декабрь/Январь — возможны задержки из-за праздников
    • Конец квартала — занятость финансовых отделов
  3. Внутренние процессы:
    • Сбор фидбека от всех интервьюеров
    • Проверка рекомендаций
    • Согласование бюджета на позицию

Что делать, если нет ответа

  1. Вежливое напоминание:
    • После 7 рабочих дней отправьте лаконичное письмо:
    Тема: Уточнение по статусу позиции [Название]

    Здравствуйте, [Имя]!

    Благодарю за возможность пообщаться на позицию [роль] [дата].
    Интересуюсь, есть ли обновления по процессу?

    С уважением,
    [Ваше имя]
  2. Правила коммуникации:
    • Не беспокойте чаще 1 раза в неделю
    • Используйте тот же канал (Email/LinkedIn), где шло общение
  3. Параллельные процессы:
    • Продолжайте собеседования в других компаниях
    • Не откладывайте другие предложения в ожидании ответа

Как интерпретировать ответы

СитуацияВероятная интерпретацияРекомендации
"Мы вам перезвоним"Стандартная формальностьЖдите 5-7 дней
"Решим на неделе"Вы в финальном спискеУточните конкретный день
"Ждем решения руководства"Идет согласование бюджета/штаткиЗапросите примерные сроки
Молчание 2+ недельВы резервный кандидат/процесс затянулсяНачинайте follow-up

Почему могут задерживать ответ

  1. Технические причины:
    • Ожидание результатов тестового задания от других кандидатов
    • Смена приоритетов в команде
  2. Организационные сложности:
    • Бюджетные согласования
    • Реорганизация отдела
  3. Человеческий фактор:
    • Болезнь ключевого интервьюера
    • Отпуск HR-менеджера

Рекомендации для кандидата

  1. Фиксируйте детали:
    • Записывайте имена интервьюеров и сроки, которые они называли
  2. Анализируйте этапы:
    • После 3+ туров уточните: "Какие следующие шаги и их сроки?"
  3. Сохраняйте профессионализм:
    • Даже при отказе поблагодарите за время
    • Запросите фидбек для улучшения:
    Благодарю за обратную связь. 
    Не могли бы вы поделиться рекомендациями по улучшению?
  4. Не принимайте паузу на свой счет:
    • 60% задержек не связаны с качеством вашего собеседования

Важно: В IT-сфере, особенно для senior-ролей, процесс может занимать 2-4 недели из-за технических проверок и согласований. Для позиций в FAANG-компаниях сроки иногда растягиваются до 2 месяцев из-за многоэтапности отбора.