РЕАЛЬНОЕ СОБЕСЕДОВАНИЕ НА MIDDLE FRONTEND РАЗРАБОТЧИКА | JAVASCRIPT REACT #frontend #собеседование
Сегодня мы разберём техническое собеседование с кандидатом на позицию фронтенд-разработчика, в ходе которого проверялись знания 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
Означает, что переменная объявлена, но значение не присвоено. Это не то же самое, что null — undefined указывает на отсутствие инициализации.
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 == undefined | true |
null === undefined | false |
null == null | true |
undefined == undefined | true |
Как работает == (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 == undefined — true. Они не приводятся к числу, это прямое правило спецификации. И они не равны ничему другому.
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)— сразу возвращает успешный PromisePromise.reject(error)— сразу возвращает отклонённый PromisePromise.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/bind — this задаётся явно:
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. Вызов через new — this указывает на новый объект:
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 this — this захватывается из окружающей области видимости на момент создания функции. Его нельзя изменить через 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]] — ссылку на внешнюю лексическую среду, в которой была создана функция. Это и есть основа цепочки областей видимости.
Механизм поиска переменной:
- Поиск начинается в текущей области видимости
- Если не найдена — переход во внешнюю область (по
[[Environment]]) - Повторяется до глобальной области
- Если не найдена в глобальной —
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-модуль имеет свою область видимости
Глобальный объект в разных средах:
| Среда | Глобальный объект | Примечание |
|---|---|---|
| Браузер | window | window.window === window |
| Node.js | global | Нет window, но есть process, Buffer |
| Web Worker | self | Нет 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-strict | Strict |
|---|---|---|
Обычный вызов foo() | window | undefined |
foo.call(null) | window | null |
foo.call(undefined) | window | undefined |
Метод obj.foo() | obj | obj |
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.
Сравнительная таблица:
| Характеристика | var | let | const |
|---|---|---|---|
| Область видимости | Функциональная | Блочная | Блочная |
| Всплытие (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)
Сравнительная таблица:
| Свойство | Declaration | Expression | Arrow | Generator | Async |
|---|---|---|---|---|---|
| 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:
- Выполнить весь синхронный код в Call Stack
- Когда Call Stack пуст — выполнить все микрозадачи из Microtask Queue
- Выполнить одну макрозадачу из Macrotask Queue
- Повернуть к шагу 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, setIntervalpending callbacks— системные колбэкиidle, prepare— внутренниеpoll— I/Ocheck— setImmediateclose 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:
- State change — изменение состояния компонента
- Re-render — React вызывает функцию компонента, получая новый JSX
- Reconciliation — React сравнивает новое Virtual DOM-дерево с предыдущим (diffing)
- 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 кастомные хуки играют роль сервисного слоя, компоненты — роль представления.
