100 популярных вопросов по Node.js

ВопросОтвет
Что такое SOLID и как эти принципы применяются в разработке на Node.js?SOLID — это набор из пяти принципов объектно‑ориентированного проектирования, направленных на создание гибкой, расширяемой и поддерживаемой архитектуры кода.

**S — Single Responsibility Principle (Принцип единственной ответственности)**
Класс (или модуль) должен иметь только одну причину для изменения, т.е. отвечать за одну бизнес‑функцию. В Node.js это достигается разделением логики по отдельным файлам/модулям: контроллеры обрабатывают запросы, сервисы — бизнес‑логику, репозитории — доступ к данным.

**O — Open/Closed Principle (Принцип открытости/закрытости)**
Программные сущности должны быть открыты для расширения, но закрыты для изменения. В JavaScript это реализуется через наследование (class extends) или композицию, а также через паттерн Strategy, позволяющий добавлять новые поведения без изменения существующего кода.

**L — Liskov Substitution Principle (Принцип подстановки Барбары Лисков)**
Объекты подклассов должны быть взаимозаменяемы с объектами базового класса без нарушения корректности программы. В Node.js это значит, что если вы заменяете, например, `DatabaseClient` на `MockDatabaseClient` в тестах, все методы должны вести себя одинаково согласно контракту.

**I — Interface Segregation Principle (Принцип разделения интерфейсов)**
Клиенты не должны зависеть от методов, которые они не используют. В JavaScript вместо «толстых» интерфейсов (объектов с множеством методов) лучше создавать несколько специализированных объектов или использовать TypeScript‑интерфейсы, каждый из которых описывает только нужный набор функций.

**D — Dependency Inversion Principle (Принцип инверсии зависимостей)**
Модули верхнего уровня не должны зависеть от модулей нижнего уровня; обе группы должны зависеть от абстракций. В Node.js это достигается через внедрение зависимостей (Dependency Injection) — передавая нужные сервисы в конструкторы или функции, а не импортируя их напрямую. Инверсия также реализуется через контейнеры IoC (например, `inversify`).

**Плюсы применения SOLID в Node.js**
- Улучшенная тестируемость: каждый модуль имеет небольшую зону ответственности и легко заменяется моками.
- Повышенная гибкость: новые требования внедряются через расширение, а не изменение существующего кода.
- Снижение связанности: зависимости явно объявляются, что упрощает рефакторинг.
- Лучшее понимание кода: каждый файл/класс имеет четкую цель, что ускоряет onboarding новых разработчиков.

**Пример** (TypeScript, но аналогично в чистом JS):

```ts
// 1. Single Responsibility
class UserRepository {
constructor(private db: DbClient) {}
async findById(id: string) { return this.db.query('SELECT * FROM users WHERE id = ?', [id]); }
}

// 2. Open/Closed via Strategy
interface NotificationStrategy {
send(to: string, message: string): Promise<void>;
}
class EmailNotification implements NotificationStrategy { /* ... */ }
class SmsNotification implements NotificationStrategy { /* ... */ }

class NotificationService {
constructor(private strategy: NotificationStrategy) {}
async notify(to: string, msg: string) { await this.strategy.send(to, msg); }
}

// 3. Liskov Substitution
class MockDbClient implements DbClient { async query() { return []; } } // можно подменить в тестах

// 4. Interface Segregation
interface Readable { read(): Promise<string>; }
interface Writable { write(data: string): Promise<void>; }
class FileReader implements Readable { /* ... */ }
class FileWriter implements Writable { /* ... */ }

// 5. Dependency Inversion
const db = new RealDbClient();
const userRepo = new UserRepository(db);
```

Эти принципы позволяют поддерживать чистую и масштабируемую кодовую базу в проектах на Node.js.
Что подразумевается под внедрением зависимостей (Dependency Injection)?Внедрение зависимостей (DI) — это паттерн проектирования, при котором объект получает свои зависимости (другие объекты, сервисы, конфигурацию) извне, а не создаёт их самостоятельно. Цель — разделить создание и использование компонентов, что повышает модульность, облегчает тестирование и упрощает замену реализаций. Существует несколько способов внедрения: через конструктор (constructor injection), через сеттер/свойство (setter injection) и через интерфейс (interface injection). В Node.js DI часто реализуется вручную: передавая зависимости в функции/классы, либо с помощью контейнеров‑инверторов, таких как InversifyJS, Awilix, Tsyringe. Пример с InversifyJS:

```ts
import 'reflect-metadata';
import { Container, injectable, inject } from 'inversify';

const TYPES = { Logger: Symbol.for('Logger'), Service: Symbol.for('Service') };

@injectable()
class ConsoleLogger {
log(msg: string) { console.log(msg); }
}

@injectable()
class UserService {
constructor(@inject(TYPES.Logger) private logger: ConsoleLogger) {}
getUser(id: number) {
this.logger.log(`Fetching user ${id}`);
return { id, name: 'John' };
}
}

const container = new Container();
container.bind<ConsoleLogger>(TYPES.Logger).to(ConsoleLogger);
container.bind<UserService>(TYPES.Service).to(UserService);

const userService = container.get<UserService>(TYPES.Service);
userService.getUser(42);
```

Плюсы DI: снижение связности, упрощённые unit‑тесты (можно подменить зависимости моками), гибкость конфигурации. Минусы: увеличение сложности конфигурации, необходимость дополнительного инструментария, возможные проблемы с производительностью при больших контейнерах. В небольших проектах часто достаточно простого ручного DI, а в крупных системах предпочтительнее использовать готовый контейнер.
Что представляет собой Middleware?Middleware — это функции, которые находятся в цепочке обработки HTTP‑запроса в серверных приложениях Node.js (например, в Express, Koa, Nest). Они принимают объект `req`, объект `res` и функцию `next`, могут выполнять любые операции: модификацию запроса/ответа, выполнение асинхронных действий, проверку прав доступа, логирование, парсинг тела, обработку ошибок и т.д. При вызове `next()` управление передаётся следующему middleware в очереди; если `next` не вызывается, цепочка прерывается и запрос считается обработанным. Middleware могут быть глобальными (применяются ко всем маршрутам), роут‑специфичными (привязываются к конкретному пути) или ошибочными (принимают четыре аргумента `err, req, res, next`). В Express порядок их подключения определяет порядок выполнения. Примеры: `app.use(express.json())` — парсит JSON‑тело; `app.use(authMiddleware)` — проверяет токен; `app.use((err, req, res, next) => { res.status(500).send(err.message); })` — глобальная обработка ошибок. Middleware позволяют разделять логику, переиспользовать код и строить гибкую архитектуру серверного приложения.
Что означает термин «идемпотентность»?Идемпотентность — это свойство операции возвращать одинаковый результат и оставлять систему в неизменном состоянии при повторном выполнении с одинаковыми входными данными. Формально, функция f считается идемпотентной, если ∀x : f(f(x)) = f(x). В программировании это означает, что многократный вызов одного и того же API‑метода, функции или команды не приводит к дополнительным побочным эффектам после первого успешного выполнения. Примеры в контексте Node.js и веб‑разработки:
- HTTP‑методы GET, HEAD, OPTIONS и DELETE обычно идемпотентны: независимо от количества запросов сервер возвращает одинаковый ресурс или подтверждает удаление без создания новых записей.
- PUT считается идемпотентным, потому что повторная отправка того же представления ресурса заменяет его тем же самым состоянием, не создавая новых версий.
- POST обычно не идемпотентен, так как каждый запрос может создавать новый объект.
Идемпотентность важна для надёжных распределённых систем: при сбоях сети клиент может повторно отправить запрос, не опасаясь дублирования транзакций. В базах данных идемпотентные запросы часто реализуются через «upsert» (INSERT ON CONFLICT DO UPDATE) или проверку существования записи перед вставкой. В Node.js идемпотентность достигается через:
1. Идентификацию запросов (idempotency‑key) и хранение их результатов в кэше/базе.
2. Использование атомарных операций (например, findOneAndUpdate с опцией upsert).
3. Явную проверку текущего состояния ресурса перед модификацией.
Недостатки: необходимость дополнительного хранилища для ключей, возможные гонки при высокой конкуренции, сложность в реализации для сложных бизнес‑процессов.
Идемпотентность часто комбинируется с безопасностью (safe methods) и согласованностью данных, обеспечивая предсказуемое поведение при повторных вызовах и облегчая построение устойчивых API.
Что подразумевается под принципом REST?REST (Representational State Transfer) — это архитектурный стиль распределённых гипермедийных систем, в котором взаимодействие происходит через стандартные операции HTTP (GET, POST, PUT, PATCH, DELETE и др.) над ресурсами, идентифицируемыми URI; ключевые ограничения включают клиент‑серверную модель, безсостояние (stateless), кэшируемость, единообразный интерфейс (идентификация ресурсов, манипуляция их представлениями, самодокументирующие сообщения и гипермедиа как движок состояния), возможность многослойной архитектуры и необязательное выполнение кода на стороне клиента (code‑on‑demand), что обеспечивает масштабируемость, простоту поддержки и независимость от конкретных технологий, делая REST популярным подходом для построения веб‑API.
Какие типы соединений (JOIN) существуют в SQL?В SQL выделяют следующие виды JOIN: **INNER JOIN** – возвращает строки, совпадающие в обеих таблицах (SELECT … FROM A INNER JOIN B ON A.id = B.a_id); **LEFT (OUTER) JOIN** – возвращает все строки из левой таблицы и совпадающие из правой, недостающие заполняются NULL (SELECT … FROM A LEFT JOIN B ON …); **RIGHT (OUTER) JOIN** – аналогично, но сохраняет все строки из правой таблицы; **FULL (OUTER) JOIN** – объединяет результаты LEFT и RIGHT, возвращая все строки из обеих таблиц с NULL там, где нет совпадения; **CROSS JOIN** – декартово произведение всех строк двух таблиц (SELECT … FROM A CROSS JOIN B); **NATURAL JOIN** – автоматически соединяет таблицы по столбцам с одинаковыми именами, эквивалентно INNER JOIN ON одинаковым столбцам; **SELF JOIN** – соединение таблицы самой с собой, часто используется с алиасами (SELECT … FROM employees e1 JOIN employees e2 ON e1.manager_id = e2.id); **JOIN USING** – сокращённый синтаксис, указывающий общие столбцы (SELECT … FROM A JOIN B USING (col)); в Node.js для выполнения таких запросов часто используют библиотеки‑обёртки: **knex** (knex('A').join('B', 'A.id', 'B.a_id')…), **Sequelize** (ModelA.belongsTo(ModelB, {foreignKey: 'a_id'}); ModelA.findAll({include: ModelB})…), **TypeORM** (createQueryBuilder('a').innerJoin('a.b', 'b')…). Каждый тип JOIN выбирается в зависимости от требуемой семантики выборки и наличия/отсутствия совпадающих записей.
Какие способы реализации асинхронности существуют в Node.js?В Node.js асинхронность реализуется с помощью:
- **Callback‑функций** – классический способ, где функция‑обработчик вызывается после завершения операции;
- **Event Loop и события** – низкоуровневая модель, где события помещаются в очередь и обрабатываются циклом событий;
- **Модуль `events`** – позволяет создавать и подписываться на пользовательские события;
- **`setTimeout`, `setInterval`, `setImmediate`, `process.nextTick`** – планируют выполнение кода в будущих тактах цикла событий;
- **Promise** – объект, представляющий результат асинхронной операции, поддерживает методы `then`, `catch`, `finally`;
- **`async/await`** – синтаксический сахар над Promise, позволяющий писать асинхронный код в стиле синхронного;
- **Streams** – позволяют обрабатывать данные по частям (чтение/запись) без блокировки;
- **Generator‑функции + `co`/`await‑generator`** – альтернативный подход к управлению асинхронными потоками;
- **Worker Threads** – отдельные потоки выполнения для CPU‑интенсивных задач, взаимодействующие через сообщения;
- **`child_process` и `cluster`** – запуск внешних процессов или форкинг нескольких экземпляров Node для распределения нагрузки;
- **Библиотеки и фреймворки** (например, `rxjs` для реактивного программирования, `bluebird` для расширенных Promise‑фич).
Каждый из методов имеет свои преимущества и ограничения, и часто они комбинируются для построения эффективных и масштабируемых приложений.
Что такое npm и для чего он используется?npm (Node Package Manager) — это официальный пакетный менеджер для платформы Node.js, предоставляющий доступ к онлайн‑реестру (npm registry) более миллиона пакетов JavaScript. С его помощью разработчики могут искать, устанавливать, обновлять и удалять зависимости проекта, фиксировать их версии в файле package.json, управлять скриптами (npm run), публиковать собственные модули, а также использовать такие команды, как npm install, npm uninstall, npm update, npm ci, npm audit и npm link. npm упрощает совместную работу, обеспечивает воспроизводимость сборок, поддерживает семантическое версионирование, позволяет управлять локальными и глобальными пакетами, а также интегрируется с CI/CD‑конвейерами и инструментами типа npx для одноразового выполнения пакетов.
Что подразумевается под объектно‑ориентированным программированием (ООП)?Объектно‑ориентированное программирование (ООП) — парадигма разработки программ, в основе которой лежит представление программы как совокупности взаимодействующих объектов. Объект объединяет данные (поля, свойства) и поведение (методы), что позволяет инкапсулировать состояние и логику. Ключевые принципы ООП: **инкапсуляция** (скрытие внутренней реализации и предоставление публичного интерфейса), **наследование** (создание новых классов на основе существующих, переиспользование кода), **полиморфизм** (возможность объектов разных классов обрабатывать одинаковые сообщения по‑разному) и **абстракция** (выделение существенных характеристик и игнорирование деталей). В JavaScript (Node.js) ООП реализуется через прототипное наследование, классы (syntactic sugar), конструкторы и функции‑фабрики. Применение ООП упрощает поддержку, расширяемость и тестирование кода, позволяет моделировать реальный мир более естественно, разбивая систему на независимые, переиспользуемые компоненты.
В чём различаются операции LEFT JOIN, RIGHT JOIN и INNER JOIN?INNER JOIN возвращает только те строки, у которых совпадают значения в обеих объединяемых таблицах; LEFT JOIN сохраняет все строки из левой (первой) таблицы и добавляет к ним совпадающие строки из правой, заполняя отсутствующие значения NULL; RIGHT JOIN — зеркальное LEFT JOIN, сохраняет все строки из правой (второй) таблицы, а недостающие из левой заполняет NULL. При использовании LEFT или RIGHT JOIN можно получить «неполные» строки, тогда как INNER JOIN исключает их полностью. Порядок таблиц важен только для LEFT/RIGHT JOIN (LEFT JOIN A ON … RIGHT JOIN B ON …) — меняя его, меняется, какая часть будет полностью сохранена. Пример:
```sql
SELECT a.id, b.val
FROM a
LEFT JOIN b ON a.id = b.a_id; -- все a, даже без b
SELECT a.id, b.val
FROM a
RIGHT JOIN b ON a.id = b.a_id; -- все b, даже без a
SELECT a.id, b.val
FROM a
INNER JOIN b ON a.id = b.a_id; -- только совпадающие пары
```
Для больших наборов данных INNER JOIN обычно быстрее, так как не требует добавления строк с NULL, но выбор зависит от индексов и фильтров. В некоторых СУБД RIGHT JOIN реже используется, так как его можно заменить LEFT JOIN, поменяв местами таблицы.
Что такое замыкание в JavaScript?Замыкание — это функция, которая сохраняет доступ к переменным своей внешней (лексической) области видимости даже после того, как внешняя функция завершила выполнение. При создании функции JavaScript фиксирует её «лексическое окружение»: ссылки на все переменные, объявленные в окружающих областях (в том числе параметры и переменные из внешних функций). Это позволяет функции‑замыканию использовать и изменять эти переменные позже, независимо от того, где и когда она будет вызвана.

Основные свойства и применения замыканий в Node.js:
- **Сохранение состояния**: позволяют имитировать приватные переменные и создавать функции‑фабрики, где каждый вызов возвращает новую функцию с собственным набором захваченных данных.
- **Модульный паттерн**: классический способ инкапсуляции приватных методов/данных без использования `class`/`private`.
- **Асинхронный код**: в колбэках, промисах и `async/await` замыкания часто удерживают контекст (например, ссылки на таймеры, запросы к базе, параметры запроса).
- **Функциональное программирование**: карринг, частичное применение и композиция функций опираются на замыкания.

Пример простого замыкания:

```js
function makeCounter() {
let count = 0; // переменная в лексической области
return function () { // функция‑замыкание
count++; // доступ к count сохраняется
return count;
};
}

const counterA = makeCounter();
console.log(counterA()); // 1
console.log(counterA()); // 2

const counterB = makeCounter();
console.log(counterB()); // 1 (отдельное замыкание)
```

**Подводные камни**:
- **Утечки памяти**: если замыкание удерживает ссылки на крупные объекты, они не будут собраны GC, пока замыкание живо.
- **Неожиданные значения** при работе с циклами и `var`; решается использованием `let`/`const` или IIFE.
- **Сложность отладки**: цепочка вложенных замыканий может затруднять понимание, откуда берутся значения.

В Node.js замыкания работают так же, как в браузере, но часто встречаются в модулях, обработчиках событий, middleware Express и при работе с асинхронными API.

```js
// middleware с замыканием, сохраняет конфиг
function logger(options) {
return function (req, res, next) {
console.log(`[${options.prefix}] ${req.method} ${req.url}`);
next();
};
}
app.use(logger({ prefix: 'REQ' }));
```

Таким образом, замыкание — механизм, позволяющий функции запоминать и использовать переменные из окружающего контекста, что делает его фундаментальным инструментом для создания гибкого, модульного и асинхронного кода.
Что подразумевается под нормализацией базы данных?Нормализация — это процесс структурирования схемы реляционной базы данных с целью устранения избыточности данных и предотвращения аномалий при вставке, обновлении и удалении. Она основывается на наборе правил, называемых нормальными формами (1NF, 2NF, 3NF, BCNF, 4NF, 5NF и др.). Первая нормальная форма требует, чтобы все атрибуты были атомарными; вторая — чтобы каждый неключевой атрибут полностью зависел от всего составного первичного ключа; третья — чтобы не было транзитивных зависимостей между неключевыми атрибутами. Более продвинутые формы устраняют многозначные и совместные зависимости. Проходя через эти формы, проектировщик делит большую таблицу на несколько связанных, тем самым уменьшает дублирование, повышает целостность и упрощает поддержание данных. При этом иногда применяется денормализация — преднамеренное введение избыточности для повышения производительности запросов, но только после тщательного анализа. Пример: таблица «Orders» с полями (OrderID, CustomerName, CustomerAddress, ProductID, ProductName, ProductPrice) нормализуется, выделяя отдельные таблицы «Customers» (CustomerID, Name, Address) и «Products» (ProductID, Name, Price), а «Orders» хранит только ссылки (CustomerID, ProductID).
В чём отличаются PostgreSQL и MongoDB?PostgreSQL — это реляционная СУБД с поддержкой ACID‑транзакций, строгой схемы таблиц, SQL‑запросов, сложных связей (JOIN), продвинутых типовых данных и расширяемости (расширения, функции, индексы). MongoDB — документно‑ориентированная NoSQL‑БД, где данные хранятся в гибких BSON‑документах, схема может быть динамической, поддерживаются вложенные структуры, массивы, индексы по полям, базовые транзакции (начиная с версии 4.0) и горизонтальное шардинг‑масштабирование. Основные различия: модель данных (таблицы vs документы), язык запросов (SQL vs MQL), консистентность (полная ACID vs eventual consistency с опциями), масштабируемость (вертикальная vs горизонтальная), типичные сценарии (транзакционные бизнес‑приложения vs аналитика больших объёмов разнородных данных), инструменты администрирования и экосистема. Выбор зависит от требований к схеме, согласованности, нагрузкам и интеграции с приложениями (например, Node.js).
В чём различаются значения `null` и `undefined` в JavaScript?`null` — это преднамеренно присвоенное значение, означающее «отсутствие какого‑либо объектного значения». Его тип — `object`. Обычно используют, когда явно хотим указать, что переменная будет содержать объект, но пока его нет. `undefined` — это значение, которое получает переменная, если ей не было присвоено никакого значения, либо если функция не возвращает результат, либо если запрашиваем несуществующее свойство объекта. Его тип — `undefined`. При строгом сравнении (`===`) они различаются, но при нестрогом (`==`) считаются равными. Оба значения приводятся к `false` в логическом контексте, однако их поведение при сериализации (`JSON.stringify`) различается: `null` сохраняется, а `undefined` опускается. Рекомендации: использовать `null` для явного «пустого» значения, а `undefined` оставлять системе для обозначения «неинициализировано». При проверке часто используют `value === null` или `typeof value === "undefined"`.
Назови известные тебе паттерны проектирования и расскажи, где их применяют.**Порождающие (Creational)** – решают задачу создания объектов.
- **Singleton** – один экземпляр класса на всё приложение; в Node часто реализуется через модульный кеш.
- **Factory Method** – метод‑фабрика создаёт объекты, позволяя подклассам менять тип создаваемого продукта; удобно для выбора реализации драйвера БД.
- **Abstract Factory** – семейство взаимосвязанных фабрик, например, для создания клиентских и серверных компонентов разных протоколов.
- **Builder** – пошаговое построение сложного объекта, полезно при конфигурации запросов к API.
- **Prototype** – клонирование объектов через `Object.create`/`Object.assign`, ускоряет создание похожих структур.

**Структурные (Structural)** – упрощают построение отношений между классами/модулями.
- **Adapter** – адаптирует интерфейс сторонней библиотеки под ваш код; в Node часто используют обёртки над `fs` или `http`.
- **Facade** – предоставляет упрощённый интерфейс к подсистеме, например, обёртка над несколькими микросервисами.
- **Proxy** – контролирует доступ к объекту, реализуется через `new Proxy`; применяется для кэширования запросов или валидации.
- **Decorator** – динамически добавляет поведение объекту; в Express‑middleware это классический пример.
- **Composite** – иерархическое объединение объектов, удобно для построения деревьев маршрутов.
- **Flyweight** – разделяет общие данные между множеством мелких объектов, экономит память при работе с большими наборами данных.

**Поведенческие (Behavioral)** – управляют взаимодействием объектов.
- **Observer (Pub/Sub)** – подписка/уведомление, реализуется через `EventEmitter`.
- **Strategy** – выбор алгоритма во время выполнения, часто используется для переключения методов сериализации.
- **Command** – инкапсуляция запросов как объектов, удобно для реализации отмены/повтора операций.
- **Mediator** – централизует коммуникацию между компонентами, пример – брокер сообщений в микросервисной архитектуре.
- **Iterator** – последовательный доступ к элементам коллекции, в Node реализуется через генераторы.
- **State** – изменение поведения объекта при смене внутреннего состояния, часто в реализации конечных автоматов.
- **Template Method** – задаёт скелет алгоритма в базовом классе, позволяя подклассам переопределять шаги.
- **Chain of Responsibility** – последовательная передача запроса по цепочке обработчиков, пример – цепочка middleware в Express.

Каждый паттерн имеет свои плюсы и ограничения; их выбор зависит от требований проекта, масштабируемости и читаемости кода.
Назови основные свойства транзакций в системах управления базами данных.Транзакция обладает свойствами ACID: **Atomicity** (атомарность – все операции либо полностью выполняются, либо откатываются), **Consistency** (согласованность – после завершения транзакции БД переходит из одного согласованного состояния в другое, соблюдая все ограничения целостности), **Isolation** (изоляция – изменения, внесённые одной транзакцией, не видны другим до её фиксации; уровни изоляции: READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE), **Durability** (долговечность – после фиксации данные сохраняются даже при сбоях оборудования). Дополнительные свойства: **независимость от сбоев** (можность отката при ошибках), **идемпотентность** (повторный запуск транзакции приводит к тем же результатам), **поддержка вложенных транзакций и savepoints** (частичный откат), **автоматический контроль конфликтов** (механизмы блокировок, MVCC), **атомарный commit/rollback** (единственная точка фиксации).
В чём причина, что JavaScript исполняется в однопоточном режиме?JavaScript изначально был создан для браузеров, где основной поток отвечает за обработку пользовательского интерфейса и выполнение скриптов; чтобы избежать гонок и блокировок UI, язык использует модель однопоточного исполнения с event loop. В Node.js та же модель сохраняется: весь JavaScript‑код исполняется в одном потоке V8, а асинхронные операции (I/O, DNS, файловая система и т.п.) делегируются в фоновый пул libuv, после завершения их результаты помещаются в очередь событий и обрабатываются в том же основном потоке. Это упрощает написание кода без необходимости в мьютексах и блокировках, обеспечивает предсказуемость поведения и совместимость с браузерным окружением, при этом позволяя эффективно использовать асинхронные неблокирующие API.
Что представляет собой технология WebSocket и как она функционирует?;WebSocket — это полно‑дуплексный протокол прикладного уровня, работающий поверх TCP и позволяющий установить постоянное соединение между клиентом (обычно браузером) и сервером после начального HTTP‑handshake. При первом запросе клиент отправляет заголовок `Upgrade: websocket`, сервер отвечает статусом 101 Switching Protocols и подтверждает переход, после чего обе стороны могут обмениваться фреймами данных в обеих направлениях без необходимости повторных HTTP‑запросов, что значительно снижает задержку и нагрузку при реализации функций в реальном времени (чат, игровые серверы, потоковая передача). В Node.js для работы с WebSocket используют нативный модуль `ws`, библиотеку `socket.io` (которая добавляет автоматический fallback на долгий опрос и удобный API), а также `uWebSockets.js` для высокой производительности; типичные задачи включают аутентификацию при установке соединения, обработку событий `connection`, `message`, `close`, управление пинг‑понтами для поддержания живости канала и масштабирование через Redis‑pub/sub или кластеризацию. Преимущества WebSocket — постоянный канал, низкая латентность, экономия трафика, поддержка бинарных данных; ограничения — необходимость поддержки протокола на сервере, возможные проблемы с прокси/файрволом и необходимость учитывать безопасность (TLS, проверка origin, контроль доступа).;["Node.js","WebSocket","реальное время","протокол"];"Junior, Middle, Senior"
Что такое полиморфизм в программировании?Полиморфизм — это способность объектов разных классов обрабатывать одинаковые сообщения (вызовы методов) по‑разному, т.е. реализовать один и тот же интерфейс с различным поведением. В контексте JavaScript (Node.js) полиморфизм достигается через прототипное наследование, классы ES6, функции‑конструкторы и duck‑typing: метод, объявленный в базовом классе, может быть переопределён в подклассе, и при вызове этого метода у экземпляра будет использована версия, соответствующая его реальному типу. Кроме классического (подклассового) полиморфизма существует параметрический (дженерики в TypeScript) и ад‑хок полиморфизм (функции, принимающие любые типы). Полиморфизм упрощает расширяемость кода, позволяет писать обобщённые алгоритмы, работать с коллекциями разных объектов через общий интерфейс и реализовывать паттерны проектирования, такие как Strategy, Visitor и Factory. Пример в Node.js:

```js
class Animal {
speak() { console.log('...'); }
}
class Dog extends Animal {
speak() { console.log('Woof!'); }
}
class Cat extends Animal {
speak() { console.log('Meow!'); }
}
function makeItSpeak(animal) {
animal.speak(); // полиморфный вызов
}
В чём заключается различие между методом then и конструкциями async/await при работе с промисами в Node.js?**then** — это метод объекта Promise, позволяющий явно привязывать обработчики onFulfilled и onRejected к результату промиса; каждый вызов then возвращает новый промис, поддерживая цепочку вызовов, что даёт гибкость в построении параллельных и условных веток, но приводит к вложенным callback‑цепочкам и требует явного управления ошибками через catch или then с null в onFulfilled. **async/await** — синтаксический сахар, введённый в ES2017, который преобразует промис‑ориентированный код в вид, похожий на синхронный: функция, объявленная с async, всегда возвращает промис; внутри неё await приостанавливает выполнение до разрешения переданного промиса, автоматически пробрасывая исключения в try/catch, что упрощает чтение, отладку и последовательную обработку, но скрывает возможность прямого построения нескольких параллельных веток без дополнительных методов (например, Promise.all). В реальном коде then полезен для построения сложных цепочек, условных переходов и параллельных операций, тогда как async/await предпочтителен для линейных, читаемых алгоритмов и централизованного управления ошибками. Оба подхода совместимы: внутри async функций можно использовать then, а then‑обработчики могут вызывать async функции.
Для чего в программировании применяются транзакции?Транзакция — это набор операций над базой данных, которые должны быть выполнены как единое целое: либо все изменения фиксируются (commit), либо при любой ошибке откатываются (rollback). Это гарантирует свойства ACID: атомарность (независимость от внешних вмешательств), согласованность (сохранность целостности данных), изолированность (каждая транзакция не видит промежуточных состояний других) и долговечность (зафиксированные изменения сохраняются даже при сбоях). В Node‑приложениях транзакции используют при выполнении нескольких связанных запросов (например, списание средств и зачисление их получателю), при миграциях схем, при пакетной обработке данных и при необходимости обеспечить согласованность при конкурирующих запросах. Большинство ORM (Sequelize, TypeORM, Prisma) и драйверов (pg, mysql2) предоставляют API для начала, подтверждения и отката транзакций, а также позволяют вложенные (savepoint) и распределённые транзакции.
Из каких частей состоит платформа Node.js?Node.js — это совокупность нескольких ключевых компонентов: движок V8, который исполняет JavaScript‑код; библиотека libuv, обеспечивающая кроссплатформенный слой для асинхронного ввода‑вывода и управления событиями; встроенный цикл событий (event loop) и пул потоков, отвечающие за неблокирующее выполнение операций; набор системных модулей (core modules) вроде fs, http, net, crypto и др., предоставляющих доступ к файловой системе, сетевым протоколам и другим ресурсам; менеджер пакетов npm, позволяющий устанавливать и использовать сторонние библиотеки; а также API для создания пользовательских нативных аддонов на C/C++.
Как работает цикл событий в Node.js?Event Loop в Node.js – это механизм, который последовательно обрабатывает задачи из очередей, обеспечивая неблокирующее выполнение кода. Он управляется библиотекой libuv и работает в одном потоке, но делегирует тяжёлые операции (файловый ввод‑вывод, сетевые запросы, таймеры) в пул потоков или в ядро ОС. Цикл проходит через несколько фаз: timers (обработчики setTimeout/setInterval), pending callbacks (I/O‑колбэки), idle, prepare, poll (ожидание и обработка новых I/O‑событий), check (setImmediate), close callbacks. Между фазами выполняются микрозадачи: процесс.nextTick и промисы (queueMicrotask). Когда стек вызовов пуст, Event Loop берёт следующую задачу из текущей фазы, помещает её в стек и исполняет. Таким образом, синхронный код блокирует цикл, а асинхронные операции откладываются до тех пор, пока их колбэки не будут обработаны в соответствующей фазе, что позволяет Node обслуживать большое количество соединений без создания новых потоков для каждого запроса.
Назови уровни изоляции транзакций в системах управления базами данных.Уровни изоляции определяют, какие аномалии могут возникать при одновременном выполнении транзакций. Существует пять стандартных уровней, определённых в SQL‑стандарте, а также некоторые специфичные реализации:

1. **Read Uncommitted** – допускает «грязные чтения» (dirty reads); транзакция может видеть незавершённые изменения других транзакций.
2. **Read Committed** – предотвращает грязные чтения, но допускает «неповторяемые чтения» (non‑repeatable reads) и «фантомы» (phantom reads). Каждое отдельное SELECT‑запрос получает данные, зафиксированные к моменту его выполнения.
3. **Repeatable Read** – устраняет грязные и неповторяемые чтения, однако в некоторых СУБД (MySQL InnoDB) допускает фантомы; в PostgreSQL и Oracle обеспечивает полную повторяемость запросов в пределах транзакции.
4. **Serializable** – самый строгий уровень; имитирует выполнение транзакций последовательно, полностью исключая грязные чтения, неповторяемые чтения и фантомы. В реализации может использовать блокировки диапазонов или многоверсионный контроль (MVCC).
5. **Snapshot** (специфичный для PostgreSQL, SQL Server, Oracle) – основан на MVCC: каждая транзакция работает с «снимком» базы на момент её начала, тем самым предотвращая все три аномалии без блокирующего поведения, но может возникать конфликт при попытке обновления одной и той же строки (конфликт записи).

**Особенности реализации в популярных СУБД:**
- **PostgreSQL** использует MVCC; уровни Read Committed и Serializable (с «serializable snapshot isolation»).
- **MySQL InnoDB** поддерживает Read Uncommitted, Read Committed, Repeatable Read (по умолчанию) и Serializable; Repeatable Read реализован через блокировки диапазонов, что предотвращает фантомы.
- **SQL Server** имеет уровни Read Uncommitted (NOLOCK), Read Committed, Repeatable Read, Snapshot и Serializable; Snapshot включён в режим «READ COMMITTED SNAPSHOT».
- **Oracle** по умолчанию работает в режиме Read Consistent (аналог Snapshot) и поддерживает только Read Committed и Serializable.

Выбор уровня зависит от требований к согласованности данных и допустимой нагрузки на систему: более строгие уровни повышают защиту от аномалий, но могут уменьшать параллелизм и увеличивать время отклика.
Какие ключевые принципы объектно‑ориентированного программирования (ООП) тебе известны?Основные принципы ООП включают: инкапсуляцию (скрытие внутренней реализации и предоставление публичного интерфейса), абстракцию (выделение существенных характеристик объекта, игнорируя детали), наследование (создание новых классов на основе существующих, переиспользование и расширение кода), полиморфизм (возможность объектов разных классов обрабатываться единым образом через общий интерфейс). Дополнительные важные концепции: связи (агрегация, композиция), принципы SOLID (единственная ответственность, открытость/закрытость, подстановки Барбары Лисков, разделение интерфейсов, инверсия зависимостей), интерфейсы и абстрактные классы для определения контрактов.
Приведи пример кода, где нарушен принцип единственной ответственности.В Node.js часто нарушают SRP, объединяя в одном модуле несколько логически разнородных задач.
**Пример нарушения:**

```js
// userService.js – нарушение SRP
const fs = require('fs');
const db = require('./db');
const nodemailer = require('nodemailer');

class UserService {
// Работа с БД
async createUser(data) {
const user = await db.users.insert(data);
await this.sendWelcomeEmail(user.email);
await this.logCreation(user.id);
return user;
}

// Отправка письма – отдельная ответственность
async sendWelcomeEmail(email) {
const transporter = nodemailer.createTransport({ /* config */ });
await transporter.sendMail({
from: '"App" <no-reply@app.com>',
to: email,
subject: 'Welcome!',
text: 'Thank you for registering.'
});
}

// Логирование в файл – отдельная ответственность
async logCreation(userId) {
const log = `User ${userId} created at ${new Date().toISOString()}\n`;
await fs.promises.appendFile('user.log', log);
}
}

module.exports = new UserService();
```

**Почему это нарушение SRP:**
1. **Множественные причины изменения.** Класс `UserService` будет менятьcя, если изменятся требования к работе с БД, способу отправки писем или формату логов.
2. **Сложность тестирования.** При unit‑тестировании `createUser` необходимо мокировать и базу, и почтовый сервис, и файловую систему, что делает тесты громоздкими.
3. **Нарушение инкапсуляции.** Каждый из методов (`sendWelcomeEmail`, `logCreation`) может быть переиспользован в других частях системы, но они скрыты внутри класса, что затрудняет повторное использование.

**Как исправить (разделить ответственности):**

```js
// userRepository.js – отвечает только за работу с БД
class UserRepository {
async create(data) {
return db.users.insert(data);
}
}
module.exports = new UserRepository();

// emailService.js – отвечает только за отправку писем
class EmailService {
async sendWelcome(email) {
const transporter = nodemailer.createTransport({ /* config */ });
await transporter.sendMail({
from: '"App" <no-reply@app.com>',
to: email,
subject: 'Welcome!',
text: 'Thank you for registering.'
});
}
}
module.exports = new EmailService();

// logger.js – отвечает только за логирование
class Logger {
async logCreation(userId) {
const log = `User ${userId} created at ${new Date().toISOString()}\n`;
await fs.promises.appendFile('user.log', log);
}
}
module.exports = new Logger();

// userService.js – оркестратор, не содержит бизнес‑логики
const userRepo = require('./userRepository');
const emailSvc = require('./emailService');
const logger = require('./logger');

class UserService {
async createUser(data) {
const user = await userRepo.create(data);
await emailSvc.sendWelcome(user.email);
await logger.logCreation(user.id);
return user;
}
}
module.exports = new UserService();
```

Теперь каждый модуль имеет одну причину для изменения, их проще тестировать (мокируем отдельные зависимости), а `UserService` выступает лишь как координатор.
Приведите пример применения принципа инверсии зависимостей.Принцип инверсии зависимостей (DIP) гласит, что высокоуровневые модули не должны зависеть от низкоуровневых, а оба должны зависеть от абстракций; абстракции не должны зависеть от деталей, а детали — от абстракций. В Node.js это обычно реализуется через внедрение зависимостей через конструктор или фабрику. Пример: создаём интерфейс Logger (в виде TypeScript‑type или обычного соглашения), реализуем ConsoleLogger и FileLogger, а класс UserService принимает объект Logger в конструкторе; таким образом UserService зависит от абстракции Logger, а конкретные реализации можно подменять без изменения кода. Пример кода: `// logger.ts\nexport interface Logger { log(message: string): void; }\n// consoleLogger.ts\nimport { Logger } from './logger';\nexport class ConsoleLogger implements Logger { log(msg) { console.log(msg); } }\n// fileLogger.ts\nimport { Logger } from './logger';\nimport * as fs from 'fs';\nexport class FileLogger implements Logger { private file = 'app.log'; log(msg) { fs.appendFileSync(this.file, msg + '\\n'); } }\n// userService.ts\nimport { Logger } from './logger';\nexport class UserService { constructor(private logger: Logger) {} createUser(name: string) { /* ... */ this.logger.log(\`User \${name} created\`); } }\n// compositionRoot.js\nimport { ConsoleLogger } from './consoleLogger';\nimport { UserService } from './userService';\nconst logger = new ConsoleLogger();\nconst userService = new UserService(logger);\nuserService.createUser('Alice');` Таким образом, замена ConsoleLogger на FileLogger не требует правок в UserService, что иллюстрирует DIP.
Что обозначает буква D в наборе принципов SOLID?Принцип D — Dependency Inversion Principle (принцип инверсии зависимостей). Он гласит, что высокоуровневые модули не должны зависеть от низкоуровневых, а оба уровня должны зависеть от абстракций (интерфейсов, абстрактных классов). Абстракции же не должны зависеть от деталей реализации, а детали — от абстракций. В Node.js это обычно реализуется через внедрение зависимостей (DI): вместо прямого `require('./db')` передаётся объект, реализующий нужный интерфейс, что упрощает тестирование, замену реализаций и повышает гибкость кода. Пример:
```js
// high‑level service
class UserService {
constructor(userRepo) { this.userRepo = userRepo; }
async getUser(id) { return this.userRepo.findById(id); }
}
// low‑level repository
class MongoUserRepo {
async findById(id) { return db.collection('users').findOne({ _id: id }); }
}
// composition root
const userRepo = new MongoUserRepo();
const userService = new UserService(userRepo);
```
Здесь `UserService` зависит от абстракции `userRepo`, а конкретная реализация (`MongoUserRepo`) может быть заменена, например, на `InMemoryUserRepo` для тестов. Принцип способствует уменьшению связанности, улучшает масштабируемость и облегчает модульное тестирование, но требует дополнительного кода для создания и управления абстракциями, а также может усложнить структуру проекта, если переусердствовать.
Что представляет собой EventEmitter в Node.js?EventEmitter — это встроенный класс из модуля events, реализующий шаблон «наблюдатель» (publish/subscribe); он позволяет объектам генерировать (emit) именованные события и регистрировать обработчики (listener) через методы on, once, addListener, а также управлять ими с помощью removeListener, removeAllListeners, setMaxListeners и getMaxListeners; при вызове emit передаются любые аргументы, которые получат все подписанные обработчики; наследоваться от EventEmitter можно через class MyClass extends EventEmitter или util.inherits, что упрощает создание пользовательских эмиттеров, позволяя инкапсулировать бизнес‑логику и асинхронные операции; важно помнить о лимите в 10 слушателей на событие (можно изменить), а также о потенциальных утечках памяти при незакрытых подписках; типичный пример: const EventEmitter = require('events'); class MyEmitter extends EventEmitter {} const myEmitter = new MyEmitter(); myEmitter.on('data', (msg) => console.log(msg)); myEmitter.emit('data', 'Hello World');.
Что такое идемпотентные HTTP‑методы?Идемпотентный HTTP‑метод — это такой метод, выполнение которого один или более раз приводит к одинаковому результату и не изменяет состояние сервера после первого успешного применения. Идея состоит в том, что повторный запрос не имеет дополнительных побочных эффектов. Классическими идемпотентными методами являются GET (получение ресурса), HEAD (получение заголовков), PUT (полная замена ресурса), DELETE (удаление ресурса) и OPTIONS (получение возможностей сервера). POST обычно не считается идемпотентным, так как каждый запрос может создавать новый ресурс или изменять состояние. Идемпотентность важна для реализации надежных клиентских библиотек, кэширования, повторных попыток запросов и распределённых систем, где возможны дублирование запросов из‑за сетевых сбоев.
Что представляет собой индекс в базе данных?Индекс — это специализированная структура данных, создаваемая над одной или несколькими колонками таблицы с целью ускорения поиска, сортировки и выполнения условий фильтрации. По сути, он хранит копию значений выбранных полей в упорядоченном виде (чаще всего в виде B‑tree), позволяя быстро находить строки без полного сканирования таблицы. Существует несколько типов индексов: B‑tree (наиболее распространённый, поддерживает диапазонные запросы), Hash (быстрый поиск точных совпадений), GiST, SP‑GiST, GIN (для полнотекстового поиска и массивов), BRIN (для больших таблиц с естественным порядком). Индексы могут быть уникальными, запрещая дублирование значений, и покрывающими (covering), когда все необходимые столбцы запроса находятся в самом индексе, что избавляет от обращения к таблице. Создание индекса требует дополнительных ресурсов: место на диске, время на построение и поддержание при вставках/обновлениях/удалениях. Поэтому их следует создавать только для часто используемых запросов, особенно в WHERE, ORDER BY, GROUP BY, JOIN. В SQL индекс создаётся командой CREATE INDEX (или UNIQUE INDEX), а удаляется DROP INDEX. Анализировать эффективность индексов помогает EXPLAIN/EXPLAIN ANALYZE, а для их оптимизации используют такие инструменты, как pg_stat_user_indexes (PostgreSQL) или sys.dm_db_index_usage_stats (SQL Server).
Как определяется уровень изоляции транзакции в базе данных?Уровень изоляции транзакции — это параметр, определяющий, насколько операции одной транзакции защищены от влияния одновременно выполняющихся транзакций. Он является частью свойства ACID (Isolation) и управляет видимостью изменённых данных до их фиксации (commit). В реляционных СУБД принято выделять четыре стандартных уровня изоляции, описанных в SQL‑стандарте, каждый из которых устраняет определённые аномалии чтения:

1. **Read Uncommitted (чтение непотверждённого)** – позволяет «грязные чтения» (dirty reads), когда транзакция читает данные, изменённые другой, ещё не зафиксированной транзакцией.
2. **Read Committed (чтение подтверждённого)** – запрещает грязные чтения, но допускает «неповторяемые чтения» (non‑repeatable reads): при повторном запросе те же строки могут вернуть другие значения, если другая транзакция их изменила и зафиксировала.
3. **Repeatable Read (повторяемое чтение)** – исключает как грязные, так и неповторяемые чтения, гарантируя, что все строки, прочитанные в транзакции, будут иметь одинаковые значения до её завершения. Однако возможны «фантомные чтения» (phantom reads): при выполнении новых запросов (например, INSERT) могут появиться новые строки, удовлетворяющие условию.
4. **Serializable (сериализуемый)** – самый строгий уровень, полностью исключающий фантомные, неповторяемые и грязные чтения, имитируя последовательное выполнение транзакций. Реализуется обычно через блокировки диапазонов или многоверсионный контроль (MVCC).

**Проблемы, которые решают уровни изоляции:**
- *Dirty read* – чтение незавершённых изменений.
- *Non‑repeatable read* – изменение уже прочитанных строк другими транзакциями.
- *Phantom read* – появление/удаление строк, удовлетворяющих запросу.

**Реализация в популярных СУБД:**
- PostgreSQL использует MVCC; по умолчанию — Read Committed, но поддерживает Repeatable Read и Serializable.
- MySQL (InnoDB) по умолчанию — Repeatable Read, реализует блокировки записей и «gap‑locks» для предотвращения фантомов.
- Microsoft SQL Server — Read Committed по умолчанию, с опцией SNAPSHOT для MVCC‑подхода.

**Установка уровня изоляции в Node.js:**
При работе с клиентами (например, `pg` для PostgreSQL, `mysql2`, `sequelize`, `typeorm`) уровень изоляции задаётся в начале транзакции:

```js
await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
// … операции …
await client.query('COMMIT');
```

Или через опции ORM:

```js
await sequelize.transaction({ isolationLevel: Transaction.ISOLATION_LEVELS.REPEATABLE_READ }, async (t) => {
// transactional queries
});
```

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

---
Что такое шаблон проектирования Singleton?Шаблон Singleton — это порождающий шаблон, гарантирующий, что у класса существует ровно один экземпляр, и предоставляющий глобальную точку доступа к этому экземпляру. В контексте Node.js реализуется обычно через модульную систему: при `require` модуль кэшируется, поэтому экспортированный объект уже является единственным. Можно также создать класс с приватным конструктором (через Symbol/WeakMap) и статическим методом `getInstance`, который при первом вызове создаёт объект, а последующие возвращают сохранённый. Основные цели: управление общим состоянием (конфигурация, пул соединений, логгер), экономия ресурсов и синхронный доступ. При реализации следует учитывать: — в многопоточном окружении (worker threads) кэш модуля не синхронен, поэтому Singleton может быть не глобален; — избегать чрезмерного использования, так как глобальное состояние усложняет тестирование; — в ES‑модулях `import` тоже кэширует экспорт. Пример простого Singleton в Node:

```js
// logger.js
class Logger {
log(msg) { console.log(`[${new Date().toISOString()}] ${msg}`); }
}
let instance = null;
module.exports = {
getInstance() {
if (!instance) instance = new Logger();
return instance;
}
};
```

И использование:

```js
const logger = require('./logger').getInstance();
logger.log('Приложение запущено');
```

Альтернативно, экспортировать уже созданный объект:

```js
module.exports = new Logger(); // сразу единственный экземпляр
```

При использовании ES‑модулей:

```js
class Config { /* … */ }
const config = new Config();
export default config; // импортирует один и тот же объект везде
```

Таким образом, Singleton обеспечивает единый объект‑ресурс, контролирует его создание и предоставляет удобный глобальный интерфейс.
Чем отличаются операторы == и === в JavaScript?Оператор == выполняет сравнение с приведением типов (abstract equality): перед сравнением значения приводятся к общему типу согласно правилам приведения, поэтому, например, 0 == false, "5" == 5 или null == undefined возвращают true. Оператор === — строгий оператор сравнения (strict equality): сравниваются как тип, так и значение без какого‑либо приведения, поэтому 0 === false, "5" === 5, null === undefined дают false. При === сравнение объектов происходит по ссылке, а не по содержимому; два разных объекта с одинаковыми свойствами не равны. Особый случай: NaN не равно ни себе ни при ==, ни при === (NaN == NaN и NaN === NaN оба false). При использовании == следует быть осторожным из‑за неявного приведения, которое может привести к неожиданным результатам; рекомендуется предпочитать === для предсказуемого и безопасного сравнения. Также стоит помнить, что == может вызвать скрытые вызовы методов valueOf/toString у объектов, тогда как === их не использует.
В чём заключается различие между протоколами HTTP и HTTPS?HTTP — это протокол передачи гипертекстовых данных без шифрования, использующий порт 80, где запросы и ответы передаются в открытом виде, что делает их уязвимыми для перехвата и подделки. HTTPS — это HTTP, защищённый при помощи TLS/SSL: соединение устанавливается через порт 443, включает процесс рукопожатия (handshake), в ходе которого происходит аутентификация сервера с помощью цифрового сертификата, согласование криптографических параметров и генерация сеансового ключа, обеспечивая конфиденциальность, целостность и подлинность передаваемых данных. Благодаря шифрованию HTTPS защищает от атак типа «человек посередине», предотвращает подслушивание и изменение трафика, поддерживает механизмы HSTS и улучшает SEO‑рейтинги. Несмотря на небольшое увеличение задержек из‑за криптографических операций, современные оптимизации (TLS 1.3, сессии резюмирования) минимизируют влияние на производительность.
В чём различаются объявления переменных var, let и const в JavaScript?`var` объявляет переменные с функциональной областью видимости, поднимается (hoisting) и инициализируется `undefined`; её можно переопределять и повторно объявлять в одной и той же функции/скрипте, а при объявлении в глобальном контексте создаёт свойство объекта `globalThis`. `let` имеет блочную область видимости, также поднимается, но доступ к ней до фактического объявления запрещён (Temporal Dead Zone); её можно переassignировать, но нельзя объявлять повторно в той же области. `const` аналогичен `let` по блочной области и TDZ, но требует инициализации при объявлении и запрещает дальнейшее переопределение привязки; при этом содержимое объектов, объявленных через `const`, остаётся изменяемым. Кроме того, `var`‑переменные свойства глобального объекта, тогда как `let`/`const` в глобальном контексте лишь объявляют переменные без создания свойства глобального объекта.
Какие различия существуют между различными типами JOIN в SQL?INNER JOIN возвращает только те строки, где совпадают значения в указанных столбцах обеих таблиц; LEFT (OUTER) JOIN возвращает все строки из левой таблицы и совпадающие строки из правой, заполняя отсутствующие значения NULL; RIGHT (OUTER) JOIN аналогично, но сохраняет все строки правой таблицы; FULL (OUTER) JOIN объединяет результаты LEFT и RIGHT, возвращая все строки из обеих таблиц с NULL там, где нет совпадений; CROSS JOIN образует декартово произведение, сочетая каждую строку первой таблицы со всеми строками второй; SELF JOIN — это JOIN таблицы самой с собой, часто используется для иерархических данных; NATURAL JOIN автоматически соединяет таблицы по столбцам с одинаковыми именами, устраняя необходимость явно указывать условие ON. Кроме того, различия могут проявляться в порядке выполнения, необходимости индексов и влиянии на план выполнения запроса.
В чём различаются типы (type) и интерфейсы (interface) в TypeScript?Типы (`type`) и интерфейсы (`interface`) в TypeScript оба описывают форму объектов, но имеют различия в возможностях и поведении:

- **Синтаксис и возможности**:
- `interface` может объявлять только объектные формы (свойства, методы, индекс‑подписи, наследование через `extends`).
- `type` позволяет создавать алиасы для любой формы – примитивов, объединений, пересечений, кортежей, функций и т.п. (`type Id = string | number`).

- **Наследование и расширение**:
- `interface` поддерживает множественное наследование через `extends` и может быть реализована классом (`implements`).
- `type` может комбинироваться только через пересечения (`&`) и объединения (`|`), но не участвует в `implements`.

- **Слияние деклараций**:
- `interface` поддерживает декларативное слияние: несколько объявлений с одинаковым именем объединяются в один интерфейс.
- `type` не может сливаться; повторное объявление приводит к ошибке.

- **Совместимость**:
- Оба используют структурную типизацию, поэтому совместимы при одинаковой форме.
- При одинаковом объявлении `interface` и `type` (например, `type Point = { x: number; y: number }` и `interface Point { x: number; y: number }`) они взаимозаменяемы в большинстве контекстов.

- **Когда использовать**:
- Выбирайте `interface`, если планируете расширять тип в разных файлах, использовать декларативное слияние или реализовывать его в классах.
- Выбирайте `type`, когда нужно описать более сложные конструкции (универсальные объединения, кортежи, условные типы) или создать алиас для примитивов.

Таким образом, `interface` более ориентирован на описание объектных форм с возможностью расширения и слияния, тогда как `type` предоставляет более гибкую систему алиасов для любых типов.
Что подразумевается под смыслом нормализации данных?Нормализация данных — это процесс структурирования схемы базы данных с целью устранения избыточности, предотвращения аномалий при вставке, обновлении и удалении, а также обеспечения целостности и логической согласованности хранимой информации. Основные задачи нормализации:
1. **Уменьшение дублирования** — каждый факт хранится в единственном месте, что экономит место и упрощает поддержание актуальности данных.
2. **Избежание аномалий** — при несоблюдении нормализации могут возникать аномалии вставки (нельзя добавить запись без полного набора данных), обновления (необходимо изменять несколько строк) и удаления (потеря необходимой информации).
3. **Обеспечение целостности** — вводятся ключи (первичные, внешние) и ограничения, которые гарантируют корректные ссылки между таблицами.
4. **Улучшение гибкости запросов** — правильно нормализованные таблицы позволяют писать более простые и предсказуемые запросы, а также легче добавлять новые атрибуты и отношения.

Нормальные формы (NF) формализуют степень нормализации:
- **1NF** — атомарность атрибутов, отсутствие повторяющихся групп.
- **2NF** — отсутствие частичной зависимости от составного первичного ключа.
- **3NF** — отсутствие транзитивных зависимостей между неключевыми атрибутами.
- **BCNF** — усиление 3NF, каждый детерминант является кандидатом в ключ.
- **4NF** — устранение многозначных зависимостей.
- **5NF** — устранение соединительных зависимостей, требующих более сложных разложений.

Процесс нормализации обычно включает:
1. Выявление всех сущностей и их атрибутов.
2. Определение первичных ключей.
3. Выделение зависимостей и построение диаграмм «сущность‑атрибут».
4. Последовательное разложение таблиц согласно требованиям каждой NF, проверка сохранения зависимостей и возможности восстановления исходных данных через JOIN‑операции.

**Плюсы**: экономия места, упрощённое обслуживание, высокая целостность, предсказуемое поведение при изменениях.
**Минусы**: увеличение количества таблиц и JOIN‑ов, что может снизить производительность при сложных запросах; иногда в аналитических системах применяют денормализацию для ускорения чтения.

В реальных проектах часто выбирают компромиссный уровень (обычно 3NF или BCNF), а для OLAP‑систем применяют частичную денормализацию (звёздные схемы).
В чём различаются перегрузка методов и переопределение методов?Перегрузка методов — это создание в одном классе нескольких функций с одинаковым именем, но различающихся списком параметров (количеством, типом или порядком). Выбор конкретной реализации происходит на этапе компиляции (статически) на основе переданных аргументов. Перегрузка обычно поддерживается в языках с статической типизацией (Java, C#, TypeScript) и в JavaScript/Node.js её можно имитировать лишь вручную, проверяя типы и количество аргументов внутри функции.

Переопределение методов — это определение в подклассе метода с тем же именем и сигнатурой, что и у метода базового класса. При вызове метода на объекте, экземпляре подкласса, используется реализация из подкласса, что происходит во время выполнения (динамический полиморфизм). Переопределение требует наследования и обычно сопровождается ключевыми словами `override` (C#), `@Override` (Java) или просто объявлением метода с тем же именем в прототипе/классе в JavaScript.

Ключевые различия:

1. **Цель** – перегрузка позволяет предоставить несколько вариантов одной операции в рамках одного класса, а переопределение меняет поведение унаследованного метода в подклассе.
2. **Время разрешения** – перегрузка решается на этапе компиляции (статически), переопределение – во время выполнения (динамически).
3. **Необходимость наследования** – перегрузка не требует наследования, переопределение обязательно подразумевает наличие иерархии классов.
4. **Сигнатура** – при перегрузке сигнатуры различаются; при переопределении сигнатура должна полностью совпадать (за исключением ковариантных возвратов в некоторых языках).

В Node.js (ES6+), где классы реализованы через прототипное наследование, перегрузка реализуется вручную проверкой `arguments.length` и типов, а переопределение происходит просто путем объявления метода с тем же именем в подклассе.

Таким образом, перегрузка — это множественное определение метода с разными параметрами в одном классе, а переопределение — это замена реализации унаследованного метода в дочернем классе.
Для чего в JavaScript (Node.js) применяется метод bind?Метод Function.prototype.bind создаёт новую функцию, у которой фиксирован контекст this и (при желании) предустановлены начальные аргументы. Это позволяет передавать функции в качестве колбэков, сохраняя нужный объект‑контекст, использовать частичное применение (partial application) для создания специализированных вариантов функции, привязывать методы к экземплярам классов при работе с событиями, таймерами или асинхронными API, а также избегать ошибок, связанных с потерей контекста при вызове функции в другом месте. Кроме того, bind может использоваться для имитации поведения «метода» у функций, которые изначально не являются методами объекта.
Для чего применяется async в JavaScript/Node.js?Async‑ключевое слово вводит асинхронные функции, позволяющие писать неблокирующий код, который возвращает промис и может использоваться с оператором await; это упрощает работу с асинхронными операциями (например, ввод‑вывод, сетевые запросы, таймеры), улучшает читаемость по сравнению с цепочками .then() и колбэками, интегрируется с Event Loop, обеспечивает последовательное выполнение асинхронных шагов без блокировки основного потока и облегчает обработку ошибок через try/catch.
Для чего используется первичный ключ (Primary Key) в базе данных?Первичный ключ — это набор одного или нескольких столбцов таблицы, значения которых однозначно идентифицируют каждую запись; он обязан быть уникальным и не может принимать NULL, автоматически создаёт уникальный индекс, ускоряя поиск и обеспечивая целостность данных, служит основой для построения внешних ключей и реализации связей «один‑ко‑многим» и «один‑к‑одному», упрощает репликацию, миграцию и сравнение данных, позволяет быстро находить, обновлять и удалять конкретные строки, а также гарантирует, что в таблице не появятся дублирующие или «потерянные» записи; рекомендуется использовать искусственные (surrogate) ключи (например, автоинкрементные целые) или естественные, если они стабильно уникальны, и избегать изменения значений первичного ключа после их назначения.
Для чего используется внедрение зависимостей (dependency injection)?Внедрение зависимостей (DI) — это паттерн инверсии управления, позволяющий передавать объектам их зависимости извне вместо того, чтобы они создавали их самостоятельно. Это снижает степень сцепления модулей, делает код более гибким и переиспользуемым, упрощает замену реализаций (например, подмену репозитория на мок в тестах), централизует конфигурацию и управление жизненным циклом объектов, повышает читаемость и упрощает масштабирование проекта. В Node‑экосистеме DI часто реализуется через контейнеры (InversifyJS, TSyringe, Awilix), которые позволяют регистрировать интерфейсы и их реализации, автоматически разрешать зависимости и поддерживать различные области видимости (singleton, transient). Благодаря DI упрощается unit‑тестирование, так как зависимости могут быть подменены простыми заглушками, а также повышается поддерживаемость кода, поскольку изменения в реализации зависимостей не требуют правок в потребителях.
Для чего применяется инкапсуляция?Инкапсуляция — это механизм объединения данных (состояния) и методов, работающих с этими данными, в единую единицу (объект) и скрытия внутренней реализации от внешнего кода. В Node.js инкапсуляцию реализуют с помощью классов ES6, приватных полей (`#field`), геттеров/сеттеров, а также замыканий в функциях‑модулях. Основные цели инкапсуляции: защита внутреннего состояния от некорректного изменения, упрощение поддержки кода за счёт ограничения доступа к внутренним деталям, возможность изменения реализации без влияния на внешние зависимости, повышение читаемости и модульности. Пример с классом:

```js
class User {
#password; // приватное поле
constructor(name, password) {
this.name = name;
this.#password = password;
}
checkPassword(pwd) {
return pwd === this.#password;
}
}
```

Здесь поле `#password` недоступно извне (`user.#password` вызовет ошибку), а доступ к нему возможен только через публичные методы. Аналогично, модульный подход с замыканиями:

```js
function createCounter() {
let count = 0; // закрытая переменная
return {
inc() { ++count; },
get() { return count; }
};
}
```

`count` скрыт внутри замыкания и может быть изменён только через методы `inc` и `get`. Инкапсуляция также способствует реализации принципов SOLID, облегчает тестирование (можно мокировать публичный интерфейс) и повышает безопасность, особенно в многопользовательских приложениях, где важно контролировать доступ к конфиденциальным данным.
Для чего нужны guard‑условия?Guards (guards) — это специальные проверки, которые позволяют гарантировать выполнение кода только при выполнении определённых условий. В контексте Node.js и TypeScript они часто используют для:
- **Типовых guard‑функций** (type guards), которые в рантайме проверяют тип переменной и позволяют компилятору сузить тип (type narrowing), делая последующий код безопаснее и избавляя от необходимости явных кастов.
- **Валидации входных данных** в API‑эндпойнтах, где guard‑функции проверяют структуру и типы получаемых JSON‑объектов, возвращая ошибки 400 при несоответствии, тем самым защищая приложение от некорректных запросов.
- **Контроль доступа** (authorization guards) в фреймворках типа NestJS, где guard‑классы определяют, имеет ли текущий пользователь права на выполнение действия, и могут прерывать запрос до попадания в контроллер.
- **Обеспечения безопасности**: guard‑мидлвары в Express могут проверять наличие и валидность JWT‑токенов, CSRF‑токенов, ограничивать количество запросов (rate‑limiting) и т.п.
Преимущества использования guards:
1. **Явная проверка условий** – код становится более читаемым и предсказуемым.
2. **Уменьшение количества ошибок** – раннее отбрасывание неверных данных или недоступных операций.
3. **Улучшение типизации** – TypeScript‑guard‑функции позволяют избежать `any` и `unknown`, делая код статически проверяемым.
4. **Повторное использование** – один guard можно применять в разных местах приложения, централизуя логику проверки.
5. **Разделение ответственности** – бизнес‑логика остаётся чистой, а проверки вынесены в отдельные функции/классы.
Пример типового guard‑функции в TypeScript:
```ts
function isUser(obj: any): obj is User {
return typeof obj === 'object' &&
typeof obj.id === 'number' &&
typeof obj.name === 'string';
}

// Использование
function greet(entity: unknown) {
if (isUser(entity)) {
console.log(`Hello, ${entity.name}`);
} else {
console.log('Entity is not a User');
}
}
```
Таким образом, guards служат механизмом защиты и уточнения контекста выполнения кода, повышая надёжность и поддерживаемость приложений.
Для чего в базе данных создают индексы?Индексы — это специальные структуры данных (чаще всего B‑tree или hash), которые позволяют быстро находить строки в таблице по значениям одного или нескольких столбцов. Они ускоряют выполнение запросов с условиями WHERE, ORDER BY, GROUP BY и JOIN, уменьшают количество полных сканирований таблицы, позволяют реализовать уникальность (UNIQUE, PRIMARY KEY), поддерживают покрывающие запросы (covering indexes) и облегчают работу с диапазонами. При этом индексы занимают дополнительное место на диске и требуют обновления при вставках, обновлениях и удалениях, что замедляет операции записи, поэтому их следует создавать только для часто используемых запросов и тщательно выбирать набор колонок. Типы индексов (B‑tree, hash, GiST, GIN, BRIN) подходят под разные типы данных и запросов, а комбинированные (мульти‑колонковые) индексы позволяют оптимизировать сложные условия. Правильный выбор и поддержка индексов повышают производительность базы данных, но требуют мониторинга их использования и периодической переоценки.
Как разработчики писали асинхронный код в JavaScript до появления синтаксиса async/await?До появления async/await асинхронный код в JavaScript реализовывался в основном с помощью:
* **Колбэков** (callback‑функций) – передача функции‑обработчика в асинхронный API (например, `fs.readFile`, `setTimeout`). Для последовательных операций использовали «callback hell», часто решая её через именованные функции или модули‑утилиты.
* **Promise** (ES6, 2015) – объект‑обёртка над асинхронным результатом, позволяющая цепочкой выстраивать операции через `then`, `catch` и `finally`. Для параллельных задач использовали `Promise.all`, `Promise.race`. Библиотеки — Bluebird, Q, RSVP — добавляли расширенные возможности (промис‑таймауты, отмена, промис‑методы).
* **Библиотеки управления потоками** – `async.js`, `when`, `lodash.async` и др. Предоставляли функции `series`, `parallel`, `waterfall`, `mapLimit` и т.п., скрывая вложенные колбэки и упрощая обработку ошибок.
* **Генераторы + runner** – комбинация ES6‑генераторов (`function*`) и вспомогательного раннера (например, `co`, `thunkify`). Генератор «выдавал» промис, а раннер автоматически переходил к следующему `yield`, имитируя синхронный стиль кода.
* **Транспилеры** – до нативного `async/await` в браузерах использовались Babel/TypeScript, которые преобразовывали `async`‑функции в код на основе генераторов и промисов.
* **Event‑emitter и потоки** – для потоковой обработки данных применялись `EventEmitter`, `Readable/Writable`‑стримы, где события (`data`, `end`, `error`) обслуживались колбэками.
Все эти подходы требовали тщательного управления ошибками (передача `err` в первый аргумент колбэка, `catch` у промисов) и часто приводили к вложенной структуре кода, которую решило проблему появление `async/await`.
Как работает сборщик мусора в Node.js?В Node.js сборщик мусора реализован в движке V8 и использует поколенческую стратегию: небольшие «молодые» объекты размещаются в памяти поколения «новый», где применяется быстрый алгоритм Scavenger (копирующий сборщик) с фазой «промаркировать‑переместить». Когда объект переживает несколько сборок, он продвигается в более старое поколение «old space», где применяется более тщательный алгоритм Mark‑Sweep‑Compact: сначала отмечаются все достижимые объекты (mark), затем неотмеченные освобождаются (sweep), а оставшиеся сжимаются, чтобы устранить фрагментацию. Сборки могут выполняться инкрементно и параллельно, чтобы минимизировать паузы: V8 делит работу на небольшие части (incremental marking) и использует фоновый поток (concurrent sweeping). Система также поддерживает финализаторы и слабые ссылки (WeakMap, WeakSet), а разработчик может влиять на поведение сборщика через флаги запуска (–max-old-space-size, –expose-gc) и вручную вызывать глобальный gc() при включённом режиме.
Как реализовать многопоточность в JavaScript?В JavaScript сам язык однопоточный, но в Node.js можно выполнять код параллельно, используя несколько механизмов: 1) модуль worker_threads (доступен с Node 10.5, стабилен с 12) позволяет создавать отдельные потоки — каждый имеет собственный V8‑контекст и event loop; данные передаются через postMessage или через SharedArrayBuffer/Atomics, что даёт возможность совместного доступа к памяти; пример: const {Worker}=require('worker_threads'); const w=new Worker('./worker.js'); w.postMessage({cmd:'calc',data:...}); w.on('message',msg=>console.log(msg)); 2) кластер (cluster) – спawns несколько процессов, каждый со своим экземпляром Node, удобно для масштабирования серверов и использования всех ядер CPU; основной процесс распределяет входящие соединения между воркерами через IPC; 3) child_process – запуск внешних скриптов или программ в отдельном процессе (fork, exec, spawn), полезно для тяжёлых задач, не требующих совместного доступа к памяти; 4) в браузерах – Web Workers и SharedWorker, которые работают в отдельных потоках и общаются через postMessage; совместный доступ к памяти реализуется через SharedArrayBuffer и атомарные операции (Atomics). Кроме того, можно писать нативные аддоны на C/C++ с собственными потоками и вызывать их из Node через N‑API. При выборе следует учитывать: необходимость совместного доступа к данным (тогда SharedArrayBuffer), размер задачи (CPU‑bound vs I/O‑bound), сложность отладки и стоимость синхронизации; для большинства CPU‑интенсивных задач в Node.js рекомендуется использовать worker_threads, а для масштабирования серверных приложений – cluster. Также важно помнить, что любые потоки работают независимо, поэтому исключения, память и глобальные переменные не разделяются, а взаимодействие происходит только через явно определённый IPC.
Как на практике реализуется принцип разделения интерфейсов?Для соблюдения ISP в Node‑проектах (особенно с TypeScript) следует определять небольшие, специализированные интерфейсы, которые описывают только те методы, которые реально нужны клиенту. Класс или модуль внедряется через зависимости, реализуя лишь те интерфейсы, которые соответствуют его ответственности; если требуется более широкая функциональность, используется композиция нескольких мелких интерфейсов вместо одного «толстого». При проектировании API‑контрактов в JavaScript/Node.js можно применять типы‑псевдоинтерфейсы (JSDoc) или отдельные функции‑модули, каждый из которых отвечает за одну задачу, что упрощает тестирование и замену реализации. Примеры: отдельные репозитории `UserReadRepository`, `UserWriteRepository`; сервисы с зависимостями `ILogger`, `IEmailSender` вместо единого `IService`. Такой подход уменьшает связность, повышает гибкость и облегчает поддержку кода.
Какие ORM доступны для разработки на Node.js?Sequelize — популярный ORM с поддержкой PostgreSQL, MySQL, MariaDB, SQLite и MSSQL; предоставляет модели, миграции, ассоциации и гибкий API запросов. TypeORM — полностью написан на TypeScript, работает с MySQL, PostgreSQL, SQLite, MariaDB, Oracle, Microsoft SQL Server и MongoDB; поддерживает декораторы, репозитории, миграции и ленивую загрузку. Prisma — современный ORM/Query Builder, ориентированный на типизацию; совместим с PostgreSQL, MySQL, SQLite, SQL Server, MongoDB (preview); генерирует типизированный клиент, миграции и имеет удобный CLI. Objection.js — построен поверх Knex.js, предлагает модельный подход, поддерживает сложные запросы, транзакции и наследование, работает с любой SQL‑БД, поддерживает TypeScript. Mongoose — ODM для MongoDB, обеспечивает схемы, валидацию, middleware и плагины; широко используется в проектах с NoSQL. Bookshelf.js — надстройка над Knex, реализует модели и отношения (hasOne, hasMany, belongsToMany), поддерживает PostgreSQL, MySQL, SQLite. Waterline — ORM из Sails.js, поддерживает множество адаптеров (MongoDB, MySQL, PostgreSQL, Redis) и предоставляет единый API. MikroORM — TypeScript‑ориентированный ORM с поддержкой PostgreSQL, MySQL, MariaDB, SQLite и MongoDB; использует Data Mapper, поддерживает lazy/eager loading, миграции и репозитории. Knex.js — хотя это скорее query builder, часто используется в сочетании с лёгкими ORM‑подобными обёртками. Каждый из перечисленных инструментов имеет свои сильные и слабые стороны, выбор зависит от требований проекта: тип базы данных, необходимость строгой типизации, размер проекта и предпочтительный стиль работы с данными.
Перечисли, какие свойства отличают RESTful сервис.RESTful сервис характеризуется следующими признаками: использует протокол HTTP со стандартными методами (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS); оперирует ресурсами, идентифицируемыми уникальными URI; является статeless – сервер не хранит контекст запросов между вызовами; поддерживает полное использование кэширования (Cache‑Control, ETag и др.); отвечает с корректными статус‑кодами HTTP; передаёт представления ресурсов в форматах JSON, XML, HTML или других через Content‑Type; реализует концепцию гипермедиа как движка состояния приложения (HATEOAS), предоставляя ссылки на связанные ресурсы в ответах; использует согласованные версии API (например, в URL или заголовках).
Какие типы потоков (stream) ты знаешь?В Node.js существует пять основных классов потоков: **Readable** — поток, из которого можно только читать данные (например, `fs.createReadStream`); **Writable** — поток, в который можно только записывать данные (например, `fs.createWriteStream`); **Duplex** — комбинированный поток, поддерживающий и чтение, и запись (например, сетевые сокеты `net.Socket`); **Transform** — специализированный Duplex‑поток, который модифицирует проходящие через него данные (например, `zlib.createGzip`, `crypto.createCipheriv`); **PassThrough** — простой трансформ, который просто пропускает данные без изменений и часто используется для отладки или как промежуточный элемент в пайплайнах. Кроме того, все эти потоки могут работать в **object mode**, позволяя передавать любые JavaScript‑объекты вместо только Buffer/строк, а модуль `stream/promises` предоставляет промис‑ориентированные функции (`pipeline`, `finished`).
Назови известные тебе фреймворки для Node.js.Самыми популярными и часто используемыми фреймворками в экосистеме Node.js являются: **Express** – минималистичный и гибкий веб‑фреймворк, подходящий для создания REST‑API и веб‑приложений; **Koa** – современный, построенный создателями Express, использующий async/await и middleware‑цепочки; **NestJS** – прогрессивный фреймворк, основанный на TypeScript, вдохновлённый Angular, предоставляет модульную архитектуру и поддержку микросервисов; **Hapi** – мощный серверный фреймворк с богатой системой плагинов и валидации; **Fastify** – ориентирован на высокую производительность и низкое потребление памяти, поддерживает плагины и асинхронный код; **Sails.js** – MVC‑фреймворк, подходящий для создания реального времени приложений и API, построен над Express; **Meteor** – полный стек для быстрой разработки реального времени приложений с автоматической синхронизацией данных; **LoopBack** – фреймворк для построения API и микросервисов, генерирует модели из источников данных; **AdonisJS** – вдохновлённый Laravel, предоставляет готовую структуру, ORM и систему аутентификации. Каждый из этих фреймворков имеет свои сильные стороны: Express – простота и гибкость, Koa – современный подход, NestJS – масштабируемость и типизацию, Fastify – производительность, а остальные – специфические возможности (MVC, реальное время, генерация API). Выбор зависит от требований проекта, предпочтений команды и требуемой архитектуры.
В чём заключаются преимущества и недостатки использования GraphQL?**Плюсы:**
- **Гибкость запросов:** клиент запрашивает ровно те поля, которые ему нужны, что уменьшает объём передаваемых данных и ускоряет работу приложения.
- **Единая точка входа:** один эндпоинт заменяет множество REST‑маршрутов, упрощая инфраструктуру и версионирование API.
- **Самодокументируемость схемы:** типы, запросы и мутации описаны в SDL, что позволяет автоматически генерировать документацию и инструменты (GraphiQL, Apollo Explorer).
- **Типовая безопасность:** статическая типизация схемы позволяет проверять корректность запросов на этапе разработки и в рантайме.
- **Эффективная работа с отношениями:** возможность вложенных запросов и фрагментов упрощает получение связанных данных без «N+1» проблем (при правильной реализации DataLoader).
- **Кеширование на клиенте:** клиентские библиотеки (Apollo Client, Relay) умеют кешировать результаты запросов, снижая нагрузку на сервер.

**Минусы:**
- **Сложность реализации сервера:** требуется построить схему, резолверы и часто интегрировать DataLoader для предотвращения N+1 запросов, что повышает порог входа.
- **Перегрузка запросов:** клиент может сформировать слишком «тяжёлый» запрос, нагрузив сервер, если нет ограничений на глубину/сложность.
- **Отсутствие кэширования на уровне HTTP:** из‑за единого эндпоинта традиционные механизмы HTTP‑кеширования (ETag, Last‑Modified) не работают, требуется кастомное кеширование.
- **Трудности с мониторингом и логированием:** запросы могут быть сильно динамичными, что усложняет трассировку и сбор метрик.
- **Проблемы с загрузкой больших файлов:** GraphQL не оптимален для передачи бинарных данных; обычно используют отдельные REST‑эндпоинты или спецификацию GraphQL‑Multipart.
- **Не всегда подходит для простых CRUD‑операций:** для небольших сервисов накладные расходы могут превысить выгоды.
В чём заключаются преимущества и недостатки микросервисной архитектуры?Микросервисная архитектура предлагает ряд преимуществ: гибкая масштабируемость (каждый сервис можно масштабировать независимо в зависимости от нагрузки), изоляция отказов (сбой одного сервиса не приводит к падению всей системы), технологическая независимость (каждый микросервис может быть реализован на наиболее подходящем стеке, в том числе Node.js, Go, Python и др.), быстрая доставка (независимые релизы позволяют выпускать новые функции без координации с другими командами), улучшенная поддерживаемость (меньший кодовый базис упрощает понимание и тестирование). Однако у подхода есть и недостатки: операционная сложность (необходимо управлять большим количеством сервисов, оркестрацией, сетевыми взаимодействиями и конфигурациями), повышенные требования к инфраструктуре (необходимы инструменты для контейнеризации, сервисного обнаружения, мониторинга и логирования), усложнённое тестирование (интеграционные тесты становятся более ресурсоёмкими), увеличенные задержки (сетевые вызовы между сервисами могут добавить латентность), риски согласованности данных (транзакции распределённые, требуют дополнительных паттернов, таких как SAGA), затраты на DevOps (команды должны обладать навыками CI/CD, Kubernetes и т.п.).
В каких ситуациях в Node.js применяется значение undefined?`undefined` появляется, когда переменная объявлена, но ей не присвоено значение; функция вызывается без оператора `return` и тем самым возвращает `undefined`; у объекта запрашивается несуществующее свойство; в массиве обращаются к индексу, для которого нет элемента; функция вызывается без аргумента, поэтому соответствующий параметр равен `undefined`; оператор `delete` удаляет свойство, после чего его значение становится `undefined`; при попытке получить значение переменной, объявленной с `let`/`const` до её инициализации (ReferenceError) или в случае неявного приведения типов, когда результатом операции является `undefined`. `undefined` также используется в проверках (`value === undefined`, `typeof value === 'undefined'`) для определения отсутствия данных, отличного от `null`, который обозначает намеренно пустое значение.
Почему PostgreSQL может решить не использовать индекс при поиске по полюPostgreSQL выбирает план выполнения на основе оценки стоимости. Отказ от использования индекса может происходить по разным причинам: низкая селективность условия (большая часть строк удовлетворяет предикату), небольшое количество строк в таблице, устаревшие статистики (не обновлены `ANALYZE`), несовпадение типов (неявное приведение, которое не поддерживается индексом), использование функций или выражений над колонкой без соответствующего expression‑индекса, отсутствие подходящего operator class для используемого оператора, наличие частичного индекса, который не покрывает все строки, отключённый или помеченный как `invalid` индекс, а также ситуация, когда стоимость последовательного сканирования ниже стоимости индексного доступа из‑за высокой стоимости случайных I/O или плохой корреляции данных с порядком индекса. В таких случаях планировщик выбирает seq‑scan или другой доступный метод.
Почему требуется, чтобы некоторые HTTP‑методы были идемпотентными?Идемпотентность означает, что многократное выполнение одного и того же запроса приводит к тому же состоянию системы, что и единичное выполнение. Для HTTP‑методов это критично, потому что:
- клиент и промежуточные прокси могут повторять запросы при сетевых сбоях, тайм‑аутах или при использовании механизма «retry», и сервер должен корректно обработать такие повторы без создания дублирующих ресурсов или неожиданных побочных эффектов;
- кэширующие и маршрутизирующие устройства используют идемпотентность для безопасного кэширования и переадресации запросов, полагаясь на то, что повторный запрос не изменит состояние;
- REST‑архитектура полагается на предсказуемое поведение методов: GET, HEAD, OPTIONS, TRACE – безопасные (не изменяют состояние), а PUT, DELETE, PATCH – должны быть идемпотентными (PUT заменяет ресурс целиком, DELETE удаляет его независимо от количества вызовов). Это упрощает построение масштабируемых, отказоустойчивых систем и облегчает тестирование;
- в Node‑среде, где часто используются библиотеки для автоматических повторов запросов (например, axios‑retry), соблюдение идемпотентности предотвращает появление гонок и утечек данных.
Продемонстрируй пример применения принципа открытости/закрытости в Node.js.`class Shape{area(){throw new Error('Not implemented');}}class Rectangle extends Shape{constructor(w,h){super();this.w=w;this.h=h;}area(){return this.w*this.h;}}class Circle extends Shape{constructor(r){super();this.r=r;}area(){return Math.PI*this.r*this.r;}}function totalArea(shapes){return shapes.reduce((s,sh)=>s+sh.area(),0);}const shapes=[new Rectangle(5,10),new Circle(3)];console.log(totalArea(shapes));` – базовый класс Shape открыт для расширения (можно добавлять новые фигуры), но закрыт для изменения существующего кода расчёта площади.
Что обозначает буква O в наборе принципов SOLID?Буква O относится к принципу Open/Closed Principle (OCP) – «принцип открытости/закрытости». Он гласит, что программные сущности (классы, модули, функции) должны быть открыты для расширения, но закрыты для модификации. То есть поведение существующего кода должно быть возможно менять путем добавления нового кода, а не изменения уже проверенного. В Node.js это достигается, например, через наследование, композицию, использование интерфейсов (TypeScript) или паттернов «Стратегия», «Декоратор». При проектировании следует выделять абстракции (абстрактные классы, интерфейсы) и реализовывать их в конкретных классах, чтобы новые требования реализовывались новым классом, а старый код оставался неизменным. Пример: базовый класс `Logger` с методом `log(message)`. Для добавления логирования в файл создаём класс `FileLogger`, реализующий тот же интерфейс, не меняя `Logger`. Это упрощает тестирование, повышает стабильность и уменьшает риск регрессий. Нарушение OCP проявляется, когда для нового поведения приходится править уже существующие функции, что приводит к частым багам и сложному рефакторингу.
Что такое Generics?Generics — это механизм статической типизации, позволяющий писать переиспользуемый и типобезопасный код, где типы данных задаются как параметры при объявлении функций, классов, интерфейсов или типов. В JavaScript типы существуют только во время разработки, поэтому Generics реализованы в TypeScript, который часто используется в проектах Node.js. При объявлении generic‑типа используется синтаксис `<T>` (или нескольких параметров `<T, K>`), где `T` — плейсхолдер для реального типа, который будет подставлен при вызове.

**Ключевые возможности Generics:**
1. **Типовая параметризация** — позволяет функции принимать аргументы любого типа, сохраняя при этом связь между входными и выходными типами.
2. **Повышение типобезопасности** — компилятор проверяет корректность использования типов, предотвращая ошибки, такие как «undefined is not a function».
3. **Переиспользуемость кода** — один generic‑алгоритм может работать с массивами, объектами, промисами и другими структурами без дублирования.
4. **Ограничения (constraints)** — через `extends` можно ограничить типы, требуя наличие определённых свойств (например, `function sort<T extends { length: number }>(arr: T): T`).
5. **Умные выводы типов** — TypeScript автоматически выводит типы из аргументов, так что часто указывать типы явно не требуется.
6. **Множественные параметры и значения по умолчанию** — можно объявлять несколько типовых параметров и задавать им значения по умолчанию (`type Pair<T = number, U = string> = [T, U];`).

**Примеры в Node.js/TypeScript:**

```ts
// Generic функция для получения первого элемента массива
function first<T>(items: T[]): T {
return items[0];
}
const num = first([1, 2, 3]); // тип num → number
const str = first(['a', 'b']); // тип str → string

// Generic класс для работы с коллекцией
class Repository<T> {
private items: T[] = [];
add(item: T) { this.items.push(item); }
getAll(): T[] { return this.items; }
}
const userRepo = new Repository<User>();
userRepo.add({ id: 1, name: 'Alice' });
```

**Ограничения и продвинутые возможности:**
- **Conditional Types** позволяют создавать типы, зависящие от условий (`type IsString<T> = T extends string ? true : false;`).
- **Mapped Types** и **Utility Types** (`Partial<T>`, `Readonly<T>`) часто комбинируются с generics для построения гибких API.
- **Infer** в условных типах позволяет извлекать типы из структур (`type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;`).

**Практические рекомендации:**
- Используйте generics, когда функция или класс оперирует с данными, тип которых неизвестен заранее, но должен сохраняться.
- По возможности задавайте ограничения, чтобы получать автодополнение и ранние ошибки.
- Не переусердствуйте: избыточные generic‑параметры могут усложнить читаемость кода.

Таким образом, Generics — это фундаментальный инструмент типизации в TypeScript, который делает код в Node.js более надёжным, читаемым и масштабируемым.
Как можно описать HTTP‑протокол с точки зрения фронтенд‑разработки?HTTP (Hypertext Transfer Protocol) — это протокол прикладного уровня, который используется браузером для обмена данными с сервером. С фронтенда он проявляется через отправку запросов (GET, POST, PUT, DELETE, PATCH, OPTIONS и др.) с помощью средств браузера (Fetch API, XMLHttpRequest, WebSockets, <img>, <script> и т.п.) и получение ответов, содержащих статусный код (200, 404, 500 и др.), заголовки (Content-Type, Cache-Control, Set-Cookie, CORS‑заголовки) и тело (HTML, JSON, XML, бинарные данные).

Важные аспекты для фронтенда:
- **Методы и тело запроса** – выбор метода в зависимости от операции, сериализация данных (JSON, FormData, URL‑encoded).
- **Статусы и обработка ошибок** – проверка `response.ok`, работа с 4xx/5xx ошибками.
- **Заголовки** – управление типом содержимого (`Content-Type`), авторизацией (`Authorization`), кэшированием (`Cache-Control`), политикой CORS (`Access-Control-Allow-Origin`).
- **Куки и токены** – передача аутентификационных данных через `Cookie` (с HttpOnly, Secure) или `Authorization`‑заголовок, управление `credentials` в `fetch`.
- **Кеширование** – использование ETag, Last-Modified, `Cache-Control` и Service Workers для офлайн‑режима.
- **Безопасность** – HTTPS, защита от XSS/CSRF, политика SameSite для кук, Content Security Policy.
- **Асинхронность** – промисы, async/await, обработка потоков данных (ReadableStream).
- **Трассировка и отладка** – инструменты DevTools (Network tab), просмотр заголовков, таймингов, размеров payload.

Таким образом, HTTP — это фундаментальный механизм, через который фронтенд получает ресурсы, взаимодействует с API и реализует пользовательский опыт.
В чём заключается принцип подстановки Лисков?Принцип подстановки Лисков (Liskov Substitution Principle, LSP) — один из пяти принципов SOLID, сформулированный Барбарой Лисков в 1987 году; он гласит, что объекты программных компонентов должны быть заменяемы их экземплярами подклассов без изменения желаемых свойств программы, то есть любой наследник должен полностью сохранять поведение базового класса, не усиливать пред‑условия, не ослаблять пост‑условия, сохранять сигнатуру и тип возвращаемого значения, не бросать неожиданные исключения и не нарушать инварианты базового класса; в Node/JavaScript это достигается через корректное переопределение методов в классах, использование типовых аннотаций (TypeScript, JSDoc) для описания контрактов и написание unit‑тестов, проверяющих, что подклассы проходят те же тесты, что и базовый класс, а также предпочтение композиции вместо наследования при нарушении LSP.
Что представляет собой Redux и для чего он используется?Redux — это предсказуемый контейнер состояния для JavaScript‑приложений, часто используемый в связке с React, но полностью независимый от UI‑библиотек и подходящий для Node.js. Он основан на трёх фундаментальных принципах: (1) единый источник правды — всё состояние приложения хранится в одном объекте — store; (2) состояние доступно только для чтения — единственный способ изменить состояние — отправка (dispatch) действий (actions), представляющих собой простые JavaScript‑объекты с полем type и необязательными данными; (3) изменения описываются чистыми функциями‑редьюсерами, которые принимают текущее состояние и действие и возвращают новое состояние без мутаций. Благодаря неизменяемости (immutability) каждый переход состояния легко отслеживается, что упрощает отладку, тестирование и внедрение функций вроде time‑travel debugging.

Для асинхронных операций в Redux используют middleware — промежуточные слои между dispatch и reducer, такие как redux‑thunk, redux‑saga или redux‑observable, позволяющие запускать побочные эффекты, работать с API и управлять последовательностью действий. Store создаётся через функцию `createStore`, в которую передаётся корневой редьюсер, начальное состояние и опциональные enhancers (например, `applyMiddleware`). Подписка на изменения осуществляется методом `store.subscribe`, а текущее состояние читается через `store.getState()`.

Redux можно применять как в клиентском, так и в серверном коде Node.js: сохранять состояние между запросами, реализовывать кеширование, управлять бизнес‑логикой микросервисов или совместно использовать один store между сервером и клиентом (SSR). При этом важно помнить о потенциальных недостатках: избыточная бюрократия при небольших проектах, необходимость писать много шаблонного кода и поддерживать строгую иммутабельность, что может замедлять разработку без соответствующих утилит (например, Immer).

В итоге Redux предоставляет централизованное, предсказуемое и расширяемое управление состоянием, облегчая масштабирование приложений, упрощая тестирование и делая процесс отладки более прозрачным.
Что представляет собой протокол TCP?TCP (Transmission Control Protocol) — это транспортный протокол уровня 4 модели OSI, обеспечивающий надёжную, упорядоченную и потоковую доставку данных между конечными узлами сети. Он работает поверх IP (Internet Protocol) и реализует соединительно‑ориентированный обмен: перед передачей данных устанавливается виртуальное соединение с помощью трёхстороннего рукопожатия (SYN, SYN‑ACK, ACK). TCP гарантирует доставку всех сегментов, используя подтверждения (ACK), таймеры повторной передачи при потере пакетов и контроль целостности через контрольную сумму. Для управления скоростью передачи применяются механизмы скользящего окна (flow control) и алгоритмы контроля перегрузки (congestion control), такие как slow start, congestion avoidance, fast retransmit и fast recovery. Портовая нумерация позволяет множеству приложений использовать один IP‑адрес одновременно. В Node.js для работы с TCP используется модуль `net`, предоставляющий API для создания серверов (`net.createServer`) и клиентских сокетов (`net.Socket`).
Как работает метод apply в JavaScript и для чего он используется?Метод apply — это встроенная функция `Function.prototype.apply`, позволяющая вызвать любую функцию, явно задав значение `this` и передав список аргументов в виде массива (или массивоподобного объекта). Синтаксис: `func.apply(thisArg, argsArray)`. При вызове `func` внутри неё `this` будет равно `thisArg`, а аргументы будут развёрнуты из `argsArray`. `apply` часто используют, когда набор аргументов известен только во время выполнения (например, массив пользовательских данных) или для передачи контекста в функции высшего порядка. Отличие от `call` в том, что `call` принимает список аргументов через запятую, а `apply` — один массив. Пример: `Math.max.apply(null, [3, 1, 4])` возвращает 4, а `Array.prototype.push.apply(arr, [5,6])` добавит элементы в массив `arr`. `apply` также применяется для имитации наследования (вызов конструктора родителя) и для создания «частично применённых» функций совместно с `bind`. Начиная с ES6, аналогичные задачи решаются оператором распространения: `func.call(thisArg, ...argsArray)`.
Как определяется объектно‑ориентированное программирование в контексте парадигм программирования?Объектно‑ориентированное программирование (ООП) — это парадигма, основанная на моделировании реального мира через объекты, которые инкапсулируют состояние (данные) и поведение (методы). Ключевые принципы ООП: инкапсуляция (скрытие внутренней реализации и предоставление публичного интерфейса), наследование (создание новых классов на основе существующих, переиспользование кода), полиморфизм (возможность использовать единый интерфейс для разных реализаций) и абстракция (выделение существенных характеристик и игнорирование деталей). ООП часто сравнивают с другими парадигмами: императивной (фокус на последовательных инструкциях), процедурной (разбиение программы на процедуры), функциональной (неизменяемость данных, функции как граждане первого класса) и логической (описание фактов и правил вывода). В контексте JavaScript/Node.js ООП реализуется через прототипное наследование, классы (ES6) и функции‑конструкторы, позволяя комбинировать объектно‑ориентированный и функциональный стили. ООП служит для повышения модульности, повторного использования кода, облегчения поддержки и масштабирования систем, а также для более естественного отображения предметных областей в программных моделях.
Как определяется агрегатная функция?Агррегатная функция — это специальный тип функции, применяемой к набору значений столбца (или к результату подзапроса) и возвращающей одно сводное значение, которое характеризует весь набор. Ключевая идея — «свести» множество строк к единому результату. В реляционных СУБД к агрегатным функциям относятся `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, `STRING_AGG`, `ARRAY_AGG` и др. Они обычно используются в операторе `SELECT` совместно с `GROUP BY`, позволяя группировать строки по одним полям и вычислять агрегаты по другим.

Примеры в SQL:
```sql
SELECT department_id, COUNT(*) AS employees,
AVG(salary) AS avg_salary,
MAX(hire_date) AS latest_hire
FROM employees
GROUP BY department_id;
```
В MongoDB агрегатные операции реализованы через pipeline‑операторы `$group`, `$sum`, `$avg`, `$min`, `$max`, `$push` и др.:
```js
db.employees.aggregate([
{ $group: { _id: "$department_id",
employees: { $sum: 1 },
avgSalary: { $avg: "$salary" },
latestHire: { $max: "$hire_date" } } }
]);
```

В Node‑приложениях агрегатные функции часто вызываются через драйверы БД (например, `pg` для PostgreSQL, `mongoose` для MongoDB) или реализуются в коде над массивами:
```js
const salaries = employees.map(e => e.salary);
const total = salaries.reduce((a,b) => a + b, 0);
const avg = total / salaries.length;
```

Важно помнить особенности:
- `COUNT(*)` считает все строки, `COUNT(column)` — только непустые значения.
- При использовании `GROUP BY` каждое поле в `SELECT`, не входящее в агрегат, должно быть включено в список группировки.
- Агрегатные функции могут быть вложенными (например, `AVG(SUM(col))` в подзапросе).
- В некоторых СУБД доступны оконные функции (`OVER`), которые позволяют вычислять агрегаты без группировки, сохраняя оригинальные строки.

Таким образом, агрегатные функции позволяют получать статистические, суммирующие и иные сводные данные из больших наборов записей, что является фундаментальной задачей при работе с базами данных и аналитикой.
Как определяется внешний ключ и в чём его назначение?Внешний ключ — это ограничение целостности в реляционных базах данных, которое связывает столбец (или набор столбцов) одной таблицы со столбцом(ами) первичного ключа другой таблицы. Он гарантирует, что значение в поле‑внешний ключ существует в родительской таблице, тем самым обеспечивая ссылочную целостность. При попытке вставить или обновить запись со значением, отсутствующим в связанной таблице, СУБД выдаст ошибку. Внешний ключ позволяет реализовать отношения «один‑ко‑многим», «один‑к‑одному» и «многие‑ко‑многим» (через промежуточные таблицы). При определении можно указать действия при удалении или обновлении связанных записей: `ON DELETE CASCADE`, `ON DELETE SET NULL`, `ON UPDATE RESTRICT` и др. Пример в SQL:
```sql
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER,
CONSTRAINT fk_user FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE SET NULL
);
```
В Node.js при работе с ORM (Sequelize, TypeORM, Knex) внешние ключи задаются в моделях/схемах и автоматически создаются при миграциях. Их наличие упрощает запросы с `JOIN`, позволяет использовать каскадные операции и повышает надёжность данных.
Как определяется множественное наследование в TypeScript?В TypeScript класс может наследовать только один базовый класс, поэтому традиционное множественное наследование, как в C++ или Python, недоступно. Тем не менее, аналогичную функциональность можно реализовать с помощью интерфейсов и миксинов. Интерфейсы позволяют описать набор свойств и методов, которые класс обязан реализовать, и один класс может реализовывать произвольное количество интерфейсов, тем самым «наследуя» их сигнатуры. Для реального объединения реализации используют паттерн миксинов: создают функции‑фабрики, принимающие базовый класс и возвращающие новый класс, в котором добавлены методы и свойства миксина; последовательно применяя такие функции, получаем класс, комбинирующий несколько поведений. Пример с интерфейсами: `interface Flyable { fly(): void; } interface Swimmable { swim(): void; } class Duck implements Flyable, Swimmable { fly(){/*...*/} swim(){/*...*/} }`. Пример миксина: `type Constructor<T = {}> = new (...args:any[])=>T; function Jumpable<TBase extends Constructor>(Base:TBase){return class extends Base{ jump(){/*...*/} } } class Animal {} const JumpingAnimal = Jumpable(Animal);`. Ограничения: свойства с одинаковыми именами в разных миксинах конфликтуют и требуют ручного разрешения; типизация сохраняет только то, что явно объявлено в результирующем классе или в его интерфейсах; при использовании `implements` проверка происходит на этапе компиляции, а миксины работают в рантайме после трансляции в JavaScript. Таким образом, в TypeScript множественное наследование реализуется косвенно через интерфейсы и миксины, а не через прямое наследование от нескольких классов.
Как определяется и для чего применяется первичный ключ в базе данных?Первальный (первичный) ключ — это набор одного или нескольких столбцов таблицы, значения которых однозначно идентифицируют каждую запись. Он обязан быть уникальным и не может принимать NULL. При объявлении первичного ключа СУБД автоматически создаёт уникальный индекс, обеспечивая быстрый поиск и поддерживая целостность данных. Ключ может быть простым (один столбец) или составным (несколько столбцов). В большинстве систем рекомендуется использовать отдельный суррогатный ключ (например, автоинкрементный INTEGER) вместо естественного, чтобы избежать изменения значений и обеспечить стабильность ссылок. Первичный ключ часто используется в качестве ссылки для внешних ключей в других таблицах, тем самым формируя отношения между данными. При проектировании важно выбирать минимальный набор столбцов, которые гарантируют уникальность, и избегать включения изменяющихся или больших по размеру полей.
Что подразумевается под перегрузкой функций?Перегрузка функций — это возможность объявлять несколько функций с одинаковым именем, но разным набором параметров (количеством, типом или порядком). В языках со статической типизацией (C++, Java) компилятор выбирает нужную версию на основе сигнатуры вызова. В JavaScript/Node.js традиционной перегрузки нет, потому что язык динамически типизирован и функции могут принимать произвольное число аргументов. Тем не менее, имитацию перегрузки реализуют различными способами: проверка `arguments.length`, использование параметров со значениями по умолчанию, оператор `...rest` для сбора произвольного количества аргументов, а также явная проверка типов внутри функции (`typeof`, `Array.isArray`, `instanceof`). При этом одна функция может вести себя по‑разному в зависимости от переданных данных. В TypeScript можно объявлять несколько сигнатур функции (overload signatures) и одну реализацию, что даёт статическую проверку и автодополнение, но в скомпилированный JavaScript остаётся одна реализация, где обычно применяется та же логика проверки аргументов. Перегрузка упрощает API, делая его более выразительным и гибким, однако требует тщательной валидации входных данных, чтобы избежать неоднозначностей и ошибок во время выполнения.
Что представляет собой фреймворк Express и для чего он применяется?Express — это минималистичный и гибкий веб‑фреймворк для Node.js, предоставляющий набор функций для создания веб‑приложений и API. Он упрощает работу с HTTP‑запросами и ответами, реализует маршрутизацию (router), поддерживает middleware‑цепочки, позволяет задавать параметры маршрутов, обрабатывать статические файлы, управлять ошибками и выполнять запросы к шаблонизаторам. Express построен на базовом модуле http Node и использует концепцию middleware, где каждый обработчик получает объекты req, res и next, передавая управление следующему слою. Фреймворк поддерживает асинхронные операции, интеграцию с базами данных, аутентификацию, валидацию и другие сторонние пакеты через npm (например, body‑parser, cors, helmet). Благодаря своей лёгкой структуре Express часто используют как основу для микросервисов, REST‑API, одностраничных приложений (SPA) и серверных рендерингов, а также в комбинации с другими библиотеками (например, Mongoose, Passport, Socket.io). Он имеет большую и активную сообщественную поддержку, обширную документацию и множество готовых шаблонов (express‑generator), что ускоряет старт разработки.
Что такое чистая архитектура?Чистая архитектура — это набор принципов и рекомендаций по организации кода так, чтобы бизнес‑логика приложения была полностью независима от внешних факторов (фреймворков, UI, баз данных, сетевых протоколов и т.п.). Основные идеи: разделение системы на слои (entities → use‑cases → interface adapters → frameworks/drivers), соблюдение правила зависимостей (внутренние слои не знают о внешних), применение SOLID‑принципов, инверсия зависимостей через абстракции (интерфейсы, репозитории). В Node.js это обычно реализуется через отдельные каталоги / модули для доменных сущностей, сервисов/use‑case, адаптеров (контроллеры, репозитории) и инфраструктурных слоёв (Express, ORM, внешние API). Такой подход упрощает тестирование, повышает гибкость и масштабируемость, позволяет менять детали реализации без затрагивания бизнес‑логики.
Является ли HTTP‑метод PUT идемпотентным?PUT считается идемпотентным: повторные запросы с тем же URI и тем же телом должны приводить к одному и тому же состоянию ресурса, хотя сервер может вернуть разные коды статуса. Идемпотентность определяется тем, что результат (состояние ресурса) не меняется при многократных одинаковых вызовах, даже если побочные эффекты (логирование, счётчики) могут происходить. Согласно RFC 7231, PUT — идемпотентный метод, в отличие от POST, который не гарантирует идемпотентность. На практике важно, чтобы сервер не генерировал новые идентификаторы при каждом запросе PUT и не изменял ресурс иначе, чем описано в запросе. Пример: `PUT /users/123 { "name": "Alice" }` — повторный запрос с тем же телом оставит пользователя 123 с именем Alice без изменений; если сервер вместо обновления создаёт новый ресурс при каждом запросе, идемпотентность нарушается.
Что представляет собой Express в экосистеме Node.js?Express — это минималистичный, гибкий и широко используемый веб‑фреймворк для Node.js, предоставляющий набор функций для построения одностраничных, мультистраничных и гибридных веб‑приложений и API. Он работает как тонкий слой над встроенным HTTP‑модулем Node, упрощая обработку запросов и ответов, маршрутизацию, работу с параметрами URL, статическими файлами и шаблонизаторами. Express поддерживает концепцию middleware — функции, которые последовательно обрабатывают запрос, могут изменять объект req/res, выполнять проверку аутентификации, парсинг тела запроса (body‑parser), логирование, обработку ошибок и т.д. Установка производится одной командой `npm install express`. После подключения фреймворка создаётся объект приложения `const app = express();`, где через методы `app.get()`, `app.post()`, `app.put()`, `app.delete()` и др. задаются маршруты. Express совместим с широким набором шаблонизаторов (Pug, EJS, Handlebars) и поддерживает интеграцию с базами данных, системами аутентификации (Passport), валидацией (express-validator) и другими библиотеками. Благодаря своей простоте и модульности Express стал базой для множества крупных проектов и сервисов, а также основой для более «тяжёлых» фреймворков, таких как NestJS.
Может ли один процесс определять, какие действия выполняет другой процесс?В современных операционных системах процессы изолированы друг от друга: каждый имеет собственное адресное пространство и не имеет прямого доступа к состоянию другого процесса. Поэтому без специального механизма процесс не «знает», что делает другой процесс. Для обмена информацией используются средства межпроцессного взаимодействия (IPC) — каналы, сокеты, очереди сообщений, общая память, сигналы и т.п. Кроме того, доступ к информации о чужих процессах регулируется правами доступа и политиками безопасности ОС; обычный пользовательский процесс может получить лишь ограниченные сведения (например, список процессов через `ps` или API `process` в Node.js, если имеет соответствующие разрешения). Если требуется наблюдать за действиями другого процесса, необходимо либо использовать предоставленные ОС API (например, `ptrace` в Linux, Windows Debugger API) при наличии привилегий, либо реализовать собственный протокол обмена данными между процессами. Таким образом, без явного IPC или привилегированного доступа один процесс не может знать, что делает другой процесс.
Когда предпочтительнее применять массив?Массивы удобны, если требуется хранить упорядоченную последовательность однотипных элементов с быстрым доступом по индексу, когда известен или приблизительно известен размер коллекции, при необходимости часто выполнять операции перебора (for, forEach, map, filter), сортировки, срезы или работать с данными, полученными из внешних источников (например, результаты запросов к базе, файлы CSV). В Node.js массивы оптимальны для сценариев, где важна плотность памяти и минимальные накладные расходы на хранение (по сравнению с объектами или Map), а также когда нужен простой способ собрать результаты асинхронных операций (Promise.all возвращает массив). При работе с большими числовыми данными стоит рассмотреть TypedArray (Uint8Array, Float64Array) для повышения производительности и экономии памяти. Массивы менее подходят, если требуется быстрый поиск по ключу, уникальность элементов без дублирования, или когда элементы имеют разные типы и часто требуется доступ по произвольному ключу — в таких случаях лучше использовать объект, Map или Set.
В чем преимущества и недостатки использования базы данных в контейнере?Плюсы: высокая портативность – образ базы можно запустить на любой машине с Docker/OCI‑runtime; изоляция от хост‑системы и других сервисов упрощает управление зависимостями и версии СУБД; облегчённый процесс развертывания и масштабирования в оркестраторах (Kubernetes, Docker‑Compose) благодаря декларативному описанию; возможность быстрого восстановления среды разработки и тестов через pull готового образа; единообразие конфигураций между разработкой, тестированием и продакшеном; автоматизация резервного копирования и миграций через контейнерные тома; гибкое ограничение ресурсов (CPU, RAM) через cgroup.
Минусы: сложность организации надёжного постоянного хранилища – требуется монтировать внешние тома или использовать сетевые хранилища, иначе данные теряются при пересоздании контейнера; возможные потери производительности из‑за дополнительного уровня абстракции и наложения файловой системы overlay/volume; ограниченная поддержка специфических функций СУБД, требующих низкоуровневого доступа к сети или дискам (например, репликация на уровне кластера, SAN); необходимость управления жизненным циклом контейнера (обновления, миграции) в дополнение к самой СУБД; потенциальные проблемы с безопасностью, если контейнеры работают с привилегиями или используют общие сети; сложность мониторинга и отладки в среде микросервисов, где база «прячется» за несколькими уровнями оркестрации.
Какие плюсы и минусы у оператора in в JavaScript/Node.js?Оператор in проверяет, существует ли указанный ключ (строка или Symbol) в объекте либо в его цепочке прототипов. **Плюсы:** простая синтаксическая запись; работает с объектами, массивами и строками; позволяет быстро проверить наличие свойства, включая унаследованные; удобно в условных выражениях; используется в конструкции `for…in` для перебора перечисляемых свойств. **Минусы:** проверка проходит по всей цепочке прототипов, поэтому может вернуть `true` для свойств, унаследованных от `Object.prototype`, что часто приводит к ошибкам; в `for…in` перечисляются также наследуемые перечисляемые свойства, поэтому обычно требуется фильтрация через `hasOwnProperty`; оператор не учитывает свойства с ключами‑Symbol, если их явно не указать; для массивов `in` проверяет наличие индекса, а не наличие значения, поэтому для проверки присутствия элемента следует использовать `Array.prototype.includes`; в некоторых случаях проверка через `in` медленнее, чем методы `Object.hasOwn` (ES2022) или `Object.prototype.hasOwnProperty`.
Чем отличается работа с базой данных при использовании ORM по сравнению с написанием нативного SQL?ORM (Object‑Relational Mapping) предоставляет уровень абстракции, преобразуя объекты JavaScript в строки таблиц и наоборот, генерируя SQL‑запросы автоматически, управляя связями, миграциями и кэшем; это ускоряет разработку, уменьшает количество ручного кода и повышает переносимость, но может скрывать детали выполнения, приводить к менее оптимизированным запросам и усложнять отладку. Нативный SQL требует ручного написания запросов, даёт полный контроль над синтаксисом, оптимизацией (индексы, планы выполнения), позволяет использовать специфичные возможности СУБД (CTE, window‑функции, хранимые процедуры), но увеличивает объём кода, повышает риск ошибок и требует более глубоких знаний SQL и схемы БД. Выбор между ними зависит от требований к производительности, сложности бизнес‑логики, скорости разработки и опыта команды.
В чём заключаются различия между бизнес‑моделью и моделью сущностей?Бизнес‑модель (Business Model) описывает бизнес‑логику приложения: правила, процессы, сценарии использования, валидацию и вычисления, обычно реализуется в сервисах/доменных сервисах и не зависит от способа хранения данных. Модель сущностей (Entity Model) представляет структуру данных, их атрибуты и связи, отражает схему базы данных и часто реализуется через ORM‑модели (например, Sequelize, TypeORM). Таким образом, бизнес‑модель оперирует абстракциями уровня домена и отвечает за «что делать», а модель сущностей — за «как хранить». В типичной архитектуре слои взаимосвязаны: бизнес‑модель использует репозитории, которые преобразуют сущности в доменные объекты и обратно. Пример в Node.js: класс `UserService` (бизнес‑логика) работает с объектом `UserEntity` (ORM‑модель), где `UserEntity` описывает таблицу `users`, а `UserService` реализует методы `register`, `login`, включающие проверку пароля, отправку email и т.п.
В чём различаются модули Cluster и Child Process в Node.js?Модуль **Child Process** позволяет создавать отдельные процессы ОС (spawn, fork, exec, execFile) и управлять ими, предоставляя каналы ввода‑вывода (stdin, stdout, stderr) и возможность обмена данными через IPC‑каналы при использовании fork. Каждый дочерний процесс полностью изолирован, имеет собственный V8‑инстанс и собственный набор ресурсов, поэтому используется для выполнения тяжёлых вычислений, запуска внешних программ или изоляции кода.

Модуль **Cluster** построен поверх Child Process и предназначен специально для масштабирования Node‑серверов на многоядерных процессорах. Он автоматически форкает воркеры‑процессы (по одному на каждое ядро) и распределяет входящие сетевые соединения между ними через встроенный балансировщик (на уровне операционной системы). Воркеры работают в том же коде, что и мастер‑процесс, могут обмениваться сообщениями через IPC и совместно использовать серверные сокеты, что упрощает горизонтальное масштабирование без необходимости вручную управлять процессами.

Основные различия:
- **Назначение**: Child Process — общий API для создания любых дочерних процессов; Cluster — специализированный инструмент для многопроцессного масштабирования серверных приложений.
- **Балансировка нагрузки**: в Cluster есть встроенный распределитель соединений, в Child Process её нет.
- **Общая логика**: в Cluster мастер‑процесс отвечает за форк воркеров и их мониторинг (перезапуск при падении), тогда как при использовании Child Process программист управляет каждым процессом вручную.
- **IPC**: обе технологии поддерживают IPC, но в Cluster IPC автоматически настроен между мастером и воркерами; в Child Process IPC доступен только при fork‑методе и требует явной настройки.
- **Ресурсы**: каждый воркер в Cluster имеет отдельный V8‑инстанс, но они могут совместно слушать один и тот же порт; в Child Process процессы полностью независимы и обычно используют разные порты или задачи.

Таким образом, выбирайте **Child Process**, когда нужен отдельный процесс для выполнения произвольных задач или запуска внешних программ, и **Cluster**, когда требуется масштабировать HTTP/HTTPS‑серверы или другие сетевые сервисы на несколько ядер без ручного распределения нагрузки.
В чём заключается различие между декоратором и обычной функцией в Node.js?Функция — это самостоятельный вызываемый объект, который принимает аргументы и возвращает значение; декоратор — это специальный тип функции‑обёртки (higher‑order function), применяемый к классам, методам или свойствам через синтаксис @, позволяющий изменить или расширить их поведение без изменения исходного кода, часто используется для метапрограммирования, внедрения зависимостей, кэширования и т.п.; в отличие от обычных функций, декоратор получает в качестве параметра целевой элемент (конструктор, метод или дескриптор) и должен вернуть модифицированный элемент или его дескриптор, при этом его выполнение происходит в момент определения класса, а не при вызове, что отличает время жизни и область применения.
В чём различия между оператором HAVING и оператором WHERE в SQL?WHERE отбирает отдельные строки до группировки и не может использовать агрегатные функции, тогда как HAVING фильтрует уже сформированные группы после применения агрегатов и может оперировать функциями типа SUM, COUNT; порядок выполнения: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY; пример: SELECT department, COUNT(*) FROM employees WHERE salary > 50000 GROUP BY department HAVING COUNT(*) > 5; здесь WHERE выбирает сотрудников с зарплатой выше 50000, а HAVING оставляет только отделы, где их количество превышает 5.
В чём различаются понятия Inversion of Control, Dependency Injection и Dependency Inversion?Inversion of Control (IoC) — принцип, согласно которому управление зависимостями и жизненным циклом объектов передаётся внешнему контейнеру или фреймворку, а не создаётся внутри самого объекта. Это «обратное» управление: вместо того, чтобы объект сам запрашивал свои зависимости, их «внедряют» в него извне. Dependency Injection (DI) — конкретный способ реализации IoC, при котором зависимости передаются объекту через конструктор, свойства (setter) или методы (interface injection). DI‑контейнер в Node‑среде (например, InversifyJS, Awilix) сканирует конфигурацию, создаёт экземпляры и автоматически подставляет нужные зависимости. Dependency Inversion Principle (DIP) — один из пяти принципов SOLID, который формулирует два правила: (1) модули верхних уровней не должны зависеть от модулей нижних уровней, а оба должны зависеть от абстракций; (2) абстракции не должны зависеть от деталей, а детали – от абстракций. DIP задаёт архитектурную цель, а IoC и DI — механизмы, позволяющие эту цель достичь. В Node‑приложениях DIP реализуется через интерфейсы (TypeScript) или контракты, а DI‑контейнер обеспечивает инъекцию реализаций, тем самым устраняя жёсткую связь между модулями и упрощая тестирование.
В чём различаются Kafka и RabbitMQ?Kafka — распределённая платформа потоковой передачи сообщений с высокой пропускной способностью, основанная на концепции журналов (log‑oriented) и партиционировании тем; RabbitMQ — брокер сообщений, реализующий модели очередей и маршрутизации (exchange‑queue) согласно AMQP, с гибкой поддержкой различных шаблонов доставки.
Чем отличаются механизмы Throttle и Debounce при применении к функциям в JavaScript?Throttle ограничивает частоту вызова функции, гарантируя, что она будет выполнена не чаще, чем один раз за заданный интервал времени, независимо от количества происходящих событий. Debounce откладывает выполнение функции до тех пор, пока не прекратятся быстрые последовательные вызовы в течение указанного тайм‑аута; функция срабатывает только один раз после последнего события. Throttle обычно используют для ограничивания нагрузки при постоянных событиях (scroll, resize), чтобы обновлять UI регулярно, но не слишком часто. Debounce применяют, когда нужно выполнить действие лишь после завершения серии быстрых событий (поиск по вводу, автосохранение), чтобы избежать лишних запросов. Реализация может быть написана вручную или взята из библиотек (lodash _.throttle, _.debounce). При выборе следует учитывать, нужен ли гарантированный периодический отклик (throttle) или окончательная реакция после паузы (debounce); оба подхода помогают уменьшить нагрузку и улучшить пользовательский опыт, но работают по разным принципам тайминга и триггеров.
В чём различаются cookie, localStorage и sessionStorage?Cookie — небольшие куски данных (до ≈ 4 KB), передаются серверу с каждым HTTP‑запросом, могут иметь срок жизни (expires/max‑age) и атрибуты безопасности (HttpOnly, Secure, SameSite). localStorage — клиентское хранилище, доступное через объект window.localStorage, сохраняет данные в виде строк, объём до ≈ 5‑10 MB, хранится постоянно (до очистки пользователем или программно) и не отправляется серверу. sessionStorage — тоже клиентское хранилище (window.sessionStorage), но данные привязаны к конкретной вкладке/окну браузера и удаляются при её закрытии; объём аналогичен localStorage, также не передаётся серверу. Основные различия: передача серверу (только cookie), время жизни (cookie — задаётся явно, localStorage — постоянное, sessionStorage — до закрытия окна), область видимости (cookie — домен/путь, localStorage — общий для всех вкладок данного origin, sessionStorage — только текущая вкладка), размер и API (cookie — строки в заголовке, методы get/set через document.cookie; Web Storage — методы getItem, setItem, removeItem, clear). Выбор зависит от задачи: аутентификация и сервер‑сайд взаимодействие — cookie; долговременное клиентское кэширование без передачи серверу — localStorage; временные данные, специфичные для одной вкладки — sessionStorage.
Каковы различия между interface и class в TypeScript?Interface — это чисто компиляторская конструкция, существующая только во время проверки типов; она описывает форму объекта, может наследовать другие интерфейсы, поддерживает декларативное объединение (declaration merging) и не генерирует JavaScript‑код. Class — это полноценный объектный тип, который существует как в фазе типизации, так и в рантайме: компилятор генерирует функцию‑конструктор (или ES‑class), поддерживает реализацию методов, конструкторы, свойства, наследование, статические члены, модификаторы доступа и может быть инстанциирован. Interface не может содержать реализацию, приватные/защищённые члены или статические свойства, в то время как class может. При совместном использовании class может реализовывать один или несколько интерфейсов, а интерфейсы могут описывать только публичную часть структуры, в том числе для функций, массивов и примитивов. Кроме того, class участвует в рефлексии (instanceof), а interface — нет.
В чём различие между module.exports и exports в Node.js?module.exports — это реальный объект, который Node возвращает при require; по умолчанию это пустой объект, и любой его свойства будут доступны импортирующему коду. exports — это лишь удобный алиас, изначально указывающий на тот же объект, что и module.exports (exports = module.exports). При добавлении свойств через exports.foo = … они попадают в объект, который будет экспортирован, пока не произойдёт переопределение. Если же присвоить exports = function(){…} или exports = {a:1}, то ссылка меняется только локально и module.exports остаётся прежним, в результате экспортировать будет прежний объект, а не новое значение. Поэтому для экспорта единственного значения (функции, класса, примитивного объекта) следует переопределять module.exports, а для добавления нескольких свойств удобно использовать exports. Ошибки часто возникают, когда разработчики пишут export = … вместо module.exports = …, из‑за чего импортируемый модуль оказывается пустым.
В чём отличается использование `return undefined` от полного отсутствия оператора `return` в функции?`return undefined` и отсутствие `return` приводят к одинаковому результату — функция завершится и вернёт значение `undefined`. При отсутствии `return` значение возвращается неявно, а при `return undefined` — явно, что позволяет явно завершить функцию в нужный момент, улучшить читаемость кода и задать точку выхода из вложенных блоков. Оба варианта одинаково работают в обычных, async и generator‑функциях: в async‑функции они эквивалентно разрешают промис со значением `undefined`, в generator‑функциях возвращают объект `{ value: undefined, done: true }`. Явный `return` может быть полезен для отладки (виден в AST) и для соблюдения стиля кода, однако с точки зрения результата разницы нет.
В чём отличаются функции spawn и fork в Node.js?Методы `spawn` и `fork` из модуля `child_process` создают дочерние процессы, но различаются по назначению, способу взаимодействия и параметрам запуска: `spawn` запускает любой исполняемый файл и предоставляет потоки `stdin`, `stdout`, `stderr` без встроенного канала IPC; `fork` специализирован для запуска других Node‑скриптов, автоматически устанавливает двунаправленный канал IPC, передаёт аргументы в виде массива, позволяет передавать сообщения объектного типа и обеспечивает более простую синхронизацию между процессами, однако имеет небольшие накладные расходы. `spawn` предпочтителен для внешних программ, когда нужен контроль над потоками ввода/вывода, тогда как `fork` удобен для распределения нагрузки между несколькими Node‑процессами и обмена данными через `process.send`/`process.on('message')`. Кроме того, `fork` автоматически ставит `execArgv` (например, `--harmony`) и использует `node` как исполняемый файл, тогда как `spawn` требует явно указать путь к бинарнику.
В чём отличаются абстрактный класс и интерфейс?Абстрактный класс — это класс, который не может быть инстанциирован и может содержать как реализованные методы, так и объявленные без реализации (abstract). Он может иметь поля, конструкторы, модификаторы доступа и реализовывать логику, общую для всех подклассов. В TypeScript абстрактный класс существует только на этапе компиляции, но после трансляции в JavaScript становится обычным классом, поэтому его свойства и методы присутствуют в рантайме. Интерфейс — это чисто типовая декларация, не генерирующая JavaScript‑код. Он описывает только сигнатуры свойств и методов, без реализации, и используется исключительно для статической типизации. Интерфейсы могут расширять друг друга (множественное наследование), а классы могут реализовывать несколько интерфейсов, но наследовать только один класс (абстрактный или обычный). При реализации абстрактного класса наследник обязан переопределить все abstract‑методы, но может переопределять и конкретные методы, а при реализации интерфейса необходимо реализовать все объявленные члены. Кроме того, абстрактный класс может хранить состояние (поля с начальными значениями) и выполнять код в конструкторе, тогда как интерфейс не может.
В чём различия между вертикальным и горизонтальным масштабированием баз данных?Вертикальное масштабирование (scale‑up) подразумевает увеличение вычислительных ресурсов — CPU, RAM, SSD — одного серверного узла, на котором работает БД. Это обычно проще в реализации, так как приложение и данные остаются на одном экземпляре, но есть физические и экономические ограничения: стоимость мощных машин растёт экспоненциально, а отказ одного узла приводит к полной недоступности сервиса; часто требуется перезапуск для применения апгрейда. Горизонтальное масштабирование (scale‑out) предполагает добавление новых узлов в кластер и распределение нагрузки между ними. Реализуется через репликацию, шардинг, распределённые транзакции и балансировщики. Позволяет почти линейно увеличивать пропускную способность и повышать отказоустойчивость, но требует более сложной архитектуры: согласование схем данных, управление консистентностью (CAP‑теорема), настройка роутеров и мониторинга. При выборе подхода в Node‑приложениях учитывают драйверы, поддерживающие кластерные подключения (например, `mongodb` с реплика‑сетами или `pg-pool` для PostgreSQL), стратегию обработки ошибок и балансировку запросов. Вертикальное масштабирование удобно для небольших нагрузок и быстрых прототипов; горизонтальное — для высоких нагрузок, глобального распределения и требований к высокой доступности.
Чем отличаются императивные и декларативные языки программирования?Императивный язык описывает алгоритм выполнения, задавая последовательность операторов, изменяющих состояние программы (переменные, память), пример – JavaScript, C, где разработчик управляет потоком управления (циклы, условные операторы, присваивания); декларативный язык описывает желаемый результат без указания пошаговых инструкций, фокусируется на «что» должно быть получено, а не «как», примеры – SQL, HTML, React JSX, а в Node.js используют декларативные API (Promise, async/await) и конфигурационные файлы; основные различия: уровень абстракции (императив – низкоуровневый, декларатив – высокоуровневый), контроль над состоянием (императивный требует явного управления, декларативный скрывает его), читаемость и поддерживаемость (декларативный обычно короче и легче понять при сложных задачах), оптимизация (компилятор/рантайм может лучше оптимизировать декларативный код), тестируемость (декларативный код часто более предсказуем); выбор зависит от задачи: для детального управления ресурсами лучше императивный, для описания бизнес‑логики, UI, запросов к данным предпочтителен декларативный.
Чем различаются методология BEM и подход Scoped‑styles при организации CSS‑кода?BEM (Block‑Element‑Modifier) — это наименовательная методика, где каждый CSS‑класс состоит из трёх частей: блока, элемента и модификатора (пример: `button__icon--large`). Классы глобальны, поэтому их имена должны быть уникальными во всём проекте; это обеспечивает предсказуемость и позволяет переиспользовать стили без конфликтов, но требует строгого соблюдения конвенций и иногда приводит к длинным именам. Scoped‑styles (например, в Vue, Svelte, Angular или при использовании CSS‑модулей) автоматически ограничивают действие сгенерированных классов только текущим компоненту. При компиляции к именам добавляются уникальные хэши, что избавляет от необходимости думать о глобальном коллизии имён, упрощает поддержку и рефакторинг, но требует сборки и иногда усложняет отладку из‑за скрытых имён. Основные различия: глобальность vs. локальность, ручное именование vs. автоматическое хеширование, зависимость от сборщика vs. независимость, гибкость переиспользования (BEM) против изоляции (Scoped). Выбор зависит от масштаба проекта, команды и используемого фреймворка: BEM подходит для больших кодовых баз без модульных систем, Scoped‑styles — для компонентных UI‑библиотек и SPA‑приложений.
В чём отличается поведение ключевого слова this в стрелочных функциях и в обычных функциях?`this` в обычных (function) — динамически привязывается к контексту вызова: в строгом режиме — `undefined`, в нестрогом — глобальный объект; при вызове как метода берётся объект‑владелец, при конструкторном вызове — создаётся новый экземпляр, а `call`/`apply`/`bind` позволяют явно переопределить значение. В стрелочных функциях (`()=>{}`) `this` не имеет собственного привязки; она захватывает (лексически) `this` из окружающего (внешнего) контекста в момент создания функции и игнорирует последующие попытки переопределить его через `call`, `apply` или `bind`. Поэтому в стрелочных функциях `this` обычно указывает на тот же объект, что и в внешней функции (например, в методах классов, обработчиках событий, промис‑цепочках). Пример:

```js
function Regular() {
console.log(this); // зависит от способа вызова
}
const Arrow = () => console.log(this); // берёт this из внешнего скрипта (global в Node = module.exports)

const obj = {
name: 'Obj',
regularMethod: Regular,
arrowMethod: Arrow,
};

obj.regularMethod(); // this === obj
obj.arrowMethod(); // this === глобальный объект (module.exports), а не obj
```

В классах:

```js
class A {
constructor() {
this.value = 10;
setTimeout(function() { console.log(this.value); }, 0); // undefined в strict‑mode
setTimeout(() => console.log(this.value), 0); // 10, т.к. arrow захватывает this из конструктора
}
}
new A();
```

Таким образом, различия:
1. **Привязка** – обычная функция: динамическая, стрелочная: лексическая.
2. **Возможность переопределения** – только у обычных через `call/apply/bind`.
3. **Использование в конструкторах** – обычные могут быть конструкторами (`new`), стрелочные – нет (выбросит ошибку).
4. **Поведение в строгом режиме** – обычные дают `undefined`, стрелочные наследуют значение из внешнего контекста.

Выбор зависит от задачи: когда нужен фиксированный контекст – стрелочная; когда требуется гибкость или возможность использовать как конструктор – обычная.