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

РЕАЛЬНОЕ СОБЕСЕДОВАНИЕ НА MIDDLE FRONTEND РАЗРАБОТЧИКА | JAVASCRIPT REACT #frontend #собеседование

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

Сегодня мы разберём техническое собеседование с кандидатом на позицию фронтенд-разработчика, в ходе которого проверялись знания JavaScript, ООП, паттернов проектирования, принципов SOLID, а также архитектурное мышление на примере практической задачи с React и менеджментом состояния. Кандидат продемонстрировал уверенное владение базовыми конъюнктами языка, понимание работы промисов, event loop, различий между var/let/const, а также принципов ООП и SOLID, хотя признал ограниченный опыт применения паттернов проектирования и написания тестов в реальных проектах. В финале обсуждалась архитектура простого React-приложения с акцентом на переиспользуемые компоненты, управление состоянием и оптимизацию рендеринга при работе с большими списками.

Вопрос 1. Какие типы данных бывают в JavaScript и что о них можно рассказать?

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

Ответ собеседника: Неполный. Были названы примитивные типы: строка, число, символ, булево значение, объект, undefined, null. Упомянут также bigint, но не было явно названо значение boolean — вместо этого использовалось размытое «бун». Ответ в целом охватывает основные типы, но с неточностями в формулировках.

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

В JavaScript существует 8 типов данных, которые делятся на две категории: примитивные и ссылочные.

Примитивные типы (7 штук)

1. Number Представляет как целые числа, так и числа с плавающей точкой. Хранятся в формате IEEE 754 (64-битный double). Особые значения: Infinity, -Infinity, NaN. Максимальное безопасное целое число — Number.MAX_SAFE_INTEGER (2^53 - 1).

2. BigInt Введён для работы с целыми числами произвольной точности. Обозначается суффиксом n (например, 12345678901234567890n). Нельзя смешивать с Number в арифметических операциях без явного приведения.

3. String Последовательность UTF-16 символов. Неизменяемый тип. Поддерживает шаблонные строки (template literals), интерполяцию и все стандартные методы работы со строками.

4. Boolean Два значения: true и false. Критически важно понимать, что JavaScript имеет систему truthy/falsy: значения 0, "", null, undefined, NaN, false являются falsy, всё остальное — truthy.

5. Undefined Означает, что переменная объявлена, но значение не присвоено. Это не то же самое, что nullundefined указывает на отсутствие инициализации.

6. Null Представляет намеренное отсутствие значения. Известная особенность: typeof null === 'object' — это исторический баг языка, который сохраняется для обратной совместимости.

7. Symbol Уникальный и неизменяемый примитив, используется как идентификатор для свойств объектов. Гарантированно уникален даже при одинаковом описании. Применяется для создания «скрытых» свойств и в метапрограммировании (Symbol.iterator, Symbol.toPrimitive и др.).

Ссылочный тип

8. Object Включает обычные объекты, массивы, функции, даты, регулярные выражения и другие структуры. Хранятся по ссылке. Сравнение объектов идёт по ссылке, а не по значению.

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

Оператор typeof возвращает строку с типом, но имеет нюансы:

  • typeof null === 'object' (баг)
  • typeof function(){} === 'function' (хотя function — подтип Object)
  • typeof [] === 'object'

Для точного определения типа используют Object.prototype.toString.call(value).

Важно для Go-разработчика: в отличие от статически типизированного Go, JavaScript — динамически типизированный язык. В Go типы проверяются на этапе компиляции, в JavaScript — во время выполнения. Это означает, что ошибки типизации в JS проявляются только в runtime, что делает тестирование и линтинг критически важными.

Вопрос 2. Чем отличаются undefined и null?

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

Ответ собеседника: Правильный. undefined означает, что переменная не определена (не было присвоено значение), а null — это явно присвоенное нулевое значение, то есть значение есть, но оно равно null. Ответ верный: undefined — отсутствие определения, null — намеренное отсутствие значения.

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

Ответ кандидата корректен, дополним деталями.

undefined — значение по умолчанию, которое JavaScript присваивает автоматически. Переменная объявлена, но в неё ничего не записали. Также функция без return возвращает undefined. Обращение к несуществующему свойству объекта или элементу массива тоже даёт undefined.

null — значение, которое разработчик присваивает явно, показывая намеренное отсутствие значения. Это говорит: «здесь должно было быть значение, но его сейчас нет».

Ключевые различия на практике:

  • typeof null === 'object' (исторический баг), а typeof undefined === 'undefined'
  • null == undefined — true (нестрогое равенство), но null === undefined — false
  • При JSON-сериализации null сохраняется, а undefined удаляется из объекта
  • В числовом контексте: Number(null) === 0, Number(undefined) === NaN

Аналогия из Go: в Go nil для указателей и интерфейсов ближе к null — явное указание на отсутствие значения. Нулевые значения типов (zero value) вроде "" для строк или 0 для int ближе к undefined — это то, что получается «по умолчанию» при объявлении без инициализации.

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

  • null — когда вы осознанно хотите обнулить значение (очистить ссылку, сбросить состояние)
  • undefined — обычно не присваивают вручную, оставляют это на усмотрение движка

В коде принято использовать null для явного обозначения пустоты, а undefined воспринимать как маркер «не инициализировано».

Вопрос 3. Равны ли null и undefined при строгом и нестрогом сравнении и в чём разница между строгим и нестрогим сравнением?

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

Ответ собеседника: Правильный. При нестрогом сравнении (==) null и undefined равны. При строгом сравнении (===) они не равны, потому что строгое сравнение проверяет и тип, и значение без приведения типов, а нестрогое сравнение предполагает приведение к одному типу перед сравнением.

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

Ответ кандидата полностью верен, дополним деталями о механике приведения типов.

Результаты сравнения null и undefined:

ВыражениеРезультат
null == undefinedtrue
null === undefinedfalse
null == nulltrue
undefined == undefinedtrue

Как работает == (Abstract Equality Comparison Algorithm)

При нестрогом сравнении JavaScript применяет алгоритм приведения типов (type coercion). Для null и undefined действует специальное правило: они равны друг другу и не равны ничему другому. Это единственный случай, когда приведение типов происходит без преобразования в число.

Другие примеры приведения через ==:

'5' == 5 // true — строка приводится к числу
false == 0 // true — boolean приводится к числу
'' == 0 // true — пустая строка приводится к числу
'' == false // true — оба приводятся к 0

Как работает === (Strict Equality)

Строгое сравнение сначала проверяет типы. Если типы разные — сразу false, без каких-либо преобразований. Если типы одинаковые — сравнивает значения.

Аналогия из Go: в Go нет нестрогого сравнения для разных типов — компилятор просто не даст сравнить int со string. Это одна из ключевых разниц между статически типизированным Go и динамическим JavaScript. В Go безопасность типов обеспечивается на этапе компиляции, в JS — на разработчике лежит ответственность за предсказуемость сравнений.

Практическая рекомендация: всегда используйте === и !==. Это устраняет целый класс трудноотлавливаемых багов, связанных с неявным приведением типов. Исключения — сравнение с null или undefined через ==, когда нужно проверить оба варианта: value == null вернёт true и для null, и для undefined.

Вопрос 4. К какому типу приводятся операнды при нестрогом сравнении?

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

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

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

Ответ кандидата в целом верен, но требует уточнения. Приведение типов при == определяется не одним правилом, а полным алгоритмом Abstract Equality Comparison из спецификации ECMAScript.

Общий принцип алгоритма ==

Алгоритм рассматривает типы операндов и применяет разные правила в зависимости от комбинации:

1. Один из операндов — Boolean Boolean всегда приводится к числу первым: true → 1, false → 0. Затем сравнение продолжается уже с числом.

true == 1 // true → Number(true) == 1 → 1 == 1
false == 0 // true → Number(false) == 0 → 0 == 0

2. Один — Number, другой — String Строка приводится к числу.

'5' == 5 // true → Number('5') == 5 → 5 == 5
'' == 0 // true → Number('') == 0 → 0 == 0
'abc' == NaN // false → Number('abc') == NaN → NaN == NaN → false

3. Один — Object, другой — примитив Объект приводится к примитиву через внутренний метод ToPrimitive, который вызывает valueOf() и/или toString().

[1,2] == '1,2' // true → [1,2].toString() == '1,2'
42 == [42] // true → Number([42].toString()) == 42
null == {} // false — null и undefined не приводятся к Object

4. null и undefined — особый случай null == undefinedtrue. Они не приводятся к числу, это прямое правило спецификации. И они не равны ничему другому.

5. NaN никогда не равен ничему, даже самому себе

NaN == NaN // false

6. Symbol нельзя привести к числу неявно

Symbol('x') == 0 // TypeError в strict mode или false

Почему это важно для Go-разработчика:

В Go оператор == работает строго — компилятор не даст сравнить значения несовместимых типов. Это исключает целый класс ошибок на этапе компиляции. В JavaScript неявное приведение типов — один из самых частых источников багов, поэтому в современной разработке принято использовать линтеры с правилом eqeqeq, которое требует применять === везде, где это возможно.

Запомните правило: при == boolean → number, string → number (если рядом с number), object → primitive. Во всех остальных случаях — читайте спецификацию или просто используйте ===.

Вопрос 5. Для чего в JavaScript добавлен тип BigInt, если уже есть Number?

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

Ответ собеседника: Неполный. BigInt нужен для работы с числами, которые превышают максимальное безопасное целое число типа Number (2^53 - 1). Кандидат не смог точно назвать максимальное значение, но верно указал, что Number ограничен и что при превышении этого предела числа могут терять точность. Ответ по сути правильный, но неполный в деталях.

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

Ограничение Number

Тип Number в JavaScript хранится в 64-битном формате IEEE 754 (double-precision floating point). Из этих 64 бит 52 отведены под мантиссу (significand) плюс 1 неявный бит, что даёт 53 бита для представления целой части.

Максимальное безопасное целое число: Number.MAX_SAFE_INTEGER = 2^53 - 1 = 9007199254740991

За этим пределом целые числа теряют точность:

9007199254740992 === 9007199254740993 // true (!)
Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 // true (!)

Это происходит, потому что формат с плавающей точкой не может представить каждое целое число — он начинает «перескакивать» через значения.

Что такое BigInt

BigInt — это тип для целых чисел произвольной точности (arbitrary-precision integers). Он не имеет верхнего предела, ограничен только объёмом доступной памяти.

Создание:

const big1 = 9007199254740991n // суффикс n
const big2 = BigInt("900719925474099123456789") // через конструктор

Ключевые ограничения BigInt

  • Нельзя смешивать с Number в арифметике:
10n + 5 // TypeError: Cannot mix BigInt and other types
10n + 5n // 15n — OK
  • Нельзя использовать с Math объектом:
Math.sqrt(4n) // TypeError
  • При делении дробная часть отбрасывается:
5n / 2n // 2n, не 2.5n
  • typeof 1n === 'bigint'

  • Нельзя использовать унарный плюс: +1n выбросит TypeError

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

  • Финансовые расчёты с большими суммами в минимальных единицах (копейки, центы)
  • Криптография и работа с хэшами
  • Уникальные идентификаторы (Twitter Snowflake ID, которые превышают 2^53)
  • Точные вычисления с датами в наносекундах
  • Математические задачи с большими простыми числами

Аналогия из Go: в Go есть пакет math/big с типами big.Int, big.Float, big.Rat. Они работают аналогично BigInt в JavaScript — обеспечивают произвольную точность, но требуют явного использования методов вместо стандартных операторов. В JavaScript же BigInt поддерживает привычную арифметику через +, -, *, /, %, **, что делает код чище.

Практический совет: если работаете с ID из внешних API (особенно Twitter/X, Discord, Snowflake), всегда парсите их как BigInt или как строки, а не как Number — иначе потеряете точность и получите некорректные идентификаторы.

Вопрос 6. Что такое Promise и как его можно описать?

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

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

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

Ответ кандидата корректен. Дополним техническими деталями.

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

Три состояния Promise:

  • pending — начальное состояние, операция ещё не завершена
  • fulfilled — операция успешно завершена, значение получено
  • rejected — операция завершилась с ошибкой

Важно: Promise переходит из pending в одно из финальных состояний ровно один раз. Это необратимый переход — после этого состояние не меняется.

Создание Promise:

const promise = new Promise((resolve, reject) => {
// Асинхронная операция
setTimeout(() => {
if (success) {
resolve(result); // переводит в fulfilled
} else {
reject(error); // переводит в rejected
}
}, 1000);
});

Потребление результата:

promise
.then(value => console.log(value)) // обработка fulfilled
.catch(error => console.error(error)) // обработка rejected
.finally(() => console.log('done')); // выполняется всегда

Цепочки и проброс значений:

fetch('/api/user')
.then(response => response.json())
.then(user => fetch(`/api/orders/${user.id}`))
.then(response => response.json())
.then(orders => console.log(orders))
.catch(err => console.error(err)); // поймает ошибку из любого шага

Статические методы:

  • Promise.resolve(value) — сразу возвращает успешный Promise
  • Promise.reject(error) — сразу возвращает отклонённый Promise
  • Promise.all([p1, p2, p3]) — ждёт все, падает при первой ошибке
  • Promise.allSettled([p1, p2, p3]) — ждёт все, возвращает результаты каждого
  • Promise.race([p1, p2, p3]) — результат первого завершившегося
  • Promise.any([p1, p2, p3]) — результат первого успешного

Аналогия из Go: Promise ближе всего к паттерну каналов в Go. Канал — это тоже «обещание» получить значение в будущем:

ch := make(chan int)

go func() {
result := doWork()
ch <- result // аналог resolve
}()

value := <-ch // аналог await / .then()

Разница в том, что в Go каналы более гибкие — можно закрыть канал (аналог reject), использовать select для ожидания нескольких каналов (аналог Promise.race), а буферизированные каналы позволяют отправить значение без блокировки отправителя.

Promise vs async/await:

async/await — это синтаксический сахар над Promise. Функция с async всегда возвращает Promise, а await приостанавливает выполнение до разрешения Promise:

async function getUserOrders(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
return user.orders;
} catch (error) {
console.error(error);
throw error;
}
}

Код выглядит синхронным, но работает асинхронно — это делает его значительно читабельнее цепочек .then().

Вопрос 7. Можно ли не вызвать resolve и reject в Promise и что произойдёт в этом случае?

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

Ответ собеседника: Правильный. Если не вызвать ни resolve, ни reject внутри Promise, он останется в состоянии pending бесконечно. Это может привести к утечкам памяти или зависанию приложения. Ответ правильный, кандидат верно указал на необходимость предусматривать таймауты.

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

Ответ кандидата полностью корректен. Дополним техническими деталями и паттернами защиты.

Что происходит с «зависшим» Promise:

Promise, в котором не вызван ни resolve, ни reject, остаётся в состоянии pending навсегда. Все подписчики (.then(), .catch(), await) будут ждать бесконечно.

const hangingPromise = new Promise((resolve, reject) => {
// ничего не вызываем
// или ранний return до resolve/reject
if (!isValid) return; // забыли вызвать reject
resolve(data);
});

await hangingPromise; // зависнет навсегда

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

  • Утечка памяти — Promise и его замыкания не будут собраны сборщиком мусора
  • Зависание цепочек вызовов и async-функций
  • В серверных приложениях — зависание обработки HTTP-запросов, исчерпание пула соединений
  • В браузере — блокировка взаимодействия пользователя с интерфейсом (если ждём через await в критическом пути)

Паттерны защиты:

1. Таймаут через Promise.race:

function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}

// Использование
try {
const result = await withTimeout(fetchData(), 5000);
} catch (err) {
console.error(err.message); // "Timeout after 5000ms"
}

2. AbortController для fetch:

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);

try {
const response = await fetch('/api/data', {
signal: controller.signal
});
clearTimeout(timeoutId);
const data = await response.json();
} catch (err) {
if (err.name === 'AbortError') {
console.log('Request timed out');
}
}

3. Всегда вызывайте resolve или reject во всех ветвях:

// Плохо — ветка без reject
new Promise((resolve, reject) => {
if (data) {
resolve(data);
}
// если !data — Promise зависнет
});

// Хорошо — все ветки покрыты
new Promise((resolve, reject) => {
if (data) {
resolve(data);
} else {
reject(new Error('No data'));
}
});

Аналогия из Go: в Go аналогичная проблема — горутина, которая пишет в канал, но никогда не отправляет значение. Читатель заблокируется навсегда. Защита — select с time.After:

select {
case value := <-ch:
fmt.Println(value)
case <-time.After(5 * time.Second):
fmt.Println("timeout")
}

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

Вопрос 8. Есть ли у Promise встроенный функционал для реализации таймаута?

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

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

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

У Promise нет встроенного метода таймаута в самом классе Promise. Однако стандартная библиотека предоставляет несколько способов реализации.

1. Promise.race — классический подход

function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
});
return Promise.race([promise, timeout]);
}

// Использование
try {
const data = await withTimeout(fetch('/api/data'), 5000);
} catch (err) {
if (err.message.includes('Timeout')) {
console.log('Превышено время ожидания');
}
}

Важный нюанс: Promise.race не отменяет исходную операцию — он просто игнорирует её результат. Запрос fetch продолжит выполняться в фоне. Для реальной отмены нужен AbortController.

2. AbortController — реальная отмена запроса

async function fetchWithTimeout(url, ms) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ms);

try {
const response = await fetch(url, { signal: controller.signal });
return await response.json();
} finally {
clearTimeout(timeoutId); // очищаем таймер при успехе
}
}

AbortController — это стандартный механизм отмены, поддерживаемый fetch, ReadableStream, EventSource и другими Web API. При вызове abort() генерируется ошибка AbortError.

3. AbortSignal.timeout (современный способ)

Начиная с Node.js 18+ и современных браузеров:

const response = await fetch('/api/data', {
signal: AbortSignal.timeout(5000)
});

Это встроенная обёртка, создающая AbortController с автоматическим таймаутом. Самый лаконичный способ.

4. AbortSignal.any — комбинирование сигналов

const userSignal = new AbortController();
const timeoutSignal = AbortSignal.timeout(5000);

const response = await fetch('/api/data', {
signal: AbortSignal.any([userSignal.signal, timeoutSignal])
});

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

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

ПодходОтменяет операциюПоддержка
Promise.raceНет, только игнорирует результатВезде
AbortControllerДаВсе современные среды
AbortSignal.timeoutДаNode 18+, современные браузеры

Аналогия из Go: в Go для таймаутов используется context.WithTimeout:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)

context.Context в Go — более развитый механизм отмены, чем в JavaScript. Он поддерживает дерево контекстов, проброс значений и стандартизирован во всей экосистеме. AbortController в JavaScript выполняет схожую роль, но его поддержка зависит от конкретного API.

Вопрос 9. Как async/await относится к Promise?

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

Ответ собеседова: Правильный. async/await — это синтаксический сахар над Promise. Асинхронная функция всегда возвращает Promise, а await позволяет писать асинхронный код в синхронном стиле, без цепочек .then() и .catch(). Ответ правильный.

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

Ответ кандидата полностью верен. Дополним техническими деталями.

async/await — это синтаксический сахар над Promise, введённый в ES2017. Он не добавляет новых возможностей, а лишь предоставляет более читаемый способ работы с асинхронным кодом.

Ключевые правила:

  • Функция с async всегда возвращает Promise, даже если внутри нет await
  • await можно использовать только внутри async-функции (или на верхнем уровне модуля — top-level await)
  • await приостанавливает выполнение async-функции, а не всего потока
async function getValue() {
return 42; // автоматически оборачивается в Promise.resolve(42)
}

// Эквивалент:
function getValue() {
return Promise.resolve(42);
}

Обработка ошибок:

// Через .then/.catch
fetchUser(id)
.then(user => fetchOrders(user.id))
.then(orders => console.log(orders))
.catch(err => console.error(err));

// Через async/await — тот же результат
async function getUserOrders(id) {
try {
const user = await fetchUser(id);
const orders = await await fetchOrders(user.id);
return orders;
} catch (err) {
console.error(err);
throw err;
}
}

Типичные ошибки:

1. Параллельное выполнение вместо последовательного:

// Плохо — запросы выполняются последовательно
const user = await fetchUser(id);
const orders = await fetchOrders(id); // ждёт завершения fetchUser

// Хорошо — запросы выполняются параллельно
const [user, orders] = await Promise.all([
fetchUser(id),
fetchOrders(id)
]);

2. Забытый await:

async function processData() {
const data = fetchData(); // вернёт Promise, а не данные!
console.log(data); // Promise { <pending> }

const data = await fetchData(); // вернёт результат
}

3. try/catch не ловит ошибки в отдельном Promise:

async function example() {
try {
fetchData(); // забыли await — ошибка не будет поймана!
} catch (err) {
// сюда не попадём
}
}

Аналогия из Go: async/await в некотором смысле аналогичен тому, как Go работает с горутинами и каналами. В Go вы тоже можете писать код, который выглядит последовательно, но работает асинхронно:

func processData() error {
user, err := fetchUser(id)
if err != nil {
return err
}
orders, err := fetchOrders(user.ID)
if err != nil {
return err
}
return nil
}

Разница в том, что в Go асинхронность явная — вы сами решаете, запускать операцию в горутине или нет. В JavaScript с async/await асинхронность скрыта за синтаксисом, и важно помнить, что await — это точка приостановки, а не просто получение значения.

Вопрос 10. Что такое контекст функции (this), как он работает и чем отличается контекст обычной функции от стрелочной?

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

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

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

Ответ кандидата полностью корректен. Дополним деталями о механизме определения this.

this в обычных функциях

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

Четыре правила определения this:

1. Вызов как метод объектаthis указывает на объект перед точкой:

const obj = {
name: 'Alice',
greet() { return this.name; }
};
obj.greet(); // this === obj

2. Обычный вызов функцииthis зависит от режима:

function show() { console.log(this); }
show(); // undefined (strict mode) или globalThis (non-strict)

3. Вызов через call/apply/bindthis задаётся явно:

function greet() { return this.name; }
greet.call({ name: 'Bob' }); // this === { name: 'Bob' }
greet.apply({ name: 'Bob' }); // this === { name: 'Bob' }

const bound = greet.bind({ name: 'Bob' });
bound(); // this === { name: 'Bob' }

4. Вызов через newthis указывает на новый объект:

function Person(name) {
this.name = name;
}
const p = new Person('Charlie'); // this === новый объект

Приоритет правил: new > call/apply/bind > метод > обычный вызов.

Проблема потерянного контекста:

const obj = {
name: 'Alice',
greet() {
setTimeout(function() {
console.log(this.name); // undefined — this === globalThis
}, 100);
}
};

this в стрелочных функциях

Стрелочные функции используют lexical thisthis захватывается из окружающей области видимости на момент создания функции. Его нельзя изменить через call, apply, bind или new.

const obj = {
name: 'Alice',
greet() {
setTimeout(() => {
console.log(this.name); // 'Alice' — this унаследован из greet()
}, 100);
}
};

Полный список различий стрелочных функций:

СвойствоОбычная функцияСтрелочная функция
thisдинамическийлексический (унаследован)
argumentsсвойнет
newможно вызватьTypeError
prototypeестьнет
superестьнет (наследует из внешней)
yieldможет быть генераторомне может

Аналогия из Go: в Go аналогом this является явный получатель (receiver) метода:

type User struct {
Name string
}

func (u User) Greet() string {
return u.Name // u — это аналог this, но он всегда явный
}

В Go нет проблемы «потерянного контекста», потому что получатель всегда передаётся явно. В JavaScript this — неявный параметр, что делает его мощным, но и источником ошибок. Стрелочные функции — это способ сделать поведение this более предсказуемым, приблизив его к модели лексической области видимости, к которой Go-разработчики привыкли.

Вопрос 11. Как работает цепочка областей видимости (scope chain) и кто является глобальным контекстом в браузере и в Node.js?

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

Ответ собеседова: Правильный. Вложенные функции имеют доступ к переменным внешних функций — контексты стакаются, и мы имеем доступ ко всем родительским контекстам. В браузере глобальный контекст — это window, в Node.js — global (или globalThis). Ответ правильный.

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

Ответ кандидата корректен. Дополним деталями о механизме поиска переменных и различиях между средами.

Цепочка областей видимости (Scope Chain)

Каждая функция при создании получает внутреннее свойство [[Environment]] — ссылку на внешнюю лексическую среду, в которой была создана функция. Это и есть основа цепочки областей видимости.

Механизм поиска переменной:

  1. Поиск начинается в текущей области видимости
  2. Если не найдена — переход во внешнюю область (по [[Environment]])
  3. Повторяется до глобальной области
  4. Если не найдена в глобальной — ReferenceError
const global = 'global';

function outer() {
const outerVar = 'outer';

function inner() {
const innerVar = 'inner';
console.log(innerVar); // 'inner' — найдено в текущей области
console.log(outerVar); // 'outer' — найдено во внешней области
console.log(global); // 'global' — найдено в глобальной области
}

inner();
}

Типы областей видимости:

  • Глобальная — одна на весь скрипт/модуль
  • Функциональная — создаётся при вызове функции (var, function declaration)
  • Блочная — создаётся для let, const, class внутри фигурных скобок {}
  • Модульная — каждый ES-модуль имеет свою область видимости

Глобальный объект в разных средах:

СредаГлобальный объектПримечание
Браузерwindowwindow.window === window
Node.jsglobalНет window, но есть process, Buffer
Web WorkerselfНет DOM, нет window
ES2020+ (универсально)globalThisРаботает везде

Важные различия между браузером и Node.js:

// В браузере
var x = 10;
console.log(window.x); // 10 — var в глобальной области попадает в window

// В Node.js
var x = 10;
console.log(global.x); // undefined — в Node.js переменные модуля НЕ попадают в global

В Node.js каждый файл оборачивается в функцию-модуль, поэтому var на верхнем уровне файла остаётся внутри модуля и не загрязняет global.

Замыкание как следствие scope chain:

Замыкание возникает, когда функция сохраняет ссылку на внешнюю область видимости даже после того, как внешняя функция завершила выполнение:

function createCounter() {
let count = 0; // захвачена замыканием

return {
increment: () => ++count,
getCount: () => count
};
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.getCount(); // 2

Переменная count не уничтожается, потому что возвращённые функции сохраняют [[Environment]], ссылающийся на лексическую среду createCounter.

Аналогия из Go: в Go работает тот же принцип лексической области видимости и замыканий:

func createCounter() func() int {
count := 0
return func() int {
count++
return count
}
}

counter := createCounter()
counter() // 1
counter() // 2

Механизм идентичен: анонимная функция захватывает переменную count из внешней области видимости. Разница в том, что в Go это явно видно по типам функций, а в JavaScript замыкания создаются неявно при каждом объявлении функции внутри другой.

Вопрос 12. В каких случаях this в браузере равен undefined, а не window?

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

Ответ собеседника: Правильный. В строгом режиме ('use strict') this в глобальном контексте равен undefined, а не window. Ответ правильный.

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

Ответ кандидата верен, дополним полным списком случаев.

Основной случай: strict mode

// Non-strict mode
function show() {
console.log(this); // window (браузер)
}
show();

// Strict mode
'use strict';
function show() {
console.log(this); // undefined
}
show();

В non-strict mode при обычном вызове функции this подменяется на глобальный объект (window). В strict mode — остаётся тем, чем был при вызове, то есть undefined.

Все случаи, когда this может быть undefined:

1. Обычный вызов функции в strict mode:

'use strict';
function foo() { return this; }
foo(); // undefined

2. Вызов через call/apply с явным null или undefined:

function foo() { return this; }
foo.call(undefined); // undefined (strict mode) или window (non-strict)
foo.call(null); // null (strict mode) или window (non-strict)

В non-strict mode null и undefined заменяются на глобальный объект. В strict mode — передаются как есть.

3. Стрелочная функция во внешней области strict mode:

'use strict';
const arrow = () => this;
arrow(); // наследует this из внешней области — undefined

4. Модули ES (ESM) — всегда strict mode:

// В .mjs файле или <script type="module">
console.log(this); // undefined — на верхнем уровне модуля this === undefined

ES-модули автоматически работают в strict mode, поэтому this на верхнем уровне модуля — всегда undefined, а не window.

5. Метод, вырванный из контекста:

const obj = {
name: 'Alice',
getName() { return this; }
};

const fn = obj.getName;
fn(); // undefined (strict mode) или window (non-strict)

Сводная таблица:

СитуацияNon-strictStrict
Обычный вызов foo()windowundefined
foo.call(null)windownull
foo.call(undefined)windowundefined
Метод obj.foo()objobj
new Foo()новый объектновый объект
Верхний уровень модуляundefined

Практический вывод: современная разработка (ES-модули, Babel, TypeScript) по умолчанию работает в strict mode, поэтому в реальном коде this при обычном вызове функции почти всегда будет undefined, а не window.

Вопрос 13. Чем отличаются var, let и const, как работают всплытие (hoisting) и области видимости?

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

Ответ собеседника: Правильный. var имеет функциональную область видимости и всплывает (hoisting) — переменная доступна до объявления со значением undefined. let и const имеют блочную область видимости и не всплывают — при обращении до объявления возникает ReferenceError (temporal dead zone). const нельзя переприсвоить, но можно менять содержимое объекта или массива, объявленного через const. var можно переприсваивать и переобъявлять. Ответ правильный, кандидат верно описал все ключевые отличия.

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

Ответ кандидата полностью корректен. Дополним деталями о механизме hoisting и Temporal Dead Zone.

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

Характеристикаvarletconst
Область видимостиФункциональнаяБлочнаяБлочная
Всплытие (hoisting)Да, со значением undefinedДа, но без инициализации (TDZ)Да, но без инициализации (TDZ)
ПереобъявлениеДаНет (SyntaxError)Нет (SyntaxError)
ПереприсваиваниеДаДаНет (TypeError)
Привязка к windowДа (в глобальной области)НетНет

Hoisting — как это работает на самом деле

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

var — всплытие с инициализацией:

console.log(x); // undefined — переменная существует, но не инициализирована
var x = 5;
// Интерпретируется как:
// var x;
// console.log(x);
// x = 5;

let/const — Temporal Dead Zone (TDZ):

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

Переменная зарегистрирована в области видимости, но до строки объявления находится в «мёртвой зоне». Доступ к ней вызывает ReferenceError. TDZ заканчивается в точке объявления.

Пример TDZ с typeof:

// С var:
console.log(typeof undeclaredVar); // 'undefined' — безопасно

// С let:
console.log(typeof tdzVar); // ReferenceError — typeof НЕ защищает от TDZ
let tdzVar = 5;

Блочная область видимости:

for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Выведет: 3, 3, 3 — одна переменная i для всего цикла

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

const и мутация:

const obj = { name: 'Alice' };
obj.name = 'Bob'; // OK — мутация содержимого
obj.age = 30; // OK — добавление свойства
obj = {}; // TypeError — нельзя переприсвоить ссылку

const arr = [1, 2, 3];
arr.push(4); // OK
arr[0] = 0; // OK
arr = []; // TypeError

Аналогия из Go: в Go нет hoisting и TDZ — переменная доступна только после объявления. Поведение let/const ближе к Go-модели:

// Go — аналог TDZ отсутствует, переменная просто не существует до объявления
// fmt.Println(x) — compile error
x := 5
fmt.Println(x) // 5

const в JavaScript ближе к константам в Go с точки зрения неизменяемости привязки (binding), но не значения. В Go константы действительно неизменяемы на уровне значений.

Вопрос 14. Какие виды функций существуют в JavaScript и в чём их главные отличия?

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

Ответ собеседника: Правильный. Существуют function declaration и стрелочные функции (arrow functions). Function declaration всплывает (hoisting) — можно вызвать до объявления. Стрелочные функции не всплывают, не имеют своего this (наследуют из внешнего контекста), не имеют объекта arguments и не могут быть конструкторами. Ответ правильный.

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

Ответ кандидата верен, но он упомянул только два вида. В JavaScript существует больше разновидностей.

Основные виды функций:

1. Function Declaration (объявление функции)

function greet(name) {
return `Hello, ${name}`;
}
  • Всплывает (hoisting) — можно вызвать до объявления в коде
  • Имеет своё this (dynamic binding)
  • Имеет объект arguments
  • Может быть конструктором (через new)
  • Имеет свойство prototype

2. Function Expression (функциональное выражение)

const greet = function(name) {
return `Hello, ${name}`;
};

// Именованное функциональное выражение
const greet = function greetFn(name) {
return `Hello, ${name}`;
};
  • Не всплывает (подчиняется правилам переменной — var/let/const)
  • Имеет своё this
  • Имеет объект arguments
  • Может быть конструктором
  • Именованный вариант удобен для отладки (имя видно в stack trace)

3. Arrow Function (стрелочная функция)

const greet = (name) => `Hello, ${name}`;
  • Не всплывает
  • Нет своего this — наследует лексически
  • Нет arguments — можно использовать rest-параметры (...args)
  • Нет prototype — нельзя вызвать через new
  • Нет super
  • Не может быть генератором
  • Более компактный синтаксис

4. IIFE (Immediately Invoked Function Expression)

(function() {
var privateVar = 'hidden';
// изолированная область видимости
})();

(() => {
// стрелочный IIFE
})();
  • Выполняется сразу после объявления
  • Создаёт изолированную область видимости
  • В современном коде заменён блочной областью {} и модулями

5. Generator Function (функция-генератор)

function* idGenerator() {
let id = 0;
while (true) {
yield ++id;
}
}

const gen = idGenerator();
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
  • Возвращает объект-генератор с методом next()
  • Поддерживает yield для приостановки выполнения
  • Может быть использована с for...of

6. Async Function

async function fetchData() {
const response = await fetch('/api/data');
return response.json();
}
  • Всегда возвращает Promise
  • Поддерживает await
  • Ошибки внутри автоматически оборачиваются в rejected Promise

7. Async Generator Function

async function* fetchPages(url) {
let nextUrl = url;
while (nextUrl) {
const data = await fetch(nextUrl).then(r => r.json());
yield data.results;
nextUrl = data.nextPage;
}
}

for await (const page of fetchPages('/api/items')) {
console.log(page);
}
  • Комбинация генератора и async
  • Поддерживает yield и await
  • Итерируется через for await...of

8. Метод объекта (сокращённая запись)

const obj = {
name: 'Alice',
greet() { // сокращённая запись
return this.name;
}
};
  • Синтаксический сахар для greet: function() {}
  • Ведёт себя как function declaration (имеет this, arguments)

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

СвойствоDeclarationExpressionArrowGeneratorAsync
HoistingДаНетНетНетНет
Свой thisДаДаНетДаДа
argumentsДаДаНетДаДа
prototypeДаДаНетДаДа
newДаДаНетНетНет
yieldНетНетНетДаДа*

*async generator поддерживает yield

Аналогия из Go: в Go есть только один вид объявления функции — func. Нет разделения на declaration/expression/arrow. Замыкания в Go работают аналогично стрелочным функциям — захватывают переменные из внешней области лексически:

func counter() func() int {
count := 0
return func() int {
count++
return count
}
}

Генераторы в Go реализуются через каналы — это более явный и контролируемый подход, чем в JavaScript:

func idGenerator() <-chan int {
ch := make(chan int)
go func() {
for i := 1; ; i++ {
ch <- i
}
}()
return ch
}

Вопрос 15. Как работает механизм Event Loop (событийный цикл) в JavaScript?

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

Ответ собеседника: Правильный. Сначала выполняется весь синхронный код (call stack). Затем выполняются все микрозадачи (microtask queue): промисы (.then/.catch/.finally), queueMicrotask, MutationObserver. Затем выполняется одна макрозадача (macrotask queue): setTimeout, setInterval, браузерные API, обработчики событий, fetch и т.д. После каждой макрозадачи снова выполняются все накопившиеся микрозадачи. Кандидат верно описал последовательность: синхронный код → микрозадачи → макрозадачи, упомянул промисы, MutationObserver, setTimeout, setInterval.

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

Ответ кандидата полностью корректен. Дополним деталями о внутреннем устройстве.

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

JavaScript — однопоточный язык. Event Loop — это механизм, который позволяет выполнять асинхронные операции без блокировки основного потока.

Компоненты:

  • Call Stack — стек вызовов для синхронного кода
  • Microtask Queue — очередь микрозадач (высший приоритет после синхронного кода)
  • Macrotask Queue — очередь макрозадач (низший приоритет)
  • Web APIs — браузерные API (DOM, fetch, setTimeout и др.), выполняются вне JS-потока

Алгоритм Event Loop:

  1. Выполнить весь синхронный код в Call Stack
  2. Когда Call Stack пуст — выполнить все микрозадачи из Microtask Queue
  3. Выполнить одну макрозадачу из Macrotask Queue
  4. Повернуть к шагу 2

Микрозадачи (Microtask Queue):

  • Promise.then(), Promise.catch(), Promise.finally()
  • queueMicrotask()
  • MutationObserver (браузер)
  • process.nextTick() (Node.js — ещё более приоритетный, чем обычные микрозадачи)
  • Object.observe() (deprecated)

Макрозадачи (Macrotask Queue):

  • setTimeout(), setInterval()
  • setImmediate() (Node.js)
  • I/O операции
  • requestAnimationFrame (браузер)
  • Обработчики событий (click, keypress и др.)
  • Рендеринг (браузер)

Пример для понимания порядка:

console.log('1'); // Синхронный

setTimeout(() => {
console.log('2'); // Макрозадача
}, 0);

Promise.resolve().then(() => {
console.log('3'); // Микрозадача
});

queueMicrotask(() => {
console.log('4'); // Микрозадача
});

console.log('5'); // Синхронный

// Вывод: 1, 5, 3, 4, 2

Важный нюанс — микрозадачи могут порождать микрозадачи:

Event Loop не переходит к макрозадачам, пока Microtask Queue не пуста. Если микрозадача порождает новую микрозадачу, она добавляется в очередь и тоже выполняется до макрозадач. Это может привести к «голоданию» (starvation) — макрозадачи не будут выполняться, пока микрозадачи не иссякнут.

Разница между браузером и Node.js:

В Node.js есть дополнительные фазы событийного цикла:

  • timers — setTimeout, setInterval
  • pending callbacks — системные колбэки
  • idle, prepare — внутренние
  • poll — I/O
  • check — setImmediate
  • close callbacks — close события

Аналогия из Go: в Go вместо Event Loop используется планировщик горутин (goroutine scheduler), который распределяет горутины по системным потокам (M:N scheduling). Каналы в Go — это аналог очередей задач, но с более строгой типизацией и контролем:

ch := make(chan string, 1) // буферизированный канал — аналог очереди

go func() {
ch <- "microtask" // отправка в канал
}()

msg := <-ch // получение из канала — блокирует, если пусто

Go-планировщик более предсказуем, потому что горутины не могут «голодать» так, как макрозадачи в JavaScript. Go использует work-stealing алгоритм для равномерного распределения нагрузки.

Вопрос 16. Писал ли тесты и на каком уровне?

Таймкод: 00:31:33

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

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

Ответ кандидата честный и адекватный. Для полноты картины опишем, какие уровни тестирования существуют и как они применяются в профессиональной разработке.

Уровни тестирования:

1. Unit-тесты (модульные)

Тестирование отдельных функций, методов или компонентов в изоляции. Самый быстрый и масштабируемый уровень.

// Jest
describe('calculateTotal', () => {
it('should sum items correctly', () => {
expect(calculateTotal([{ price: 10 }, { price: 20 }])).toBe(30);
});

it('should return 0 for empty cart', () => {
expect(calculateTotal([])).toBe(0);
});
});

2. Integration-тесты (интеграционные)

Тестирование взаимодействия между модулями: компонент + API, сервис + база данных, контроллер + репозиторий.

// Тестирование API-эндпоинта с реальным или тестовым сервером
describe('POST /api/users', () => {
it('should create user and return 201', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Alice', email: 'alice@test.com' });

expect(response.status).toBe(201);
expect(response.body).toHaveProperty('id');
});
});

3. E2E-тесты (end-to-end)

Тестирование всего потока от начала до конца через реальный интерфейс или API.

// Cypress / Playwright
describe('Checkout flow', () => {
it('should complete purchase', () => {
cy.visit('/products');
cy.get('[data-testid="add-to-cart"]').first().click();
cy.visit('/checkout');
cy.get('#email').type('user@test.com');
cy.get('#submit').click();
cy.contains('Order confirmed').should('be.visible');
});
});

4. Снапшот-тесты

Фиксируют вывод компонента и сравнивают с эталоном при каждом запуске.

it('renders correctly', () => {
const tree = renderer.create(<MyComponent />).toJSON();
expect(tree).toMatchSnapshot();
});

Аналогия из Go: в Go тестирование встроено в стандартную библиотеку через пакет testing:

func TestCalculateTotal(t *testing.T) {
items := []Item{{Price: 10}, {Price: 20}}
result := CalculateTotal(items)
if result != 30 {
t.Errorf("expected 30, got %d", result)
}
}

// Бенчмарки — уникальная возможность Go
func BenchmarkCalculateTotal(b *testing.B) {
items := generateItems(1000)
for i := 0; i < b.N; i++ {
CalculateTotal(items)
}
}

В Go также есть table-driven tests — идиоматический подход к тестированию с таблицей входных данных:

func TestAdd(t *testing.T) {
cases := []struct {
a, b, expected int
}{
{1, 2, 3},
{0, 0, 0},
{-1, 1, 0},
}

for _, tc := range cases {
result := Add(tc.a, tc.b)
if result != tc.expected {
t.Errorf("Add(%d, %d) = %d, want %d", tc.a, tc.b, result, tc.expected)
}
}
}

Рекомендация для кандидата: начните с unit-тестов для чистых функций (pure functions) — это самый простой и быстрый способ привыкнуть к тестированию. Для Go-разработчика это будет знакомо, так как культура тестирования в Go-сообществе значительно сильнее, чем в типичных JavaScript-проектах.

Вопрос 17. Какие четыре столпа объектно-ориентированного программирования (ООП) и что они означают?

Таймкод: 00:32:49

Ответ собеседника: Правильный. Четыре принципа ООП: 1) Наследование — класс может наследовать свойства и методы родительского класса и расширять их. 2) Инкапсуляция — скрытие внутреннего состояния объекта, доступ только через методы. 3) Полиморфизм — возможность объектов разных классов использовать один и тот же интерфейс, но с разной реализацией. 4) Абстракция — выделение фундаментальных характеристик объекта через абстрактный класс или интерфейс. Кандидат верно назвал и объяснил все четыре принципа.

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

Ответ кандидата полностью корректен. Дополним примерами на JavaScript и сравнением с Go.

1. Абстракция (Abstraction)

Выделение существенных характеристик объекта и игнорирование несущественных. Определяем «что» объект делает, а не «как».

class PaymentProcessor {
// Абстракция: пользователь знает только метод pay, не знает детали
async pay(amount, currency) {
const validated = this.validate(amount, currency);
const result = await this.charge(validated);
return this.receipt(result);
}

validate(amount, { /* ... */ }) { /* скрытая реализация */ }
charge(data) { /* скрытая реализация */ }
receipt(result) { /* скрытая реализация */ }
}

2. Инкапсуляция (Encapsulation)

Скрытие внутреннего состояния и предоставление контролируемого доступа через публичный интерфейс.

class BankAccount {
#balance = 0; // приватное поле (hard private)

deposit(amount) {
if (amount <= 0) throw new Error('Amount must be positive');
this.#balance += amount;
}

getBalance() {
return this.#balance;
}
}

const account = new BankAccount();
account.deposit(100);
account.getBalance(); // 100
account.#balance; // SyntaxError — доступ запрещён

3. Наследование (Inheritance)

Создание нового класса на основе существующего с переиспользованием и расширением функциональности.

class Animal {
constructor(name) { this.name = name; }
speak() { return `${this.name} makes a sound`; }
}

class Dog extends Animal {
speak() { return `${this.name} barks`; } // переопределение
fetch() { return `${this.name} fetches`; } // расширение
}

const dog = new Dog('Rex');
dog.speak(); // 'Rex barks'
dog.fetch(); // 'Rex fetches'

4. Полиморфизм (Polиморфизм)

Возможность объектов разных типов обрабатываться через единый интерфейс.

class Cat extends Animal {
speak() { return `${this.name} meows`; }
}

class Cow extends Animal {
speak() { return `${this.name} moos`; }
}

const animals = [new Dog('Rex'), new Cat('Whiskers'), new Cow('Bessie')];

// Полиморфизм: один интерфейс speak(), разное поведение
animals.forEach(animal => console.log(animal.speak()));
// Rex barks
// Whiskers meows
// Bessie moos

Как это работает в Go:

Go не имеет классического ООП с наследованием, но реализует все четыре принципа через другие механизмы:

// Абстракция + Инкапсуляция через интерфейсы
type Speaker interface {
Speak() string
}

// Наследование через встраивание (embedding)
type Animal struct {
Name string
}

func (a Animal) Speak() string {
return fmt.Sprintf("%s makes a sound", a.Name)
}

type Dog struct {
Animal // встраивание — аналог наследования
}

func (d Dog) Speak() string {
return fmt.Sprintf("%s barks", d.Name)
}

// Полиморфизм через интерфейсы
func MakeSound(s Speaker) {
fmt.Println(s.Speak())
}

MakeSound(Dog{Animal{Name: "Rex"}}) // Rex barks

В Go нет extends, implements, private, public. Вместо этого: встраивание для наследования, интерфейсы для полиморфизма (реализуются неявно — duck typing), регистр первой буквы для инкапсуляции (заглавная — экспортируемая).

Вопрос 18. Какие паттерны проектирования использовал на практике и на какие группы они делятся?

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

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

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

Ответ кандидата частично верен. Дадим полную классификацию с примерами.

Три группы паттернов (GoF — Gang of Four):

1. Порождающие (Creational Patterns)

Отвечают за механизмы создания объектов, скрывая логику создания.

ПаттернНазначение
SingletonГарантирует единственный экземпляр класса
Factory MethodОпределяет интерфейс создания, подклассы решают какой объект создавать
Abstract FactoryСемейство связанных объектов без указания конкретных классов
BuilderПошаговое построение сложного объекта
PrototypeКлонирование объектов
// Singleton
class Database {
static #instance = null;

static getInstance() {
if (!Database.#instance) {
Database.#instance = new Database();
}
return Database.#instance;
}
}

// Builder
class QueryBuilder {
#query = '';
#params = [];

select(fields) { /* ... */ return this; }
from(table) { /* ... */ return this; }
where(condition) { /* ... */ return this; }
build() { return { query: this.#query, params: this.#params }; }
}

const query = new QueryBuilder()
.select(['id', 'name'])
.from('users')
.where('age > 18')
.build();

2. Структурные (Structural Patterns)

Определяют, как классы и объекты компонуются в более крупные структуры.

ПаттернНазначение
AdapterПреобразует интерфейс одного класса в другой
DecoratorДинамически добавляет поведение объекту
FacadeУпрощённый интерфейс к сложной подсистеме
ProxyЗаместитель объекта, контролирующий доступ к нему
CompositeДревовидная структура объектов
BridgeРазделяет абстракцию и реализацию
// Facade
class OrderFacade {
constructor() {
this.payment = new PaymentService();
this.inventory = new InventoryService();
this.notification = new NotificationService();
}

async placeOrder(order) {
const available = await this.inventory.check(order.items);
if (!available) throw new Error('Out of stock');

await this.payment.charge(order.total);
await this.inventory.reserve(order.items);
await this.notification.sendConfirmation(order);
}
}

// Decorator (через высшие функции)
function withLogging(fn) {
return function(...args) {
console.log(`Calling ${fn.name} with`, args);
const result = fn.apply(this, args);
console.log(`Result:`, result);
return result;
};
}

const loggedFetch = withLogging(fetch);

3. Поведенческие (Behavioral Patterns)

Определяют алгоритмы и распределение ответственности между объектами.

ПаттернНазначение
ObserverЗависимость «один ко многим» при изменении состояния
StrategyСемейство взаимозаменяемых алгоритмов
CommandИнкапсуляция запроса как объекта
IteratorПоследовательный доступ к элементам коллекции
StateИзменение поведения при смене состояния
Chain of ResponsibilityЦепочка обработчиков запроса
// Observer
class EventEmitter {
#listeners = new Map();

on(event, callback) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, []);
}
this.#listeners.get(event).push(callback);
}

emit(event, data) {
const listeners = this.#listeners.get(event) || [];
listeners.forEach(cb => cb(data));
}
}

// Strategy
const paymentStrategies = {
card: (amount) => ({ method: 'card', amount, fee: amount * 0.02 }),
crypto: (amount) => ({ method: 'crypto', amount, fee: amount * 0.005 }),
bankTransfer: (amount) => ({ method: 'bank', amount, fee: 0 }),
};

function processPayment(amount, strategy) {
return paymentStrategies[strategy](amount);
}

Аналогия из Go: многие паттерны GoF в Go реализуются идиоматически, без явных классов:

  • Strategy — через функции первого класса и интерфейсы
  • Observer — через каналы
  • Singleton — через sync.Once
  • Decorator — через обёртки интерфейсов
  • Iterator — через каналы или range
// Strategy в Go
type PaymentStrategy interface {
Pay(amount float64) error
}

type CardPayment struct{}
func (c CardPayment) Pay(amount float64) error { /* ... */ }

type CryptoPayment struct{}
func (c CryptoPayment) Pay(amount float64) error { /* ... */ }

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

В Go паттерны часто выражаются проще благодаря первоклассным функциям, интерфейсам и каналам — трем механизмам, которые заменяют многие объектные паттерны из мира классического ООП.

Вопрос 19. Что такое принципы SOLID и применял ли их на практике?

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

Ответ собеседника: Неполный. Кандидат верно расшифровал и объяснил три принципа SOLID: S (Single Responsibility), O (Open/Closed), L (Liskov Substitution). Остальные два принципа (I — Interface Segregation, D — Dependency Inversion) кандидат не назвал.

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

Дадим полное описание всех пяти принципов SOLID с примерами.

S — Single Responsibility Principle (Принцип единственной ответственности)

Класс должен иметь только одну причину для изменения. Каждый модуль отвечает за одну задачу.

// Нарушение: класс отвечает за валидацию, сохранение и уведомление
class User {
validate() { /* ... */ }
save() { /* ... */ }
sendEmail() { /* ... */ }
}

// Правильно: разделение ответственности
class User { /* данные */ }
class UserValidator { validate(user) { /* ... */ } }
class UserRepository { save(user) { /* ... */ } }
class EmailService { sendWelcome(user) { /* ... */ } }

O — Open/Closed Principle (Принцип открытости/закрытости)

Программные сущности открыты для расширения, но закрыты для модификации.

// Нарушение: добавление нового формата требует изменения функции
function calculateArea(shape) {
if (shape.type === 'circle') return Math.PI * shape.radius ** 2;
if (shape.type === 'square') return shape.side ** 2;
// добавление нового типа = изменение этой функции
}

// Правильно: расширяем через полиморфизм
class Circle {
constructor(radius) { this.radius = radius; }
area() { return Math.PI * this.radius ** 2; }
}

class Square {
constructor(side) { this.side = side; }
area() { return this.side ** 2; }
}

function calculateArea(shape) {
return shape.area(); // не меняется при добавлении новых фигур
}

L — Liskov Substitution Principle (Принцип подстановки Лисков)

Объекты подтипов должны быть заменяемы объектами базового типа без нарушения корректности программы.

// Нарушение: квадрат «ломает» поведение прямоугольника
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
setWidth(w) { this.width = w; }
setHeight(h) { this.height = h; }
area() { return this.width * this.height; }
}

class Square extends Rectangle {
setWidth(w) { this.width = w; this.height = w; } // нарушение!
setHeight(h) { this.width = h; this.height = h; } // нарушение!
}

// Клиентский код ожидает, что width и height независимы
function testRectangle(rect) {
rect.setWidth(5);
rect.setHeight(4);
console.log(rect.area()); // 20 для Rectangle, 16 для Square — ошибка!
}

I — Interface Segregation Principle (Принцип разделения интерфейса)

Клиенты не должны зависеть от интерфейсов, которые они не используют. Лучше много маленьких интерфейсов, чем один большой.

// Нарушение: один огромный интерфейс
interface Worker {
work();
eat();
sleep();
}

// Робот вынужден реализовать eat() и sleep(), хотя они ему не нужны

// Правильно: разделённые интерфейсы
interface Workable { work(); }
interface Eatable { eat(); }
interface Sleepable { sleep(); }

class Human implements Workable, Eatable, sleepable {
work() { /* ... */ }
eat() { /* ... */ }
sleep() { /* ... */ }
}

class Robot implements Workable {
work() { /* ... */ }
// не нуждается в eat() и sleep()
}

D — Dependency Inversion Principle (Принцип инверсии зависимостей)

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

// Нарушение: сервис напрямую зависит от конкретной реализации
class MySQLDatabase {
query(sql) { /* ... */ }
}

class UserService {
constructor() {
this.db = new MySQLDatabase(); // жёсткая привязка к MySQL
}
}

// Правильно: зависимость от абстракции
class UserService {
constructor(database) { // dependency injection
this.db = database;
}
}

// Можно подставить любую реализацию
const service = new UserService(new MySQLDatabase());
const testService = new UserService(new InMemoryDatabase());

Аналогия из Go: в Go принципы SOLID реализуются естественно благодаря интерфейсам:

// Interface Segregation + Dependency Inversion
type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}

// Зависимость от абстракций, а не от конкретных типов
func ProcessData(r Reader, w Writer) error {
buf := make([]byte, 1024)
n, err := r.Read(buf)
if err != nil {
return err
}
_, err = w.Write(buf[:n])
return err
}

В Go интерфейсы реализуются неявно (duck typing), что делает Dependency Inversion и Interface Segregation практически бесплатными — не нужно явно указывать implements.

Вопрос 20. Как относишься к CSS/вёрстке, какие технологии стилизации используешь и есть ли предпочтения по методологии именования классов?

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

Ответ собеседова: Правильный. Кандидат оценивает свой уровень владения CSS на 4 из 5. Использует SCSS и SCSS-модули. Особо над названиями классов не заморачивается — подстраивается под стиль, принятый на проекте. Ответ честный и развёрнутый.

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

Ответ кандидата адекватный и честный. Дополним обзором основных технологий стилизации и методологий.

Технологии стилизации:

1. CSS Modules

Локальная область видимости для CSS-классов. Имена классов автоматически хэшируются, что исключает конфликты.

/* Button.module.css */
.button { background: blue; }
.primary { background: green; }
import styles from './Button.module.css';
<button className={styles.primary}>Click</button>

2. SCSS/SASS

Препроцессор с переменными, вложенностью, миксинами и функциями.

$primary-color: #3498db;
$spacing: 8px;

.button {
background: $primary-color;
padding: $spacing * 2;

&:hover {
background: darken($primary-color, 10%);
}

&--primary {
background: green;
}
}

3. Styled Components (CSS-in-JS)

Стили пишутся внутри JavaScript-файлов через tagged template literals.

import styled from 'styled-components';

const Button = styled.button`
background: ${props => props.primary ? 'green' : 'blue'};
padding: 8px 16px;

&:hover {
opacity: 0.8;
}
`;

4. Tailwind CSS

Utility-first CSS-фреймворк. Стили применяются через классы прямо в HTML/JSX.

<button className="bg-blue-500 hover:bg-blue-700 text-white py-2 px-4 rounded">
Click
</button>

5. CSS Custom Properties (CSS Variables)

Нативные переменные в CSS, поддерживают наследование и изменение через JavaScript.

:root {
--primary-color: #3498db;
--spacing: 8px;
}

.button {
background: var(--primary-color);
padding: var(--spacing);
}

Методологии именования:

BEM (Block Element Modifier)

Самая распространённая методология. Структура: block__element--modifier.

/* Block */
.card { }

/* Element */
.card__title { }
.card__body { }

/* Modifier */
.card--featured { }
.card__title--large { }

Принципы:

  • Block — самодостаточный компонент
  • Element — часть блока, не имеет смысла отдельно
  • Modifier — вариация блока или элемента

Другие подходы:

  • SMACSS — классификация правил на базовые, модульные, состояния и темы
  • OOCSS — разделение структуры и оформления
  • Atomic CSS — каждый класс делает одну вещь (Tailwind следует этому подходу)

Аналогия из Go: в Go нет проблемы глобального пространства имён стилей, но есть аналогичная проблема — организация пакетов. Принцип тот же: избегать глобального загрязнения, использовать модульность, следовать единому стилю именования (camelCase для неэкспортируемых, PascalCase для экспортируемых сущностей). Подход CSS Modules ближе всего к модели пакетов в Go — каждый модуль имеет свою изолированную область видимости.

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

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

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

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

Ответ кандидата корректен. Дополним деталями о механизме diffing и внутренней работе React.

Virtual DOM — это JavaScript-объект, представляющий дерево DOM-узлов. Это лёгкая копия реального DOM, с которой можно работать быстро, без обращения к браузерному рендерингу.

Зачем это нужно:

Реальные операции с DOM дорогой. Каждое изменение DOM может вызвать:

  • Recalculate Style — пересчёт стилей
  • Layout — пересчёт позиций элементов
  • Paint — отрисовка пикселей
  • Composite — наложение слоёв

Virtual DOM минимизирует количество обращений к реальному DOM, собирая изменения и применяя их пачками.

Жизненный цикл обновления в React:

  1. State change — изменение состояния компонента
  2. Re-render — React вызывает функцию компонента, получая новый JSX
  3. Reconciliation — React сравнивает новое Virtual DOM-дерево с предыдущим (diffing)
  4. Commit — React применяет только необходимые изменения к реальному DOM

Алгоритм Diffing:

React использует эвристический алгоритм сравнения с двумя ключевыми допущениями:

  • Элементы разных типоров дают разные деревья
  • Ключ (key) указывает, какие элементы стабильны между рендерами
// Без key — React пересоздаёт все элементы при изменении порядка
{items.map(item => <li>{item.name}</li>)}

// С key — React понимает, какой элемент переместился
{items.map(item => <li key={item.id}>{item.name}</li>)}

Правила сравнения:

  • Одинаковый тип элемента — React обновляет атрибуты, не пересоздаёт узел
  • Разный тип элемента — React полностью размонтирует старое дерево и создаст новое
  • Списки с key — React использует key для сопоставления элементов между рендерами
// Было
<div className="old">Text</div>

// Стало — React обновит только className
<div className="new">Text</div>

// Было
<span>Text</span>

// Стало — React уничтожит span и создаст div
<div>Text</div>

Fiber Architecture (React 16+):

React Fiber — это переписанный алгоритм согласования, который позволяет:

  • Разбивать рендеринг на чанки (chunking)
  • Приоритизировать обновления (concurrent features)
  • Приостанавливать и возобновлять работу
  • Откатываться от незавершённой работы

Аналогия из Go: Virtual DOM можно сравнить с буфером записи (write buffer). Вместо того чтобы записывать каждое изменение на диск отдельно, вы накапливаете изменения в памяти и записываете пачкой. React накапливает изменения в Virtual DOM и «записывает» их в реальный DOM одним батчем, минимизируя дорогие операции.

Важно: современные фреймворки уходят от Virtual DOM. Svelte компилирует компоненты в императивный DOM-код. Solid использует реактивность без Virtual DOM. Vue 3 использует компилятор с оптимизациями, который сокращает необходимость в полном diffing.

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

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

Ответ собеседника: Неполный. Кандидат упомянул RTK (Redux Toolkit) и MobX. Верно объяснил назначение: хранение данных без привязки к компонентам, глобальное хранилище. Однако не упомянул другие популярные решения.

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

Зачем нужны менеджеры состояния:

По мере роста приложения локальное состояние компонентов (useState) перестаёт справляться с задачами:

  • Общие данные между далёкими компонентами (prop drilling)
  • Кэширование серверных данных
  • Синхронизация состояния между вкладками
  • Предсказуемое управление сложной бизнес-логикой

Основные менеджеры состояния:

1. Redux Toolkit (RTK)

Современный стандарт для Redux. Устраняет бойлерплейт оригинального Redux.

import { createSlice, configureStore } from '@reduxjs/toolkit';

const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1 },
decrement: (state) => { state.value -= 1 },
incrementByAmount: (state, action) => { state.value += action.payload },
},
});

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

Принципы: единственный store, неизменяемость состояния, чистые редьюсеры, действия как события.

2. Zustand

Минимальный менеджер состояния. Нет бойлерплейта, нет контекста, нет провайдеров.

import { create } from 'zustand';

const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
reset: () => set({ count: 0 }),
}));

function Counter() {
const { count, increment } = useStore();
return <button onClick={increment}>{count}</button>;
}

3. MobX

Реактивное управление состоянием через наблюдаемые объекты.

import { makeAutoObservable } from 'mobx';

class CounterStore {
count = 0;

constructor() {
makeAutoObservable(this);
}

increment() {
this.count += 1;
}
}

4. Recoil

Атомарное состояние от Facebook. Состояние разбито на атомы.

import { atom, useRecoilState } from 'recoil';

const counterState = atom({
key: 'counter',
default: 0,
});

function Counter() {
const [count, setCount] = useRecoilState(counterState);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

5. Jotai

Минималистичный атомарный менеджер, похож на Recoil, но проще.

import { atom, useAtom } from 'jotai';

const countAtom = atom(0);

function Counter() {
const [count, setCount] = useAtom(countAtom);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

6. React Context API

Встроенное решение, не требует библиотек. Подходит для простых случаев.

const ThemeContext = createContext();

function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Component />
</ThemeContext.Provider>
);
}

Сравнение:

РешениеБойлерплейтРазмерКривая обученияЛучше для
Redux ToolkitСредний~11kBКрутаяСложная бизнес-логика
ZustandМинимальный~1kBПологаяУниверсально
MobXНизкий~16kBСредняяРеактивные приложения
RecoilНизкий~20kBСредняяАтомарное состояние
JotaiМинимальный~3kBПологаяАтомарное состояние
Context APIНизкий0ПологаяПростые случаи

Аналогия из Go: менеджер состояния — это аналог глобального хранилища или синглтон-сервиса в Go-приложениях. В Go для этого часто используют паттерн «сервис» с зависимостями, передаваемыми через конструктор (dependency injection):

type AppState struct {
User *User
Theme string
mu sync.RWMutex
}

func (s *AppState) SetTheme(theme string) {
s.mu.Lock()
defer s.mu.Unlock()
s.Theme = theme
}

В Go состояние управляется явно через мьютексы и каналы. В JavaScript — через иммутабельность и паттерны вроде Redux или реактивность вроде MobX.

Вопрос 23. Как организовать архитектуру для задачи: кнопка, при клике на которую загружаются данные с бэкенда (список книг) и отображаются на странице?

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

Ответ собеседова: Правильный. Кандидат предложил разумный подход: для простого случая — локальный стейт (useState), при клике вызывается функция запроса, данные сохраняются в стейт и отображаются. Для более сложного случая предложил разделение: кнопка как переиспользуемый компонент, логика загрузки данных отдельно, отображение отдельно. Также упомянул менеджеры состояний для кэширования запросов и идею создания универсального компонента списка с возможностью передачи типа данных и компонента для рендера. Дополнительно предложил виртуализацию для больших списков и бесконечную прокрутку.

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

Ответ кандидата очень хороший. Дополним конкретной архитектурой с кодом.

Простой случай — локальный стейт:

function BookListPage() {
const [books, setBooks] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

const loadBooks = async () => {
setLoading(true);
setError(null);
try {
const data = await fetch('/api/books').then(r => r.json());
setBooks(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};

return (
<div>
<button onClick={loadBooks} disabled={loading}>
{loading ? 'Загрузка...' : 'Загрузить книги'}
</button>
{error && <p className="error">{error}</p>}
<ul>
{books.map(book => (
<li key={book.id}>{book.title}</li>
))}
</ul>
</div>
);
}

Средний случай — кастомный хук для логики загрузки:

// hooks/useBooks.js
function useBooks() {
const [books, setBooks] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

const loadBooks = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await fetch('/api/books').then(r => r.json());
setBooks(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, []);

return { books, loading, error, loadBooks };
}

// components/BookListPage.jsx
function BookListPage() {
const { books, loading, error, loadBooks } = useBooks();

return (
<div>
<button onClick={loadBooks} disabled={loading}>
{loading ? 'Загрузка...' : 'Загрузить книги'}
</button>
{error && <p className="error">{error}</error>}
<BookList books={books} />
</div>
);
}

function BookList({ books }) {
return (
<ul>
{books.map(book => (
<BookItem key={book.id} book={book} />
))}
</ul>
);
}

Продвинутый случай — React Query (TanStack Query) для кэширования:

import { useQuery } from '@tanstack/react-query';

const fetchBooks = async () => {
const response = await fetch('/api/books');
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
};

function BookListPage() {
const { data: books = [], isLoading, error, refetch } = useQuery({
queryKey: ['books'],
queryFn: fetchBooks,
enabled: false, // не загружать автоматически
});

return (
<div>
<button onClick={() => refetch()} disabled={isLoading}>
{isLoading ? 'Загрузка...' : 'Загрузить книги'}
</button>
{error && <p className="error">{error.message}</p>}
<BookList books={books} />
</div>
);
}

React Query автоматически предоставляет: кэширование, повторные запросы при ошибке, фоновое обновление, дедупликацию запросов, инвалидацию кэша.

Универсальный компонент списка с виртуализацией:

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items, renderItem, itemHeight = 40 }) {
const parentRef = useRef(null);

const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => itemHeight,
});

return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map(virtualItem => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
transform: `translateY(${virtualItem.start}px)`,
height: virtualItem.size,
}}
>
{renderItem(items[virtualItem.index])}
</div>
))}
</div>
</div>
);
}

Аналогия из Go: разделение логики запроса и отображения аналогично разделению на слои в Go-приложениях:

// service/book_service.go — бизнес-логика
type BookService struct {
repo BookRepository
}

func (s *BookService) GetBooks(ctx context.Context) ([]Book, error) {
return s.repo.FindAll(ctx)
}

// handler/book_handler.go — HTTP слой
func (h *BookHandler) GetBooks(w http.ResponseWriter, r *http.Request) {
books, err := h.service.GetBooks(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(books)
}

Каждый слой отвечает за одно: сервис за бизнес-логику, хендлер за HTTP, репозиторий за хранение. В React кастомные хуки играют роль сервисного слоя, компоненты — роль представления.