Все специализации

Node.js-разработчик: вопросы на собеседовании

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

Event Loop — это механизм, позволяющий Node.js выполнять неблокирующие I/O операции, несмотря на то, что JavaScript является однопоточным языком. Event Loop непрерывно мониторит стек вызовов, очередь колбэков и различные очереди событий (таймеры, I/O).

Основные фазы Event Loop:

timers: Выполняет колбэки запланированные setTimeout() и setInterval().

pending callbacks: Выполняет колбэки системных операций ввода-вывода (например, сетевых ошибок), отложенные до следующей итерации Event Loop.

idle, prepare: Внутренние фазы Event Loop.

poll:

Вычисляет, как долго блокироваться и опрашивать операции ввода-вывода.

Обрабатывает события в очереди I/O колбэков, когда их количество достигает определенного предела или исчерпывается время.

check: Выполняет колбэки запланированные setImmediate().

close callbacks: Выполняет колбэки закрытия, например, socket.destroy().

process.nextTick() и микротаски (обещания, queueMicrotask()) выполняются между фазами Event Loop и до перехода к следующей фазе или завершения текущей. process.nextTick() имеет более высокий приоритет, чем микротаски.

Event Loop позволяет Node.js эффективно обрабатывать множество параллельных запросов без создания отдельного потока для каждого из них, что обеспечивает высокую производительность.

ISR (Incremental Static Regeneration) — это механизм кэширования страниц в веб-приложениях, который позволяет обновлять статические страницы после их первого построения, не прибегая к полному перестроению всего сайта.

Используется для:

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

Актуальности данных: Позволяет обновлять контент на статических страницах без необходимости ручного или длительного процесса деплоя.

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

Улучшения SEO: Поисковые роботы получают статические страницы, что способствует лучшему индексированию.

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

Пример использования в Next.js (один из фреймворков, реализующих ISR):

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

Синхронные операции блокируют выполнение потока до своего завершения.

Примеры синхронных операций в Node.js:

Операции с файловой системой, заканчивающиеся на Sync:const fs = require('fs');

// Чтение файла синхронно

const data = fs.readFileSync('file.txt', 'utf8');

Блокирующие функции CPU-интенсивных модулей, которые не используют асинхронные API: Например, сложные математические расчеты в обычном цикле.

Некоторые операции в нативных модулях (C++), если они специально не реализованы асинхронно.

Операции, которые в своей основе являются блокирующими в операционной системе (например, ожидание ввода в консоль).

Большинство встроенных функций Node.js, работающих с сетевыми операциями, файловой системой, базами данных и т.д., имеют асинхронные аналоги, чтобы избежать блокировки основного потока выполнения. Синхронные версии обычно используются только в утилитах командной строки или при инициализации, где блокировка приемлема.

Шина данных (Event Bus или Message Bus) — это программный паттерн, который позволяет различным компонентам системы взаимодействовать друг с другом, не зная о детальном устройстве и существовании друг друга напрямую. Компоненты публикуют события в шину, а другие компоненты могут подписываться на нужные им типы событий. Шина выступает посредником, маршрутизируя события от издателей к подписчикам.

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

Слабая связность: Компоненты становятся менее зависимыми друг от друга. Изменение одного компонента minimально влияет на другие.

Расширяемость: Легко добавлять новые компоненты, которые либо публикуют, либо подписываются на события.

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

UI-компоненты: Позволяет компонентам интерфейса обмениваться информацией без прямого вызова методов.

Системы с реактивной архитектурой: Основа для построения систем, реагирующих на события.

Пример реализации в Node.js:

Можно реализовать с использованием стандартного модуля EventEmitter.

Фабричный метод, Абстрактная фабрика, Строитель (Builder), Одиночка (Singleton), Прототип, Адаптер, Декоратор, Фасад, Легковес (Flyweight), Заместитель (Proxy), Команда (Command), Итератор (Iterator), Посредник (Mediator), Хранитель (Memento), Наблюдатель (Observer - хотя в JS есть EventEmitter/RxJS, паттерн "из коробки" встречается реже в классическом виде), Состояние (State), Стратегия (Strategy), Шаблонный метод (Template Method).

В JavaScript/TypeScript многие из этих задач решаются иными средствами:

Прототипное наследование вместо классического.

Функции высшего порядка и замыкания.

Динамическая типизация (для JS).

Модульная система.

Например:

Singleton: В JS/TS часто реализуется через модули или замыкания, а не через статические методы класса.// Реализация Singleton через модуль

const singletonInstance = {

data: 'some data'

};

module.exports = singletonInstance;

Factory Method / Abstract Factory: В JS/TS часто используются функции, возвращающие объекты, или простые классы. Нет жесткой необходимости в интерфейсах и абстрактных классах в том же смысле, что в Java.// Простая "фабрика" в JS

function createObject(type) {

if (type === 'A') {

return { name: 'Object A' };

} else if (type === 'B') {

return { name: 'Object B' };

}

return null;

}

Decorator: В JS/TS это либо функции-обертки, либо синтаксис декораторов (экспериментальный или стандартизированный в TS).// Пример декоратора в TypeScript

function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {

const originalMethod = descriptor.value;

descriptor.value = function(...args: any[]) {

console.log(`Calling ${propertyKey} with args: ${JSON.stringify(args)}`);

const result = originalMethod.apply(this, args);

console.log(`Method ${propertyKey} returned: ${JSON.stringify(result)}`);

return result;

};

return descriptor;

}

class MyClass {

@logMethod

myMethod(arg1: string): string {

return `Processed: ${arg1}`;

}

}

Builder: В JS/TS часто реализуется через цепочки вызовов методов или простые объектные литералы.// Пример "билдера" через цепочку вызовов

class Config {

constructor() {

this._settings = {};

}

withSetting(key, value) {

this._settings[key] = value;

return this; // Возвращаем this для цепочки

}

build() {

return this._settings;

}

}

const myConfig = new Config()

.withSetting('timeout', 1000)

.withSetting('retry', 3)

.build();

Паттерны GOF, разработанные в контексте ООП языков вроде Java, могут быть менее идиоматичными или излишне сложными для JavaScript/TypeScript, где часто предпочтение отдается функциональным или прототипно-ориентированным подходам.

Обычно это PUT или POST запрос по адресу, содержащему токен подтверждения.

Пример:

Избегать полиморфных операций.

Предпочитать мономорфные операции.

Использовать скрытые классы.

Оптимизировать циклы.

Использовать литералы объектов и массивов.

Избегать delete, eval и arguments.

Применять use strict.

Своевременно освобождать неиспользуемую память.

Деоптимизация — это процесс, при котором оптимизированный компилятором код становится менее эффективным, возвращаясь к более медленной, необработанной версии. В контексте Node.js, работающего на V8, это происходит, когда оптимизирующий компилятор (Turbofan) сделал предположения (инлайн-кэширование, специфичные типы), которые впоследствии оказались ложными или недействительными из-за изменений в данных или поведении программы. Это заставляет движок отказаться от оптимизированной версии функции и вернуться к интерпретируемой или менее оптимизированной версии.

Причины деоптимизации могут включать:

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

Мегаморфность: Когда к точке вызова функции применяется слишком много различных типов объектов (более 4-х), V8 перестает "полиморфно" кэшировать и деоптимизирует.

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

Использование неоптимизируемых встроенных функций: Некоторые встроенные функции или специфические конструкции могут приводить к деоптимизации.

Последствия деоптимизации:

Замедление производительности: Код выполняется медленнее.

Повышенное потребление ресурсов: Может возрасти использование CPU и памяти.

Пример (упрощенный):

Для диагностики деоптимизации можно использовать флаги V8, такие как --trace-deopt.

Рекурсия — это функция, которая вызывает сама себя в процессе выполнения.

Два основных компонента рекурсивной функции:

Базовый случай (или условие выхода): Условие, при выполнении которого рекурсия завершается. Без него функция будет вызывать сама себя бесконечно, что приведет к переполнению стека вызовов.

Рекурсивный шаг: Шаг, на котором функция вызывает сама себя с измененным аргументом, приближающим ее к базовому случаю.

Пример вычисления факториала с использованием рекурсии:

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

Может делать код более читаемым и элегантным для определенных задач (например, обход деревьев, обработка фракталов).

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

Недостатки рекурсии:

Может быть менее производительной по сравнению с итеративными решениями из-за накладных расходов на вызовы функций и управление стеком.

Неправильно реализованный базовый случай может привести к бесконечной рекурсии и переполнению стека вызовов.

Иногда труднее отлаживать.

В Node.js, как и в других языках, следует осторожно использовать глубокую рекурсию из-за ограничений на размер стека вызовов. Для глубоких рекурсивных структур иногда применяют оптимизацию хвостовой рекурсии, если поддерживается движком (хотя V8 не оптимизирует все случаи хвостовой рекурсии автоматически), или преобразуют рекурсивный вызов в итеративный.

Инкапсуляция нужна для сокрытия внутренней реализации объекта от внешнего мира и предоставления доступа к его данным только через определенный интерфейс (методы).

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

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

Управляемость: Изменение внутренней реализации объекта не влияет на код, который его использует, если внешний интерфейс не меняется.

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

Гибкость: Позволяет легко заменять внутреннюю реализацию объекта на другую, более эффективную, без изменения кода клиентов.

Пример на JavaScript (с использованием приватных полей):

Шина — это программный шаблон или компонент, обеспечивающий унифицированный механизм обмена сообщениями или событиями между различными частями системы или отдельными сервисами.

Основные типы шин:

Шина событий (Event Bus/Event Emitter): Позволяет компонентам подписываться на определенные события и получать уведомления, когда эти события происходят, независимо от инициатора события.

Шина команд (Command Bus): Используется для отправки объектов-команд в определенные обработчики, инкапсулируя намерение выполнить какое-либо действие.

Шина сообщений (Message Bus): Более общий термин, который может включать в себя как события, так и команды, или использоваться как брокер сообщений для асинхронной коммуникации.

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

Внутри приложения для взаимодействия между модулями.const { EventEmitter } = require('events');

const eventBus = new EventEmitter();

eventBus.on('userCreated', (userData) => {

console.log('New user created:', userData);

});

// Другой модуль

function createUser(data) {

// Логика создания пользователя

eventBus.emit('userCreated', data); // Эмитируем событие

}

Между микросервисами с использованием брокеров сообщений (таких как Kafka, RabbitMQ).const amqp = require('amqplib');

async function sendMessage() {

const connection = await amqp.connect('amqp://localhost');

const channel = await connection.createChannel();

const queue = 'my_queue';

const msg = 'Hello, world!';

await channel.assertQueue(queue, { durable: false });

channel.sendToQueue(queue, Buffer.from(msg));

console.log('Sent message:', msg);

setTimeout(() => {

connection.close();

}, 500);

}

sendMessage();

Родительский процесс получит событие exit. Если дочерний процесс завершился с ошибкой, также будет сгенерировано событие error и передан объект ошибки. Стандартные потоки ввода-вывода дочернего процесса (stdout, stderr) будут закрыты, и родительский процесс должен обработать эти события, чтобы избежать утечек ресурсов (например, очистить обработчики событий).

Пример обработки завершения:

Если родительский процесс не прослушивает событие exit, он продолжит свое выполнение. Если дочерний процесс завершился с ошибкой и событие error не обработано, возникнет необработанное исключение и, скорее всего, родительский процесс завершится.

Также могут возникнуть процессы-зомби, если родительский процесс не обрабатывает событие exit и операционная система не может корректно очистить ресурсы дочернего процесса. Это происходит, когда дочерний процесс завершился, но его PID и статус завершения остаются в таблице процессов до тех пор, пока родитель не "соберет" его статус.

Node.js однопоточен для выполнения JavaScript-кода благодаря механизму Event Loop. Он непрерывно проверяет очередь событий и выполняет соответствующие колбэки.

Для выполнения блокирующих операций (ввод/вывод, работа с файлами, сеть) Node.js использует пул потоков (Thread Pool) из библиотеки libuv. Эти операции выполняются асинхронно в отдельных потоках, не блокируя основной поток Event Loop'а. Результаты или ошибки возвращаются обратно в Event Loop через колбэки.

Ключевые компоненты:

Event Loop: Основной цикл выполнения, который обрабатывает события и вызовы функций.

Callback Queue: Очередь для колбэков готовых асинхронных операций.

Thread Pool: Отдельные потоки для выполнения блокирующих задач.

libuv: Библиотека низкого уровня, предоставляющая Event Loop и Thread Pool.

Таким образом, Node.js обеспечивает высокую производительность для конкурирующих задач за счет неблокирующего ввода/вывода, несмотря на однопоточность выполнения JavaScript.

Серверный рендеринг (SSR) — это техника, при которой первое отображение веб-страницы происходит на сервере, а не в браузере пользователя.

При обычном клиентском рендеринге браузер получает пустой HTML-файл с ссылкой на JavaScript. Браузер загружает JavaScript, парсит его, выполняет и только потом динамически строит DOM и отображает содержимое.

При SSR сервер обрабатывает компоненты приложения, формирует готовый HTML-код и отправляет его браузеру.

Этапы SSR:

Запрос: Пользовательский браузер отправляет HTTP-запрос на сервер.

Рендеринг на сервере: Сервер запускает JavaScript-код приложения, включая логику рендеринга UI-компонентов, и получает полностью сформированный HTML-строку.

Ответ: Сервер отправляет этот HTML-файл браузеру.

Первичное отображение: Браузер получает HTML и сразу отображает содержимое страницы, так как все элементы уже присутствуют в разметке.

Гидратация (Hydration): После загрузки HTML браузер загружает и выполняет JavaScript-код приложения. Этот код "оживляет" статичную разметку, добавляя интерактивность, обработчики событий и привязывая состояние.

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

Улучшенное SEO: Поисковые роботы лучше индексируют страницы с полностью сформированным HTML.

Более быстрая первая отрисовка (FCP - First Contentful Paint): Пользователь видит содержимое страницы быстрее.

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

Недостатки SSR:

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

Сложность разработки: Требуется учитывать изоморфность кода (возможность выполнения как на сервере, так и на клиенте).

Больший размер ответа: Первый ответ сервера содержит полный HTML.

Node.js использует неблокирующий, событийно-ориентированный ввод/вывод. Основной принцип работы — Event Loop, который позволяет обрабатывать множество операций асинхронно, не создавая отдельный поток для каждого запроса. Вместо этого, когда Node.js получает запрос, требующий длительной операции (например, доступ к файлу или базе данных), он отправляет эту операцию в фоновый пул потоков (для системных операций) или просто продолжает выполнять следующий код, пока не получит уведомление об окончании асинхронной операции через callback-функцию или промис. Это позволяет Node.js эффективно использовать ресурсы и обрабатывать большое количество одновременных клиентских подключений с относительно небольшим количеством потоков.

Основные компоненты и механизмы:

Event Loop: Сердце Node.js. Это одиночный цикл, который постоянно проверяет колбэки в очереди и выполняет их, когда стек вызовов пуст.

Неблокирующий ввод/вывод: Операции ввода/вывода не приостанавливают выполнение кода. Node.js инициирует операцию и регистрирует колбэк, который вызовется после её завершения.

Событийно-ориентированная архитектура: Node.js активно использует паттерн "издатель-подписчик". Когда происходит событие (например, получение данных), соответствующие обработчики (подписчики) выполняются.

Libuv: Кроссплатформенная библиотека, предоставляющая абстракцию над различными низкоуровневыми механизмами асинхронного ввода/вывода операционных систем (Epoll, Kqueue, IOCP и т.д.), а также пул потоков для некоторых операций.

V8 JavaScript Engine: Выполняет JavaScript-код. Эффективно оптимизирует выполнение кода.

Пример асинхронной операции:

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

Для задач, интенсивно использующих ресурсы CPU (например, сложные вычисления), однопоточная природа Node.js может стать узким местом. В таких случаях используются подходы, такие как:

Кластеризация (cluster module): Запуск нескольких экземпляров Node.js процесса, слушающих один порт. Каждому процессу доступно одно ядро CPU.

Воркеры (worker_threads module): Создание отдельных потоков внутри одного процесса Node.js для выполнения CPU-интенсивных задач без блокировки основного Event Loop.

Инфраструктурное масштабирование: Использование балансировщиков нагрузки и горизонтальное масштабирование на уровне серверов.

Таким образом, Node.js эффективно справляется с большим количеством запросов благодаря своей неблокирующей архитектуре и Event Loop, особенно в задачах, где преобладает ввод/вывод.

В среднем случае — $O(1)$.

В худшем случае (при большом количестве коллизий) — $O(n)$, где $n$ — количество элементов в Map.

Это связано с тем, что Map использует хеш-таблицу. Время доступа к элементу зависит от качества хеш-функции и реализации разрешения коллизий. В идеале каждый элемент имеет уникальный хеш и доступ мгновенный. При коллизиях может потребоваться перебор элементов с одинаковым хешем.

Использование методологий БЭМ, OOCSS, SMACSS.

CSS-модули.

Styled Components / Emotion.

Shadow DOM.

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

Наиболее важные для Node.js протоколы:

HTTP/1.1, HTTP/2, HTTP/3: Основа веб-коммуникаций. Node.js имеет встроенные модули для работы с HTTP.

TCP (Transmission Control Protocol): Надежный, ориентированный на соединение протокол транспортного уровня. Гарантирует доставку пакетов данных и их порядок.

UDP (User Datagram Protocol): Без подключения, ненадежный протокол транспортного уровня. Быстрее TCP, подходит для потоковой передачи данных и приложений, чувствительных ко времени.

TLS/SSL: Протоколы для обеспечения безопасности и шифрования данных в сети, часто используются поверх TCP для HTTPS.

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

Понимание сетевых протоколов критично для разработки масштабируемых и надежных сетевых приложений на Node.js.

На уровне инфраструктуры в приложении могут находиться следующие компоненты и сервисы:

Базы данных: Хранилища данных (PostgreSQL, MongoDB, Redis и др.).

Брокеры сообщений: Системы для асинхронного взаимодействия между сервисами (RabbitMQ, Kafka).

Кэширование: Сервисы для хранения часто используемых данных (Redis, Memcached).

Логирование и мониторинг: Системы для сбора, агрегации и анализа логов, а также мониторинга производительности (ELK Stack, Prometheus, Grafana, Sentry).

Инструменты оркестрации контейнеров: Управление жизненным циклом контейнеризированных приложений (Docker Swarm, Kubernetes).

Системы хранения файлов: Объектные хранилища или файловые системы для статических ресурсов (S3, MinIO).

Шлюзы API: Точка входа для внешних запросов, выполняющая функции аутентификации, авторизации, балансировки нагрузки (NGINX, API Gateway).

Системы CI/CD: Инструменты для автоматизации процессов сборки, тестирования и развертывания (Jenkins, GitLab CI, GitHub Actions).

Сетевые компоненты: Балансировщики нагрузки (Load Balancer), межсетевые экраны (Firewall), DNS-серверы.

Управление секретами: Системы для безопасного хранения и выдачи секретов (ключей API, паролей) (HashiCorp Vault, AWS Secrets Manager).

Сервисные сетки: Инфраструктурный слой для управления взаимодействием между микросервисами (Istio, Linkerd).

Системы управления конфигурацией: Инструменты для автоматизации настройки серверов и приложений (Ansible, Puppet, Chef).

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

Libuv — это кроссплатформенная библиотека ввода-вывода, лежащая в основе Node.js.

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

Асинхронный ввод-вывод: Обеспечивает неблокирующие операции ввода-вывода, такие как работа с файлами, сетью и процессами.

Цикл событий (Event Loop): Управляет асинхронными операциями и вызывает соответствующие колбэки, когда они завершены.

Пул потоков (Thread Pool): Используется для выполнения потенциально блокирующих операций ввода-вывода, чтобы не блокировать основной цикл событий.

Таймеры: Предоставляет функции для установки задержек и интервалов.

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

Libuv реализует модель, где основной поток выполняет цикл событий, обрабатывая неблокирующие операции, а более медленные или блокирующие задачи делегируются пулу потоков, возвращая результаты в цикл событий по их завершении.

Ключевые аспекты Libuv:

Кроссплатформенность: Предоставляет единый API для разных операционных систем (Windows, macOS, Linux и др.), abstraгируя различия в низкоуровневых реализациях I/O.

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

Низкоуровневость: Предоставляет примитивы, на основе которых строятся более высокоуровневые API в Node.js.

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

Существуют две основные системы модульности в JavaScript: CommonJS (CJS) и ES Модули (ESM).

CJS использовался в Node.js по умолчанию до появления ESM. ESM является стандартом для JavaScript и поддерживается в современных браузерах и версиях Node.js. ESM обладает преимуществами в виде статического анализа, возможности tree-shaking (удаление неиспользуемого кода) и асинхронной загрузки модулей.

Пример CJS:

Пример ESM:

Добавить обработчик события click на документ (document). В обработчике проверять, был ли клик совершен вне модального окна, сравнивая event.target с модальным окном или его содержимым. Если клик был вне, скрывать модальное окно.

Также можно использовать обработчик на оверлей (фоновый элемент), который покрывает весь экран, если таковой используется для модального окна. Клик по оверлею тоже будет закрывать модальное окно.

Высокий порог вхождения из-за использования концепции модулей, контроллеров, сервисов, провайдеров и декораторов, основанных на паттернах ООП и IoC.

Избыточность для простых API или микросервисов, так как требуется настройка множества файлов и структур.

Жесткая структура, которая может ограничивать гибкость при работе с нестандартными задачами или интеграции сторонних библиотек, плохо вписывающихся в парадигму Nest.js.

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

Зависимость от TypeScript, что является плюсом для крупных проектов, но может быть недостатком для разработчиков, не уверенных в этом языке.

Node.js использует движок V8 от Google, который компилирует JavaScript в машинный код непосредственно на стороне сервера.

Процесс выполнения выглядит так:

Разбор (Parsing): V8 парсит JS-код и строит Abstract Syntax Tree (AST).

Компиляция (Compilation): ТурбоФлэн (TurboFan) - компилятор в V8 - компилирует AST в машинный код. Сначала применяется более быстрый компилятор (Ignition), а затем, при оптимизации, более производительный (TurboFan).

Выполнение (Execution): Скомпилированный код выполняется. За счет асинхронной архитектуры Node.js и цикла событий (Event Loop), операции ввода/вывода (например, чтение файла, сетевые запросы) не блокируют выполнение основного кода.

Оптимизация/Деоптимизация (Optimization/Deoptimization): V8 постоянно профилирует код во время выполнения. Часто выполняемые участки (Hot Spots) могут быть оптимизированы для ускорения. Если исходные предположения для оптимизации перестают быть верными, происходит деоптимизация, и код возвращается к менее оптимизированной версии.

Основные компоненты:

V8 Engine: Ядро выполнения, парсинг, компиляция, выполнение JS.

libuv: Библиотека, предоставляющая кросс-платформенную асинхронную поддержку для операций I/O (файловая система, сеть, таймеры) и реализующая цикл событий.

Node.js Core Modules: Встроенные модули, написанные на C++ и JavaScript, предоставляющие базовые API (http, fs, path и т.д.).

Цикл событий (Event Loop):

Node.js использует неблокирующий ввод/вывод через цикл событий. Когда выполняется асинхронная операция, она передается в системное ядро (через libuv), а поток выполнения JS продолжает работать. Результат операции обрабатывается, когда она завершается, и соответствующий колбэк помещается в очередь событий для последующего выполнения в цикле событий.

Замедление или зависание приложения, увеличение задержек (latency) при обработке запросов.

Высокая загрузка CPU, часто близкая к 100%, при отсутствии интенсивных вычислений.

Постоянное увеличение использования оперативной памяти, не связанное с ростом данных (утечки памяти).

Несрабатывание запланированных операций (например, таймеров) или их срабатывание с большой задержкой.

Ошибки "Event loop is locking up" или подобные предупреждения в логах.

Использование инструментов мониторинга, показывающих длительное блокирование Event Loop:

Node.js core modules: process.hrtime.bigint() для измерения выполнения кода.

Профилировщики: node --prof для сбора данных о производительности.

Библиотеки для мониторинга Event Loop: blocked, eventemitter2 (в режиме отладки).

Стек вызовов (Call Stack) — это структура данных, которая отслеживает выполнение функций. Когда функция вызывается, она помещается на вершину стека. Когда функция завершает работу, она удаляется из стека. JavaScript однопоточный, поэтому у него один стек вызовов.

Основные операции:

Push: Добавление функции на вершину стека.

Pop: Удаление функции с вершины стека.

Пример:

Отслеживание стека:

Ошибки переполнения стека (Stack Overflow) происходят, когда стек заполняется слишком большим количеством вызовов (например, при бесконечной рекурсии) и не может добавить новые функции.

REST (Representational State Transfer) – это архитектурный стиль для построения распределенных систем, в частности веб-сервисов. Он определяет набор ограничений и принципов, которые помогают создавать масштабируемые, отказоустойчивые и легко поддерживаемые веб-сервисы.

Основные принципы REST:

Клиент-серверная архитектура: Разделение ответственности между клиентом и сервером. Клиент запрашивает ресурсы, сервер отвечает на запросы.

Отсутствие состояния (Stateless): Сервер не хранит информацию о состоянии клиента между запросами. Каждый запрос должен содержать всю необходимую информацию для его обработки.

Кэширование: Клиенты или промежуточные узлы могут кэшировать ответы сервера.

Единообразный интерфейс (Uniform Interface): Упрощает взаимодействие между клиентом и сервером за счет стандартизированных способов доступа к ресурсам:

Идентификация ресурсов с помощью URI.

Манипуляции ресурсами через представления (например, в формате JSON или XML).

Самоописывающие сообщения.

HATEOAS (Hypermedia as the Engine of Application State): Репрезентации ресурсов содержат ссылки на другие связанные ресурсы.

Многоуровневая система (Layered System): Система может состоять из нескольких слоев (например, балансировщик нагрузки, кэш, сервер), каждый из которых не влияет на взаимодействие между клиентом и сервером.

Код по запросу (Code on Demand, опционально): Сервер может передавать исполняемый код клиенту (например, JavaScript), который расширяет функциональность клиента.

Назначение REST:

Упрощение интеграции: Обеспечивает стандартный способ взаимодействия между различными системами.

Масштабируемость: Отсутствие состояния на сервере упрощает горизонтальное масштабирование.

Производительность: Кэширование снижает нагрузку на сервер и сокращает время ответа.

Надежность: Разделение на слои повышает отказоустойчивость.

Портативность: Клиент может взаимодействовать с разными серверами, реализующими RESTful API.

REST используется для создания веб-APIs, которые позволяют приложениям (веб-сайтам, мобильным приложениям, другим сервисам) взаимодействовать друг с другом и получать доступ к данным и функциональности.

Event loop можно оценить с помощью различных инструментов и техник, фокусируясь на задержках или блокировках:

Проверка метрик:

process.eventLoopUtilization(): Возвращает объект с информацией о загрузке Event Loop с момента последнего вызова или старта процесса. Позволяет отследить процент времени, которое Event Loop тратит на выполнение задач.

const elu = process.eventLoopUtilization();

console.log(`Event loop spent ${elu.utilization * 100}% of time busy.`);

process.resourceUsage(): Предоставляет статистику использования ресурсов, включая время, потраченное Event Loop на выполнение задач.

const usage = process.resourceUsage();

console.log(`Event loop idle time: ${usage.idlru_timer_lag} ms`);

Мониторинг uv_loop_t метрик (через нативные расширения или профилировщики): Доступ к более низкоуровневым метрикам libuv, например, задержке таймеров, количеству обрабатываемых хэндлов.

Проверка метрик:

Middleware и обертки:

Измерение времени выполнения промисов и колбэков: Оборачивание асинхронных операций для логирования их времени выполнения. Если колбэки или промисы выполняются слишком долго, это может блокировать Event Loop.

async function measureExecution(fn) {

const start = process.hrtime.bigint();

await fn();

const end = process.hrtime.bigint();

console.log(`Function executed in ${Number(end - start) / 1e6} ms`);

}

Профилирование:

Node.js Inspector (встроенный профилировщик): Позволяет собирать профиль CPU, показывая, какие функции потребляют больше всего времени. Длинные синхронные блоки в этом профиле указывают на блокировку Event Loop.

node --inspect index.js

Сторонние профилировщики: Например, clinic или flamebearer, которые предоставляют визуализации в виде "пламенных графов" (flame graphs), помогая выявить горячие точки.

Профилирование:

Мониторинг задержки (Latency):

Специализированные библиотеки: Такие как event-loop-inspector (хотя устарел, демонстрирует концепцию) или пользовательские реализации, постоянно проверяющие, насколько быстро Event Loop обрабатывает поставленные задачи (например, используя setImmediate и измеряя задержку).

function checkEventLoopLatency() {

setImmediate(() => {

const latency = Number(end - start) / 1e6;

if (latency > 10) { // Пример порога

console.warn(`High event loop latency: ${latency} ms`);

}

});

}

setInterval(checkEventLoopLatency, 100); // Проверка каждые 100 мс

Мониторинг HTTP-запросов (для веб-приложений): Высокое среднее время ответа или большой разброс (long tail latencies) могут свидетельствовать о блокировке Event Loop из-за длительного процессинга.

Системные метрики:

Загрузка CPU: Хотя не напрямую связано с Event Loop, процессор, постоянно загруженный одной Node.js нодой, может указывать на длительные синхронные вычисления, блокирующие Event Loop.

Системные метрики:

Использование комбинации этих методов позволяет получить полную картину о "здоровье" Event Loop.

Виртуальный DOM (VDOM) — это легковесное представление реального DOM в памяти. Он служит для оптимизации обновления пользовательского интерфейса.

При изменении состояния приложения, вместо прямого манипулирования реальным DOM (что является затратной операцией), фреймворк создает новую версию виртуального DOM. Затем текущая версия VDOM сравнивается с новой, находя минимальные различия. Этот процесс называется дифф-алгоритмом.

Найденные различия (патч) применяются к реальному DOM единым пакетом. Это минимизирует количество операций с реальным DOM и повышает производительность, особенно для сложных и часто обновляющихся интерфейсов.

Основные преимущества VDOM:

Производительность: Уменьшает количество прямых манипуляций с реальным DOM.

Кроссплатформенность: Позволяет использовать одну логику для рендеринга на разных платформах (браузер, мобильные приложения).

Простота разработки: Абстрагирует разработчика от сложных особенностей работы с реальным DOM.

Вызывает функцию с заданным значением this и аргументами, предоставленными в виде массива (или массивоподобного объекта).

Основное применение:

Изменение контекста выполнения функции (значения this).

Передача аргументов в виде массива.

Сравнение с call и bind:

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

К сложным (объектным) типам данных в JavaScript относятся:

Объекты (Object): Упорядоченные коллекции пар ключ-значение. Могут содержать функции, массивы и другие объекты.// Пример объекта

const person = {

name: 'John',

age: 30

};

Массивы (Array): Особый тип объектов, предназначенный для хранения упорядоченных коллекций элементов.// Пример массива

const numbers = [1, 2, 3, 4, 5];

Функции (Function): Объекты, которые можно вызывать для выполнения определенного кода.// Пример функции

const greet = function(name) {

console.log('Hello, ' + name);

};

Даты (Date): Объекты для работы с временем и датами.// Пример Date

const now = new Date();

Регулярные выражения (RegExp): Объекты для работы с шаблонами текста.// Пример RegExp

const pattern = /abc/;

Карты (Map): Коллекция пар ключ-значение, где ключами могут быть любые типы данных.// Пример Map

const myMap = new Map();

myMap.set('a', 1);

myMap.set('b', 2);

Множества (Set): Коллекция уникальных значений любого типа.// Пример Set

const mySet = new Set([1, 2, 2, 3]); // Содержит только 1, 2, 3

Символы (Symbol): Уникальные и неизменяемые примитивные значения, которые могут использоваться как ключи свойств объекта, чтобы избежать конфликтов имен. Хотя Symbol является примитивным по своей структуре, его часто относят к сложным типам из-за его специфического использования в контексте объектов и уникальности, отличающей его от других примитивов.// Пример Symbol

const mySymbol = Symbol('description');

const obj = {

[mySymbol]: 'unique value'

};

Сложные типы данных, в отличие от примитивных, хранятся в памяти по ссылке.

async/await - это синтаксический сахар над промисами. Он позволяет писать асинхронный код в более читаемом, синхронном стиле, используя ключевые слова async для функций, внутри которых используются await для ожидания разрешения промиса. Промисы же представляют собой объекты, представляющие конечное завершение (или сбой) асинхронной операции и ее результирующее значение.

Цикл событий (Event Loop) — это сердце Node.js, которое позволяет ему выполнять асинхронные операции неблокирующим способом. Он управляет порядком выполнения колбэков, зарегистрированных для асинхронных событий.

Когда асинхронная операция (например, чтение файла, сетевой запрос) инициируется в Node.js, она немедленно передается в низкоуровневую библиотеку libuv (или аналогичную). Node.js не ждет ее завершения, а двигается дальше, выполняя следующий код.

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

Таким образом, Event Loop обеспечивает асинхронность Node.js, позволяя выполнять другие задачи, пока ожидаем завершения длительных I/O операций. Это предотвращает блокировку главного потока и делает Node.js эффективным для высоконагруженных приложений.

Основные фазы Event Loop:

timers: Выполняет колбэки setTimeout() и setInterval().

pending callbacks: Выполняет колбэки из отложенной очереди.

idle, prepare: Используется внутренне.

poll: Ожидает новых событий ввода/вывода и выполняет их колбэки.

check: Выполняет колбэки setImmediate().

close callbacks: Выполняет колбэки закрытия (например, сокетов).

Микрозадачи (промисы) выполняются после каждой фазы Event Loop, до перехода к следующей.

В приведенном примере console.log('Конец выполнения - до чтения файла') выполнится до колбэка fs.readFile, поскольку чтение файла является асинхронной операцией, и Event Loop продолжит выполнение скрипта, не дожидаясь его завершения. Колбэк будет помещен в очередь и выполнен позже, когда файл будет прочитан и Event Loop будет готов.

enumerable — это атрибут дескриптора свойства объекта в JavaScript. Он определяет, будет ли свойство перечислено при использовании циклов for...in или методов, таких как Object.keys(), Object.values() и Object.entries().

По умолчанию, большинство свойств, создаваемых с помощью оператора присваивания или литералов объектов, являются перечисляемыми (enumerable: true). Свойства, добавляемые с помощью Object.defineProperty() или Object.defineProperties(), по умолчанию не перечисляемы (enumerable: false), если явно не указано иное.

Не перечисляемые свойства не включаются в результаты этих операций, но остаются доступными для прямого доступа (например, object.property) и могут быть получены с помощью Object.getOwnPropertyNames() или Object.getOwnPropertySymbols().

Пример:

Для обнаружения и устранения утечек памяти я использую следующие методы:

Мониторинг: Регулярно отслеживаю потребление памяти с помощью встроенных инструментов Node.js, таких как --expose-gc, process.memoryUsage(), или внешних инструментов вроде PM2 или Prometheus/Grafana.

Инструменты профилирования:

Heap Snapshots: Снимаю "снимки кучи" в разные моменты времени с помощью Chrome DevTools (или любого другого отладчика, поддерживающего протокол V8) и сравниваю их для выявления объектов, которые растут со временем и не должны этого делать.

// Пример использования heapdump для создания снимка кучи

const heapdump = require('heapdump');

// ... ваш код ...

// Создание снимка кучи

heapdump.writeSnapshot((err, filename) => {

if (err) console.error(err);

else console.log('Снимок кучи сохранен в:', filename);

});

CPU Profiling: Иногда утечки связаны не напрямую с памятью, а с зацикливаниями или чрезмерным использованием ЦПУ, которое опосредованно может привести к проблемам с памятью.

Анализ кода: Вручную просматриваю код на предмет распространенных причин утечек:

Неотписанные обработчики событий.

// Пример утечки: не удаленный обработчик события

const EventEmitter = require('events');

const emitter = new EventEmitter();

function handler() { /* ... */ }

emitter.on('myEvent', handler);

// Если emitter продолжает существовать, а ссылка на обработчик где-то потеряна

// или объект, к которому он привязан, должен быть уничтожен,

// обработчик будет удерживать его в памяти.

// Правильно:

// emitter.off('myEvent', handler);

Неочищенные таймеры (setInterval, setTimeout).

Замыкания, которые случайно удерживают ссылки на большие объекты.

Глобальные переменные, которые не очищаются.

Кэши, которые бесконечно растут без механизма вытеснения.

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

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

Code Reviews: Привлекаю коллег к анализу кода на предмет потенциальных утечек.

Процесс отрисовки страницы начинается, когда браузер получает и начинает обрабатывать HTML-документ. Этот процесс включает несколько этапов:

Парсинг HTML: Строится дерево DOM (Document Object Model).

Построение CSSOM (CSS Object Model): Парсится CSS, применяемый к странице.

Построение Render Tree: Объединяются DOM и CSSOM для создания дерева отрисовки, содержащего элементы, видимые на странице, и их стили.

Layout (Расчёт макета): Определяются геометрические размеры и позиции каждого элемента на странице.

Painting (Отрисовка): Производится отрисовка пикселей элементов на экране.

Compositing (Композитинг): Слои отрисованных элементов объединяются в окончательное представление на экране.

Хотя процесс отрисовки начинается на стороне клиента (браузера), его запуск триггерится получением HTML, который зачастую генерируется динамически на Node.js-сервере. Сервер отвечает за подготовку и отправку первоначального HTML-ответа.

В конструкторы передача зависимостей осуществляется через их параметры. Это называется внедрением зависимостей (Dependency Injection, DI).

Существует три основных способа внедрения зависимостей:

Через конструктор (Constructor Injection): Зависимости передаются как аргументы конструктора при создании экземпляра класса.

class Database {

// ...

}

class UserRepository {

constructor(db) { // Зависимость Database передается в конструктор

this.db = db;

}

// ...

}

const db = new Database();

const userRepository = new UserRepository(db); // Внедрение зависимости при создании объекта

Через сеттер (Setter Injection): Зависимости передаются через публичные методы-сеттеры после создания экземпляра класса.

class Database {

// ...

}

setDatabase(db) { // Метод-сеттер для внедрения зависимости

this.db = db;

}

// ...

}

const userRepository = new UserRepository();

userRepository.setDatabase(db); // Внедрение зависимости через сеттер

Через свойства (Property Injection): Зависимости присваиваются напрямую публичным свойствам объекта после его создания.

class Database {

// ...

}

// public db; // Свойство для зависимости

constructor() {

// ...

}

// ...

}

userRepository.db = db; // Внедрение зависимости через свойство

Наиболее рекомендуемым способом является внедрение через конструктор, так как это гарантирует наличие всех необходимых зависимостей при создании объекта и делает зависимости очевидными. В некоторых случаях используются сеттеры или свойства, например, при внедрении опциональных зависимостей или в фреймворках, использующих Property Injection.

Деплой (или развертывание) - это процесс доставки, установки и настройки приложения на целевую среду (серверы, облачные платформы и т.д.) с целью сделать его доступным для конечных пользователей или других систем.

Ключевые этапы деплоя могут включать:

Сборка (Build): Создание исполняемых артефактов (модули, скомпилированный код). В Node.js это может быть просто копирование исходного кода, либо транспиляция (например, из TypeScript) и упаковка.

Тестирование (Testing): Проверка работоспособности артефактов на различных уровнях (модульное, интеграционное).

Упаковка (Packaging): Создание дистрибутива приложения (например, Docker-образ, архив).

Развертывание (Deployment): Копирование упакованного приложения на целевую среду.

Настройка (Configuration): Установка специфических для среды параметров (переменные окружения, настройки базы данных).

Запуск (Runtime): Старт процессов приложения.

Мониторинг (Monitoring): Наблюдение за работой приложения после деплоя.

В контексте Node.js деплой часто включает:

Установку зависимостей с помощью npm install или yarn install.

Настройку веб-сервера (например, Nginx или Caddy) для проксирования запросов к Node.js процессу.

Использование менеджеров процессов, таких как PM2 или Forever, для поддержания жизненного цикла приложения и автоматического перезапуска в случае сбоев.

Настройку переменных окружения для различных сред (разработка, стейджинг, продакшн).

Примеры сред деплоя для Node.js:

Виртуальные машины (AWS EC2, DigitalOcean Droplets).

Платформы как сервис (Heroku, Render, Vercel).

Контейнерные платформы (Docker, Kubernetes).

Функции как сервис (AWS Lambda, Google Cloud Functions).

Браузер начнет загружать изображение, но не будет знать его точных размеров до полной загрузки. Это может привести к следующим проблемам:

Сдвиг контента (Layout Shift): При загрузке изображения без указанных размеров, его отображение может сдвинуть остальной контент страницы. Это негативно влияет на пользовательский опыт и метрики производительности, такие как Cumulative Layout Shift (CLS).

Повторная отрисовка (Repaint): Браузеру приходится пересчитывать и перерисовывать компоновку страницы после получения фактических размеров изображения.

Рекомендуется всегда указывать размеры изображения с помощью атрибутов width и height или использовать CSS для выделения места под изображение до его полной загрузки.

Или с помощью CSS:

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

Используется для создания более динамичных и интерактивных веб-приложений, позволяя обновлять только часть контента страницы в ответ на действия пользователя или получение свежих данных. Это улучшает производительность и пользовательский опыт.

JavaScript'ий примитивный тип, представляющий не-числовое значение. Возникает при неудачных попытках преобразовать нечисловые значения в числа или при выполнении математических операций, результат которых не является действительным числом.

Основные свойства:

typeof NaN равно "number".

NaN не равно самому себе (NaN === NaN ложно).

Единственный способ проверить на NaN — использовать isNaN() или Number.isNaN().

Примеры получения NaN:

Примеры проверки на NaN:

Обычные задачи (macrotasks) выполняются после завершения текущего макроцикла Event Loop. Микрозадачи (microtasks) выполняются после завершения текущей функции, но до старта следующего макроцикла.

К обычным задачам относятся: скрипты, таймеры (setTimeout, setInterval), операции ввода/вывода.

К микрозадачам относятся: промисы (.then(), .catch(), .finally()), process.nextTick(), queueMicrotask(), MutationObserver (в браузерах).

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

Порядок вывода:

Основные отличия:

Выражение в JavaScript — это любой фрагмент кода, который производит значение.

Примеры выражений:

Литералы (строки, числа, массивы, объекты):// Числовой литерал

10

// Строковый литерал

"hello"

// Литерал массива

[1, 2, 3]

// Литерал объекта

{ name: "Alice" }

Идентификаторы (имена переменных):// Идентификатор переменной, возвращает значение переменной

myVariable

Арифметические и логические операции:// Арифметическое выражение

5 + 3 * 2

// Логическое выражение

a > b && c < d

Вызовы функций:// Вызов функции, возвращает значение, которое вернула функция

myFunction(arg1, arg2)

Присваивание (значение выражения присваивания — это присвоенное значение):// Выражение присваивания

x = 10

Тернарный оператор:// Тернарное выражение

condition ? value1 : value2

В отличие от операторов, выражения всегда возвращают значение.

Оператор == (нестрогое сравнение) выполняет приведение типов перед сравнением значений.

Оператор === (строгое сравнение) не выполняет приведение типов и сравнивает значения и типы данных без изменений.

Примеры:

Как правило, рекомендуется использовать === для более предсказуемого поведения и предотвращения неявного приведения типов, которое может привести к ошибкам.

Пул потоков (thread pool) – это набор рабочих потоков, которые могут быть использованы для выполнения задач. Вместо создания нового потока для каждой задачи, задачи ставятся в очередь, и свободные потоки из пула берут их на выполнение.

Основные преимущества:

Снижение накладных расходов: Создание и уничтожение потоков требует ресурсов. Пул потоков минимизирует эти затраты.

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

Повышение производительности: Потоки готовы к работе сразу, без задержек на создание.

В Node.js пул потоков используется для выполнения блокирующих операций, таких как файловые операции, сетевые запросы к СУБД и криптографические вычисления. Это реализуется через модуль libuv, часть которого отвечает за работу с пулом.

Пример использования (абстрактный, в Node.js прямое управление пулом потоков через libuv обычно не требуется, это делается автоматически для части API):

Размер пула потоков по умолчанию в Node.js составляет 4. Его можно изменить с помощью переменной окружения UV_THREADPOOL_SIZE.

Не следует путать потоки пула с потоками Node.js (workers), которые предназначены для выполнения CPU-интенсивных JavaScript-операций. Пул потоков libuv используется для нативных блокирующих операций.

EventEmitter - класс в Node.js, который лежит в основе многих встроенных модулей (например, http, fs), и используется для реализации событийной модели.

Основные возможности:

Подписка на события: Позволяет регистрировать функции (обработчики), которые будут выполняться при возникновении определенного события.

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

Передача данных: При эмиссии события можно передавать любые данные обработчикам.

Основные методы:

emitter.on(eventName, listener): Добавляет обработчик listener для события eventName.

emitter.once(eventName, listener): Добавляет одноразовый обработчик listener для события eventName. Он будет вызван только один раз при первом срабатывании события.

emitter.emit(eventName[, ...args]): Вызывает всех обработчиков, зарегистрированных для события eventName, передавая им аргументы ...args.

emitter.removeListener(eventName, listener): Удаляет конкретный обработчик listener для события eventName.

emitter.removeAllListeners([eventName]): Удаляет все обработчики для события eventName. Если eventName не указан, удаляет всех обработчиков для всех событий.

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

EventEmitter реализует паттерн "Наблюдатель". Это позволяет строить слабосвязанные системы, где компоненты взаимодействуют через события, не зная о внутренней реализации друг друга.

package.json — это манифест проекта Node.js, содержащий метаданные, необходимые для управления зависимостями, сборки и запуска приложения.

Основные поля и их роль:

name: Название проекта.

version: Текущая версия проекта.

description: Краткое описание проекта.

keywords: Массив ключевых слов для описания проекта.

homepage: URL домашней страницы проекта.

bugs: Объект с информацией о баг-трекере.

license: Тип лицензии проекта.

author: Информация об авторе проекта.

contributors: Список контрибьюторов.

files: Массив файлов, включаемых при публикации пакета.

main: Точка входа в приложение (основной файл).

browser: Точка входа для клиентской части (если применимо).

bin: Команды, которые должны быть доступны глобально при установке пакета.

man: Пути к файлам man-страниц.

directories: Объект с путями к директориям проекта (например, lib, bin, man, doc).

repository: Объект с информацией о репозитории исходного кода.

scripts: Объект, содержащий скрипты, которые можно выполнять с помощью npm.

config: Объект для настроек, специфичных для скриптов.

dependencies: Объект, содержащий зависимости, необходимые для работы проекта в продакшене.

devDependencies: Объект, содержащий зависимости, необходимые только для разработки и тестирования.

peerDependencies: Зависимости, от которых пользователь пакета тоже должен зависеть.

bundledDependencies: Массив имен зависимостей, которые должны быть упакованы с пакетом.

optionalDependencies: Зависимости, которые могут быть установлены, но их отсутствие не приведет к ошибке установки.

engines: Объект, указывающий версии Node.js и npm, поддерживаемые проектом.

os: Массив операционных систем, на которых работает проект.

cpu: Массив архитектур CPU, на которых работает проект.

preferGlobal: Булево значение, указывающее, должен ли пакет устанавливаться глобально.

private: Булево значение, указывающее, является ли пакет приватным и не должен ли быть опубликован в npm.

publishConfig: Настройки для публикации пакета в npm.

package.json играет ключевую роль в управлении зависимостями (установка, обновление, удаление), определении скриптов для автоматизации задач (запуск, тестирование, сборка), предоставлении метаданных о проекте для npm и других инструментов, а также в обеспечении воспроизводимости сборки проекта.

Да, Event Loop может быть заблокирован.

Это происходит, когда в колбэке выполняется ресурсоемкая или длительная синхронная операция, которая не возвращает управление Event Loop достаточно долго. Например, выполнение сложных вычислений или блокирующие операции ввода-вывода, такие как синхронное чтение большого файла.

Последствия блокировки:

Сервер перестает отвечать на запросы (для веб-сервера).

Таймеры (setTimeout, setInterval) отрабатывают с задержкой или вовсе не срабатывают вовремя.

Другие ожидающие события (например, сетевые) не обрабатываются.

Общий отклик приложения резко ухудшается.

Примеры блокирующих операций:

Для предотвращения блокировки следует использовать асинхронные API Node.js:

Использовать асинхронные версии модулей (fs.readFile вместо fs.readFileSync).

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

Использовать воркеры (worker_threads) для выполнения ресурсоемких вычислений в отдельном потоке.

Применять асинхронные структуры данных и алгоритмы.

Тег <img> используется для вставки изображения как отдельного содержимого на страницу. Это семантически значимый элемент, который браузеры и поисковые системы распознают как изображение. Он поддерживает атрибуты alt, что важно для SEO и доступности.

Задание фона через CSS (background-image) применяется для декоративных целей — фоновые изображения не считаются содержимым страницы и не индексируются как отдельные элементы. Они не поддерживают альтернативный текст и не влияют на SEO напрямую.

Основные отличия:

<img> добавляет изображение в поток документа, влияет на разметку и доступен для взаимодействия.

Фоновое изображение через CSS не влияет на структуру документа и служит для оформления.

Пример:

С точки зрения производительности, <img> позволяет браузеру лучше управлять загрузкой и кэшированием, а фоновые изображения могут быть загружены только при необходимости (например, при отображении элемента).

Смещение макета (Layout Shift) — это внезапные изменения в расположении элементов на веб-странице во время загрузки, вызывающие скачки контента. Информацию о нем можно найти в:

Google Search Console: В разделе "Core Web Vitals" отображаются данные о CLS (Cumulative Layout Shift) вашего сайта, показывая страницы с плохими показателями.

Отчеты Lighthouse: Инструмент аудита в Chrome DevTools (и в GSC) включает метрику CLS и предлагает рекомендации по ее улучшению.

Web Vitals Report (Google): Специализированный отчет, предоставляющий подробные данные о Web Vitals (включая CLS) для вашего сайта.

PageSpeed Insights: Онлайн-инструмент от Google, анализирующий производительность страницы, включая CLS, и дающий рекомендации.

Chrome DevTools Performance Tab: Позволяет визуализировать процесс рендеринга и выявлять моменты смещения макета. В разделе "Experience" можно увидеть "Layout Shifts".

Блог Google Developers: Часто публикует статьи и руководства по оптимизации Web Vitals, включая подробное объяснение CLS и способы его исправления.

Спецификация Layout Instability API: Техническое описание API, используемого для измерения смещений макета.

MDN Web Docs: Предоставляет документацию по Layout Instability API и сопутствующим темам.

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

Отчеты Lighthouse и PageSpeed Insights предоставляют конкретные данные о величине смещения и предлагают потенциальные причины, такие как изображения без указанных размеров, динамически добавляемый контент или шрифты, вызывающие мигание текста (FOIT/FOUT).

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

Основные преимущества:

Низкая задержка: Данные передаются практически мгновенно.

Эффективность: Уменьшается накладные расходы на установку соединения.

Двусторонняя связь: Сервер может отправлять данные клиенту без явного запроса.

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

Чаты и мессенджеры

Онлайн-игры

Торговые платформы в реальном времени

Мониторинг данных (например, биржевые котировки)

Протокол веб-сокетов начинается с обычного HTTP-запроса, который содержит заголовок Upgrade: websocket. Если сервер поддерживает веб-сокеты, он отвечает с подтверждением обновления протокола, и соединение переключается на протокол веб-сокетов.

Пример установки соединения на стороне клиента (браузер):

Пример обработки соединения на стороне сервера (Node.js с использованием библиотеки ws):

Backend-сервер для REST API или GraphQL API.

Сервисы реального времени (чаты, онлайн-игры) с использованием WebSocket.

Микросервисы.

Инструменты сборки фронтенда (Webpack, Gulp).

Скрипты автоматизации (CI/CD, обработка данных).

Консольные утилиты.

Использование в качестве платформы для бессерверных функций (serverless functions) (напр., AWS Lambda, Google Cloud Functions).

Применение в IoT (Internet of Things) для обработки данных с устройств.

Память в JavaScript управляется автоматически. Она делится на две основные области:

Куча (Heap): Здесь хранятся объекты и функции. Память распределяется динамически при создании этих сущностей.

Стек (Stack): Здесь хранятся примитивные значения (строки, числа, булевы, null, undefined, Symbol, BigInt) и ссылки на объекты в куче. Стек работает по принципу LIFO (Last-In, First-Out).

Когда ссылка на объект больше не используется, сборщик мусора (Garbage Collector) освобождает память в куче. Алгоритмы сборки мусора могут варьироваться, но часто применяются маркировка и очистка (Mark and Sweep):

Маркировка: Сборщик мусора сканирует все достижимые из корневых узлов (глобальные переменные, локальные переменные текущей функции) объекты и помечает их как "живые".

Очистка: Все непомеченные объекты (недостижимые из корневых узлов) считаются "мертвыми" и удаляются из памяти.

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

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

В браузере состояние (state) — это данные, которые сохраняются между запросами и взаимодействиями пользователя с веб-страницей. Поскольку HTTP — протокол без состояния, браузер использует несколько механизмов для хранения состояния:

Cookies — маленькие текстовые файлы, которые браузер отправляет на сервер с каждым запросом. Используются для аутентификации, сессий и персонализации.

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

SessionStorage — похож на LocalStorage, но данные живут только в рамках одной вкладки и удаляются при закрытии вкладки.

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

Да, напрямую нет. Node.js — это среда выполнения для JavaScript. Для выполнения кода на TypeScript его необходимо предварительно компилировать в JavaScript с помощью компилятора TypeScript (tsc).

Возможные способы выполнения TypeScript в Node.js:

Предварительная компиляция: Компиляция всего проекта перед запуском командой tsc. Затем запуск полученных .js файлов с помощью node.

# Установка компилятора

npm install -g typescript

# Компиляция

tsc

# Запуск

node dist/index.js

Использование ts-node: Инструмент для выполнения TypeScript без предварительной компиляции. Он компилирует файлы на лету. Удобен для разработки и отладки.

# Установка

npm install -D ts-node typescript @types/node

# Запуск

npx ts-node src/index.ts

Использование bundlers (Webpack, Parcel, esbuild): Эти инструменты могут компилировать TypeScript и собирать код в один или несколько файлов JavaScript, оптимизированных для запуска в Node.js или браузере.

Таким образом, Node.js выполняет только JavaScript, но инструменты экосистемы TypeScript позволяют бесшовно использовать TypeScript в Node.js проектах.

Node.js использует движок V8 от Google, который компилирует JavaScript в машинный код. Node.js предоставляет среду выполнения с неблокирующим вводом/выводом (I/O) через цикл событий (event loop).

Основные компоненты и принципы работы:

V8 Engine: Парсит, компилирует и выполняет JavaScript-код.

Libuv: Кроссплатформенная библиотека, предоставляющая асинхронный I/O, управление потоками, событиями и таймерами. Именно она обеспечивает неблокирующую работу.

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

Single-threaded Model: Сам JavaScript-код выполняется в одном потоке. Однако Node.js может использовать пул потоков для выполнения тяжелых операций ввода/вывода вне основного потока, чтобы не блокировать его.

Пример асинхронной операции:

В этом примереfs.readFile является асинхронной операцией. Node.js передает запрос на чтение файла в Libuv, который выполняет его. Основной поток продолжает выполнение console.log('Это сообщение выведется раньше...');. Когда чтение файла завершается, Libuv помещает соответствующее событие в очередь событий. Event loop подхватывает это событие и вызывает колбэк-функцию с содержимым файла.

Основные фазы (упрощенно) Event Loop:

timers: Выполнение колбэков setTimeout() и setInterval().

pending callbacks: Выполнение колбэков отложенных операций системы.

idle, prepare: Внутренние операции Node.js.

poll: Ожидание новых событий I/O; выполнение колбэков I/O.

check: Выполнение колбэков setImmediate().

close callbacks: Выполнение колбэков закрытия (например, socket.on('close', ...))

Node.js позволяет эффективно обрабатывать большое количество одновременных запросов за счет неблокирующего I/O и Event Loop, что делает его подходящим для создания высокопроизводительных сетевых приложений.

Да, можем, но это не рекомендуется для продакшена.

HTTP/2 может работать поверх TCP как без шифрования (cleartext HTTP/2), так и в зашифрованном виде (HTTP/2 over TLS).

Основные причины, почему cleartext HTP/2 редки и не рекомендованы:

Совместимость с браузерами: Большинство современных браузеров (Chrome, Firefox, Edge и др.) поддерживают HTTP/2 только поверх TLS. Это связано с тем, что существующая инфраструктура (промежуточные прокси, брандмауэры) может некорректно обрабатывать HTTP/2 без TLS, используя старые механизмы согласования протокола (например, Upgrade-хидер). Переход на HTTP/2 over TLS (через ALPN) более надежен.

Безопасность: HTTP/2, как и HTTP/1.1, чувствителен к атакам типа "Man-in-the-Middle". TLS обеспечивает целостность и конфиденциальность данных.

Стандартизация: Хотя стандарт HTTP/2 (RFC 7540) описывает оба варианта, де-факто стандартом стала реализация поверх TLS.

Технически, для запуска cleartext HTTP/2 на сервере Node.js можно использовать модули, поддерживающие эту функциональность (например, встроенный http2 с опцией allowHTTP1 или сторонние библиотеки, если требуется более низкоуровневый контроль).

Пример минимального сервера на Node.js с cleartext HTTP/2:

Однако, такой сервер не будет доступен по HTTP/2 из большинства стандартных браузеров без дополнительных настроек (например, принудительного использования h2c через специальные тулы или нестандартное ПО).

В продакшене всегда следует использовать HTTP/2 поверх TLS (HTTPS) для совместимости, безопасности и надежного согласования протокола с клиентами.

Map в JavaScript используется для хранения пар "ключ-значение". Ключи могут быть любых типов данных (включая объекты и примитивы), в отличие от обычных объектов, где ключи преобразуются в строки. Это делает Map удобным для случаев, когда ключи не являются строками или когда требуется сохранить порядок вставки элементов. Map также имеет явный метод size для определения количества элементов и лучшую производительность при частых добавлениях/удалениях элементов по сравнению с объектами.

Перенаправление в HTTP — это механизм, при котором веб-сервер сообщает клиенту (браузеру), что запрошенный ресурс перемещен на другой URL. Клиент затем автоматически выполняет новый запрос по указанному адресу. Основные коды состояния HTTP для перенаправления:

301 Moved Permanently: Ресурс был окончательно перемещен. Клиенту следует использовать новый URL для будущих запросов. Поисковые системы передают ссылочный вес.

302 Found (ранее "Moved Temporarily"): Ресурс временно доступен по другому адресу. Клиенту следует продолжать использовать исходный URL для будущих запросов. Поисковые системы обычно не передают ссылочный вес.

303 See Other: Ответ на запрос, обычно POST, предписывающий клиенту получить ресурс по другому URL с использованием метода GET.

307 Temporary Redirect: Аналогичен 302, но не позволяет сменить метод HTTP (например, POST остается POST).

308 Permanent Redirect: Аналогичен 301, но не позволяет сменить метод HTTP.

Процесс:

Клиент отправляет запрос к URL A.

Сервер, вместо отправки содержимого URL A, возвращает HTTP-ответ с кодом состояния перенаправления (например, 301) и заголовком Location, содержащим новый URL (URL B).

Клиент (браузер) автоматически извлекает URL B из заголовка Location и отправляет новый запрос к URL B.

Сервер обрабатывает запрос к URL B и возвращает соответствующий ответ (например, 200 OK).

В Node.js с использованием Express.js перенаправление выполняется методом res.redirect():

Заголовки:

Перенаправления используются для исправления опечаток в URL, перемещения веб-сайтов, обеспечения HTTPS, слияния страниц и URL-шоутенинга.

Основные компоненты NestJS:

Модули (Modules): Организуют код приложения в логические группы. Каждый модуль может экспортировать и импортировать другие модули.

Контроллеры (Controllers): Обрабатывают входящие запросы и возвращают ответы. Определяют маршруты и привязывают их к методам класса.

Провайдеры (Providers): Служат для внедрения зависимостей. Могут быть сервисами, репозиториями, фабриками или любыми пользовательскими классами.

Сервисы (Services): Инкапсулируют ключевую логику бизнеса, доступную для использования в контроллерах и других провайдерах.

Также используются:

Мидлвары (Middleware): Функции, которые выполняются до обработчика маршрута. Используются для аутентификации, логирования, модификации запросов и ответов.

Пайпы (Pipes): Преобразуют входящие данные и валидируют их.

Гарды (Guards): Определяют, имеет ли пользователь доступ к конкретному маршруту или ресурсу.

Интерсепторы (Interceptors): Позволяют привязывать дополнительную логику до и после выполнения метода, например, трансформировать результат или обрабатывать исключения.

Фильтры исключений (Exception Filters): Перехватывают необработанные исключения и отправляют форматированный ответ клиенту.

Основные идеи Node.js:

Неблокирующий ввод/вывод (Async I/O): Использование асинхронной модели операций ввода/вывода вместо традиционной блокирующей. Это позволяет Node.js эффективно обрабатывать большое количество одновременных соединений без создания отдельного потока для каждого запроса, что значительно снижает накладные расходы на переключение контекста и использование памяти.

// Пример асинхронного чтения файла

const fs = require('fs');

fs.readFile('file.txt', 'utf8', (err, data) => {

if (err) {

console.error(err);

return;

}

console.log(data);

});

console.log('Эта строка выполнится до завершения чтения файла'); // Иллюстрация неблокирующего поведения

Однопоточная архитектура: Основной цикл событий (Event Loop) Node.js работает в одном потоке для обработки запросов. Неблокирующий ввод/вывод позволяет этому единственному потоку не простаивать в ожидании завершения операций, а обрабатывать другие задачи, что повышает производительность при высоких нагрузках.

Цикл событий (Event Loop): Фундаментальный механизм, управляющий асинхронными операциями. Он постоянно проверяет очередь событий и отправляет их на обработку, когда стек вызовов свободен. Библиотека libuv отвечает за реализацию цикла событий и взаимодействие с операционной системой.

JavaScript как основной язык: Использование JavaScript позволяет разработчикам использовать один язык как на фронтенде (в браузере), так и на бэкенде (на сервере), упрощая разработку и переиспользование кода.

Модульная система (CommonJS): В Node.js была реализована модульная система (изначально на базе CommonJS, сейчас также поддерживается ES Modules), позволяющая разбивать код на небольшие, переиспользуемые модули, что способствует лучшей организации кода и его поддержке.

// export.js

exports.myFunction = () => {

console.log('Hello from module!');

};

// import.js

const myModule = require('./export');

myModule.myFunction(); // Вызов функции из импортированного модуля

Виртуальная машина V8: Использование высокопроизводительной виртуальной машины V8 от Google (той же, что используется в браузере Chrome) для выполнения JavaScript-кода. Это обеспечивает высокую скорость выполнения и постоянное улучшение производительности благодаря усилиям Google.

Фокус на масштабируемые сетевые приложения: Node.js изначально создавался с целью облегчить разработку высокопроизводительных и масштабируемых сетевых приложений, таких как веб-серверы и API.

Libpq — это библиотека C, предоставляющая низкоуровневый API для взаимодействия с базой данных PostgreSQL. В Node.js она обычно используется через биндинги или драйверы, такие как node-postgres (pg), которые обертывают ее функциональность для удобного использования в JavaScript.

Основные возможности и концепции:

Соединение: Установление и управление подключениями к серверу PostgreSQL.

Выполнение команд: Отправка SQL-запросов и получение результатов.

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

Параметризованные запросы: Защита от SQL-инъекций.

Асинхронный режим: Поддержка неблокирующих операций ввода-вывода.

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

Для работы с базами данных в Node.js я использую следующие инструменты и технологии:

ORM/ODM:

Sequelize: Для реляционных баз данных (PostgreSQL, MySQL, SQLite, SQL Server). Предоставляет мощный набор функций для маппинга объектов на реляционные таблицы, транзакций, миграций и валидации.

Mongoose: Для баз данных MongoDB. Объектно-документное маппирование, схемы данных, валидация, запросы.

Клиенты баз данных:

pg: Нативный клиент для PostgreSQL.

mysql2: Более современный и быстрый клиент для MySQL по сравнению с mysql.

Инструменты миграции:

sequelize-cli: Используется в связке с Sequelize для управления миграциями реляционных баз данных.

migrate-mongo: Инструмент для управления миграциями в MongoDB.

Инструменты управления базами данных (GUI):

DBeaver: Универсальный инструмент для различных типов баз данных.

MongoDB Compass: Официальный GUI для MongoDB.

Кэширование:

Redis: Часто использую для кэширования данных из базы данных, что снижает нагрузку на основную БД и ускоряет ответы.

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

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

Выбор конкретного инструмента зависит от типа базы данных (реляционная или NoSQL), требований проекта к абстракции и сложности запросов, а также предпочтений команды.

В JavaScript к ложным (falsy) значениям относятся те, которые при приведении к булевому типу становятся false. К ним относятся:

false

0 (число ноль)

-0 (отрицательный ноль)

0n (BigInt ноль)

"" (пустая строка)

null

undefined

NaN

Все остальные значения считаются истинными (truthy).

Пример:

Weak reference — это ссылка на объект, которая не мешает сборщику мусора удалить этот объект, если на него больше нет других "сильных" ссылок. Если объект удален, weak reference становится невалидной. Основное применение — кеширование объектов, связанных с другими объектами, или реализация слабых коллекций (например, WeakMap, WeakSet).

В JavaScript weak references реализуются через:

WeakMap: Коллекция "ключ-значение", где ключи — объекты (на них ссылаются слабо), а значения могут быть любыми. Если ключ удален сборщиком мусора, соответствующая пара "ключ-значение" также удаляется из WeakMap.

WeakSet: Коллекция объектов, на которые ссылаются слабо. Если объект удален сборщиком мусора, он также удаляется из WeakSet.

WeakRef: Позволяет создавать слабую ссылку на любой объект. Можно попытаться получить значение слабой ссылки с помощью метода .deref(). Если объект был удален, .deref() вернет undefined.

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

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

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

Обнаруженная проблема: Описание симптомов блокировки Event Loop.

Предварительный анализ: Выявленные причины (тяжелые синхронные операции, бесконечные циклы, долгие I/O без асинхронности).

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

Влияние: Описание последствий (задержки ответов API, невозможность обработки новых запросов, зависания приложения).

Предлагаемые шаги по устранению:

Идентификация точного места блокировки (с помощью prof инструментов, логов).

Рефакторинг синхронных операций в асинхронные.

Использование worker threads для CPU-интенсивных задач.

Оптимизация алгоритмов.

Применение неблокирующих I/O операций.

Добавление метрик для мониторинга загрузки Event Loop.

Сроки (оценочно): Примерная оценка времени для внесения изменений и тестирования.

Необходимая помощь/Ресурсы: Если требуется участие других членов команды или дополнительные инструменты.

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

Основные методы Promise:

.then(): Обрабатывает успешное выполнение Promise и возвращает новый Promise. Принимает функцию-обработчик для успешного результата.

.catch(): Обрабатывает отклонение Promise и возвращает новый Promise. Принимает функцию-обработчик для ошибки. Является синтаксическим сахаром для .then(null, rejectionHandler).

.finally(): Выполняет функцию независимо от результата Promise (успех или ошибка). Удобен для очистки ресурсов.

Promise.all(iterable): Возвращает новый Promise, который выполняется, когда выполняются все Promise в переданном итерируемом объекте. Отклоняется, если хотя бы один из Promise отклоняется.

Promise.race(iterable): Возвращает новый Promise, который выполняется или отклоняется, как только выполняется или отклоняется любой из Promise в переданном итерируемом объекте.

Promise.resolve(value): Возвращает Promise, который выполняется с указанным значением.

Promise.reject(reason): Возвращает Promise, который отклоняется с указанной причиной.

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

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

Существуют три основных типа внедрения зависимостей:

Внедрение через конструктор (Constructor Injection): Зависимости передаются в конструктор объекта.

Внедрение через сеттер (Setter Injection): Зависимости передаются через публичные методы-сеттеры.

Внедрение через интерфейс (Interface Injection): Объект реализует интерфейс, который содержит метод для внедрения зависимости. (Менее распространенный тип в Node.js)

Пример внедрения через конструктор:

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

Тестируемость: Легче подменять реальные зависимости моками или заглушками.

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

Сопровождаемость: Изменения в одной части системы меньше влияют на другие.

Переиспользование: Компоненты становятся более переиспользуемыми.

В Node.js для реализации DI часто используются фреймворки, такие как NestJS (использует свой IoC-контейнер) или библиотеки типа InversifyJS.

В функциональных компонентах, в отличие от классовых, отсутствуют следующие классические методы жизненного цикла:

componentDidMount

componentDidUpdate

componentWillUnmount

shouldComponentUpdate

getSnapshotBeforeUpdate

componentDidCatch

getDerivedStateFromProps (реализуется иначе)

Однако, их функциональность можно воспроизвести или заменить с помощью хуков, таких как useEffect, useLayoutEffect, useCallback, useMemo, useReducer, useRef и других.

Сложности с отладкой из-за неявного связывания контекста (this).

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

Увеличение потребления памяти из-за хранения замыканий.

Возможные конфликты имен переменных в замыканиях.

Не всегда очевидное поведение при асинхронных операциях.

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

Основные аспекты:

Производительность: Ускорение выполнения операций, уменьшение времени отклика.

Использование ресурсов: Снижение потребления CPU, оперативной памяти, пропускной способности сети.

Энергоэффективность: Уменьшение энергопотребления (актуально для мобильных и IoT устройств).

Размер кода: Уменьшение размера исполняемых файлов или объема данных.

Примеры методов оптимизации:

Алгоритмическая: Выбор более эффективных алгоритмов и структур данных.

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

Оптимизация компилятором: Использование флагов оптимизации при компиляции.

Кэширование: Хранение часто используемых данных для быстрого доступа.

Параллелизм/Многопоточность: Распределение нагрузки между несколькими ядрами CPU.

Важно: Преждевременная оптимизация может быть вредной. Следует сначала убедиться в наличии проблем с производительностью, используя инструменты профилирования.

Да, можно.

Для хранения данных сессии вместо cookie можно использовать:

URL Rewriting: Добавлять идентификатор сессии в URL каждого запроса.

Скрытые поля формы: Передавать идентификатор сессии в скрытом поле <input type="hidden"> при отправке формы.

HTTP-заголовки: Отправлять данные сессии в кастомных HTTP-заголовках.

Локальное хранилище (LocalStorage) или хранилище сессии (SessionStorage) в браузере: Хранить идентификатор сессии на стороне клиента и отправлять его с каждым запросом.

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

Идемпотентность HTTP-метода означает, что многократное выполнение одного и того же запроса с теми же параметрами приведет к тому же результату на сервере, как и однократное выполнение. Результат может быть успешным или неуспешным, но состояние сервера после выполнения не изменится.

Применение в веб-разработке:

GET: Идемпотентный. Многократное получение ресурса не меняет его состояние.

PUT: Идемпотентный. Многократная полная перезапись ресурса приводит к тому же конечному состоянию ресурса.

DELETE: Идемпотентный. Многократное удаление ресурса после первого успешного удаления не меняет состояние (ресурс остается удаленным).

POST: Не идемпотентный. Многократная отправка данных может привести к созданию нескольких одинаковых ресурсов или многократному выполнению действия.

PATCH: Не идемпотентный по умолчанию, но может быть идемпотентным, если операция частичного обновления определена как идемпотентная.

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

Пример неидемпотентной операции с POST:

Пример идемпотентной операции с PUT:

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

Переменные, объявленные с помощью var, имеют функциональную или глобальную область видимости. В цикле, например, замыкание захватывает последнее значение переменной var из-за подъема (hoisting).

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

В контексте замыкания это означает, что замыкание, захватившее переменную var, будет ссылаться на одно и то же место в памяти, которое содержит последнее значение переменной. Замыкание, захватившее переменную let, будет ссылаться на отдельное место в памяти для каждой итерации цикла.

Promise инициируется в момент создания нового экземпляра класса Promise. Функцию-исполнитель (executor), переданную в конструктор, среда выполнения JavaScript вызывает сразу же, синхронно.

Пример:

Бинарное дерево поиска (Binary Search Tree, BST) — это особый вид бинарного дерева данных, в котором каждый узел содержит ключ (значение), и для каждого узла выполняется следующее свойство:

Ключи всех узлов в левом поддереве меньше ключа данного узла.

Ключи всех узлов в правом поддереве больше ключа данного узла.

Левое и правое поддеревья также являются бинарными деревьями поиска.

Это свойство позволяет эффективно осуществлять операции поиска, вставки и удаления элементов. В сбалансированном бинарном дереве поиска эти операции выполняются за время O(log n), где n — количество узлов в дереве. В худшем случае (вырожденное дерево) время выполнения может достигать O(n), превращая дерево в связанный список.

Пример узла:

Основные операции:

Поиск: Начинается с корня. Если искомое значение меньше ключа текущего узла, переходим в левое поддерево; если больше — в правое; если равно — найден.

Вставка: Аналогично поиску, но когда достигается подходящее место (null), создается новый узел.

Удаление: Наиболее сложная операция. Зависит от количества потомков удаляемого узла (0, 1 или 2). При удалении узла с двумя потомками обычно заменяется наименьшим элементом из правого поддерева или наибольшим из левого.

Бинарные деревья поиска используются для создания словарей, множеств и для эффективной сортировки данных (сортировка деревом). Для поддержания сбалансированности и гарантии логарифмического времени операций используются самобалансирующиеся варианты, такие как AVL-деревья или красно-черные деревья.

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

Главное отличие от события load:

DOMContentLoaded: Сработает, когда структура DOM готова.

load: Сработает только после полной загрузки всех ресурсов страницы (изображений, CSS, скриптов, если они не async/defer).

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

Это событие часто используется для выполнения JavaScript-кода, который манипулирует DOM, так как нет необходимости ждать загрузки всех внешних ресурсов.

Столкнулся с проблемами избыточной связанности между модулями, когда изменения в одном модуле вызывали необходимость доработок в других. Также возникали трудности с циклическими зависимостями, которые усложняли тестирование и поддержку кода. Проблемы с версионированием модулей приводили к конфликтам зависимостей в больших проектах. Наконец, некорректное управление областями видимости (scope) приводило к загрязнению глобального пространства имен или непредвиденному поведению.

Data-атрибуты позволяют встраивать произвольные данные в стандартные HTML-элементы без дополнительных хаков, вроде нестандартных атрибутов. Они определяются префиксом data-, за которым следует любое имя, содержащее только латиницу в нижнем регистре, цифры, дефисы, точки, двоеточия или подчеркивания.

Доступ к значению data-атрибутов можно получить через DOM Manipulation

или CSS.

Пример:

В JavaScript:

В CSS:

Использование data-атрибутов:

Хранение данных, специфичных для пользовательского интерфейса.

Передача данных от HTML к JavaScript.

Определение стиля элементов на основе их данных.

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

Валидный HTML5.

Четкое разделение контента и представления.

Удобный доступ и манипулирование данными.

Недостатки:

Не предназначены для хранения больших объемов данных.

Могут быть менее производительными по сравнению с другими методами в определенных сценариях.

JS-скрипты без атрибутов async или defer, расположенные в <head>.

CSS-файлы без атрибутов media="print" или media="all", расположенные в <head>.

CSS с @import.

Пересечение массивов — это операция нахождения всех элементов, которые присутствуют одновременно в двух или более массивах.

Основные подходы к реализации:

Использование filter и includes: Простой и читаемый подход для небольших массивов. Производительность может снижаться для больших массивов из-за вложенных циклов (filter и includes в худшем случае).const intersection = array1.filter(element => array2.includes(element));

Использование Set: Более эффективный подход для больших массивов, так как проверка наличия элемента в Set имеет временную сложность O(1) в среднем.const set2 = new Set(array2);

const intersection = array1.filter(element => set2.has(element));

Сортировка и два указателя: Если массивы отсортированы, можно использовать два указателя для линейного прохождения по обоим массивам. Имеет временную сложность O(n log n) за счет сортировки, затем O(n).const sortedArray1 = [...array1].sort((a, b) => a - b);

const sortedArray2 = [...array2].sort((a, b) => a - b);

const intersection = [];

let i = 0;

let j = 0;

while (i < sortedArray1.length && j < sortedArray2.length) {

if (sortedArray1[i] === sortedArray2[j]) {

intersection.push(sortedArray1[i]);

i++;

j++;

} else if (sortedArray1[i] < sortedArray2[j]) {

i++;

} else {

j++;

}

}

Выбор метода зависит от размера массивов и требований к производительности. Для небольших массивов filter и includes достаточно, для больших массивов предпочтительнее использовать Set или сортировку.

DOCTYPE (Document Type Declaration) — инструкция для веб-браузера о том, к какому типу документа относится текущий файл (например, HTML, XHTML) и какую версию спецификации использовать для его рендеринга. Он не является HTML-тегом, а скорее относится к синтаксису SGML/XML. Наличие DOCTYPE в начале HTML-документа критически важно для включения стандартного режима рендеринга в браузерах, который обеспечивает предсказуемое и единообразное отображение веб-страницы в соответствии со стандартами.

Примеры:

HTML-форма — интерактивный элемент веб-страницы для сбора данных пользователя. Она состоит из элементов управления (поля ввода, кнопки отправки, флажки, переключатели и т.д.), заключенных в тег <form>.

При отправке формы данные собираются из всех элементов управления внутри тега <form> и отправляются на указанный серверный адрес.

Ключевые атрибуты тега <form>:

action: URL-адрес, на который будут отправлены данные формы.

method: HTTP-метод, используемый для отправки данных (GET или POST).

GET: Данные добавляются к URL в виде параметров запроса. Видны в адресной строке. Подходят для запросов данных (например, поиска). Ограниченная длина.

POST: Данные отправляются в теле HTTP-запроса. Не видны в адресной строке. Подходят для отправки конфиденциальных данных или больших объемов.

Необходимые компоненты для отправки формы:

Тег <form>: Обертка для элементов формы.

Элементы управления: Входные поля (<input>), текстовые области (<textarea>), раскрывающиеся списки (<select>) и т.д., каждый из которых имеет атрибут name (для идентификации данных при отправке) и value (значение, которое будет отправлено).

Кнопка отправки: <button type="submit"> или <input type="submit">. Инициирует процесс отправки данных.

Атрибуты action и method: Определяют, куда и как будут отправлены данные.

Серверное приложение: Должно принимать и обрабатывать данные, отправленные формой. Node.js-приложение может использовать фреймворки, такие как Express, для парсинга данных формы из тела запроса (req.body для POST) или параметров URL (req.query для GET).

Пример простой формы:

Пример обработки в Node.js (с Express):

Существует несколько способов, самый надежный - использовать функцию Number.isNaN().

Number.isNaN():

Это наиболее предпочтительный способ, так как он правильно различает между NaN и другими нечисловыми значениями, которые при преобразовании к числу дают NaN (например, объекты).

// Возвращает true только для NaN

console.log(Number.isNaN(NaN)); // true

console.log(Number.isNaN(undefined)); // false

console.log(Number.isNaN({})); // false

console.log(Number.isNaN('abc')); // false

console.log(Number.isNaN(null)); // false

console.log(Number.isNaN(0 / 0)); // true

Number.isNaN():

isNaN() (глобальная функция):

Эта функция преобразует аргумент к числу перед проверкой. Из-за этого может возвращать true для значений, которые не являются NaN, но не могут быть преобразованы в число. Избегайте использования для строгой проверки на NaN.

// Возвращает true для NaN и некоторых нечисловых значений

console.log(isNaN(NaN)); // true

console.log(isNaN(undefined)); // true

console.log(isNaN({})); // true

console.log(isNaN('abc')); // true

console.log(isNaN(null)); // false

Сравнение с самим собой:

Спецификация JavaScript гласит, что NaN является единственным значением, которое не равно само себе. Этот метод надежен и работает, но Number.isNaN() более читабелен.

// true только если значение равно NaN

const value = NaN;

console.log(value !== value); // true

const anotherValue = 5;

console.log(anotherValue !== anotherValue); // false

Рекомендуется использовать Number.isNaN() для точной проверки на NaN.

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

Ключевые различия сведены в таблицу:

Важно отметить, что для объектов и массивов, объявленных с const, сама переменная не может быть переназначена к другому объекту/массиву, но содержимое объекта/массива может быть изменено.

DOM (Document Object Model) — это программный интерфейс для HTML, XML и SVG документов. Он предоставляет структурное представление документа в виде дерева узлов и определяет, каким образом программы могут получать доступ к его содержимому, структуре и стилям, а также изменять их.

Virtual DOM — это концепция, являющаяся программным представлением DOM в памяти. Он используется в библиотеках и фреймворках, таких как React, для оптимизации обновлений UI.

Сравнение:

Процесс работы Virtual DOM в React:

При изменении состояния компонента React создает новое дерево Virtual DOM.

Это новое дерево Virtual DOM сравнивается с предыдущим деревом (процесс "diffing").

Вычисляется минимальный набор изменений, необходимых для обновления настоящего DOM.

Эти изменения пакетируются и применяются к настоящему DOM (процесс "reconciliation").

Пример концепции Virtual DOM (упрощенно):

Редирект в HTTP — это механизм перенаправления клиента (браузера) с одного URL на другой.

Работает это следующим образом:

Клиент отправляет HTTP-запрос на исходный URL.

Сервер получает запрос и вместо отправки страницы для исходного URL, отправляет HTTP-ответ с одним из статус-кодов перенаправления (например, 301 - постоянное, 302 - временное).

В этом ответе сервер также включает заголовок Location с новым URL, на который нужно перенаправить клиента.

Браузер, получив ответ со статус-кодом перенаправления и заголовком Location, автоматически отправляет новый HTTP-запрос уже на URL, указанный в заголовке Location.

Примеры статус-кодов редиректа:

Пример HTTP-ответа с редиректом:

В Node.js редирект обычно реализуется через установку соответствующего статус-кода и заголовка Location в объекте ответа res.

Пример реализации в Express.js:

Пример реализации на чистом Node.js:

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

Основные цели использования Set:

Удаление дубликатов: Эффективный способ получить уникальные элементы из массива или другой итерируемой структуры.

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

Управление уникальными элементами: Удобно для задач, где важна уникальность каждого элемента.

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

Получение списка уникальных пользователей онлайн.

Хранение уникальных тегов для статьи.

Реализация алгоритмов, требующих работы с множествами (пересечение, объединение).

Set поддерживает методы для работы с коллекцией, такие как add(), delete(), has(), clear(), а также итерируется с помощью for...of или метода forEach().

ORM (Object-Relational Mapping) не находится на периферии разработки. Он является широко используемым инструментом, особенно в веб-фреймворках, для упрощения взаимодействия с реляционными базами данных. Периферийным он может казаться при работе с низкоуровневым доступом к БД или в специфических высоконагруженных системах, где критична максимальная производительность и полный контроль над SQL-запросами.

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

Абстракция: Позволяет работать с данными как с объектами, скрывая детали SQL.

Ускорение разработки: Сокращает количество шаблонного кода для операций CRUD.

Безопасность: Часто включает защиту от SQL-инъекций.

Переносимость: Облегчает переход между различными СУБД (хотя полная переносимость rarely достигается).

Сопровождение: Улучшает читаемость и поддерживаемость кода.

Недостатки ORM:

Производительность: Может генерировать неоптимальные SQL-запросы.

Сложность: Изучение и правильное использование ORM требует времени.

Ограничения: Не всегда подходит для сложных агрегаций or специфических запросов.

"Проблема N+1 запроса": Распространенная проблема производительности, связанная с ленивой загрузкой.

Типичные ORM для Node.js:

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

В итоге, ORM — это важный инструмент в арсенале Node.js разработчика для большинства приложений, где удобство и скорость разработки преобладают над необходимостью ювелирной настройки каждого запроса.

Пользователь вводит URL в адресную строку браузера или нажимает ссылку.

Браузер выполняет поиск IP-адреса хоста в DNS (Domain Name System).

Браузер устанавливает TCP-соединение с сервером веб-сайта по полученному IP-адресу.

Браузер отправляет HTTP-запрос на сервер, запрашивая HTML-документ.

Сервер обрабатывает запрос и отправляет HTTP-ответ с HTML-содержимым и метаданными (заголовками).

Браузер получает HTML-документ и начинает парсить его, строя Document Object Model (DOM).

При обнаружении в HTML ссылок на CSS-файлы, JavaScript-файлы и изображения, браузер делает дополнительные HTTP-запросы для их загрузки.

Браузер обрабатывает CSS, применяя стили к элементам DOM, создавая CSS Object Model (CSSOM).

После построения DOM и CSSOM, браузер объединяет их для создания Render Tree.

Браузер выполняет расчеты макета Render Tree (Layout/Reflow) для определения геометрических характеристик элементов.

Браузер раскрашивает (Paint) элементы на экране на основе Render Tree и информации о макете.

Браузер выполняет JavaScript-код, который может изменять DOM, CSSOM или запускать дополнительные HTTP-запросы (например, AJAX). Изменения приводят к повторному выполнению шагов 6-11.

call и apply вызывают функцию немедленно, устанавливая контекст this и передавая аргументы. call принимает аргументы по отдельности, а apply — массивом. bind возвращает новую функцию, с привязанным контекстом this и аргументами, не вызывая исходную функцию сразу.

Он фиксирует точную версию каждого пакета, включая их зависимости, используемых в проекте. Это гарантирует, что при установке зависимостей на другом компьютере или позже будут установлены те же версии, что и при создании файла package-lock.json, избегая проблем с версиями.

Функция первого рода (Higher-Order Function) в Node.js, как и в других языках с поддержкой функционального программирования, — это функция, которая:

Принимает одну или несколько функций в качестве аргументов.

Возвращает другую функцию в качестве результата.

Это позволяет создавать абстракции, комбинировать функции и управлять потоком выполнения более гибко.

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

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

Функции для работы с массивами: map, filter, reduce принимают функцию, которая применяется к каждому элементу массива.

Декораторы или wrapper функции: Функции, добавляющие дополнительную логику к другой функции.

Основные преимущества использования функций первого рода:

Повышение абстракции: Позволяют скрывать детали реализации и работать на более высоком уровне.

Повторное использование кода: Функции могут быть легко переиспользованы в различных контекстах.

Улучшение читаемости: Код становится более декларативным и понятным.

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

Spring — это комплексный фреймворк для создания корпоративных приложений на Java. Он предлагает множество модулей для различных задач, таких как управление транзакциями, безопасность, доступ к данным (через интеграцию с ORM, например, Hibernate). Ключевой аспект Spring — инверсия управления (IoC) и внедрение зависимостей (DI), которые делают код более модульным и тестируемым.

Hibernate — это мощный ORM (Object-Relational Mapping) фреймворк для Java. Он упрощает взаимодействие с базами данных, позволяя работать с объектами Java вместо того, чтобы писать SQL-запросы напрямую. Hibernate берёт на себя задачи мэппинга объектов на таблицы базы данных и выполнения SQL-операций.

В архитектуре приложений они часто используются вместе:

Уровень данных: Hibernate используется для взаимодействия с базой данных, предоставляя абстракцию над SQL.

Бизнес-логика/Сервисный уровень: Spring управляет жизненным циклом компонентов, внедряет зависимости (например, DAO, предоставленные Hibernate) в сервисы и управляет транзакциями на этом уровне.

Уровень представления (при необходимости): Spring MVC (часть Spring Framework) может использоваться для создания веб-интерфейса, который вызывает сервисы.

Внедрение Spring и Hibernate обеспечивает:

Ускорение разработки: Готовые решения для общих задач.

Тестируемость: IoC/DI делает компоненты легко тестируемыми.

Масштабируемость: Модульная структура позволяет масштабировать отдельные части приложения.

Сопровождаемость: Чёткое разделение задач и использование паттернов делают код проще для понимания и изменения.

Пример интеграции Spring и Hibernate:

В конфигурации Spring (часто через JavaConfig), настраивается SessionFactory Hibernate и сканируются компоненты (@Repository, @Service и т.д.) для внедрения зависимостей и управления транзакциями.

Async/await — это синтаксический сахар над промисами в JavaScript, который позволяет писать асинхронный код в синхронном стиле.

Они нужни для:

Улучшения читаемости кода: Асинхронный код становится более последовательным и понятным, избегая "callback hell".

Упрощения обработки ошибок: try...catch блоки работают с async/await так же, как и с синхронным кодом.

Более легкой отладки: Поток выполнения более очевиден.

Ключевые моменты:

Функция, помеченная async, всегда возвращает промис.

Оператор await можно использовать только внутри async функции. Он приостанавливает выполнение async функции до тех пор, пока промис, к которому он применяется, не выполнится или не будет отклонен, а затем возвращает результат или выбрасывает ошибку.

Стилизация чекбоксов и радиокнопок обычно требует скрытия нативного элемента и создания кастомного. Инпуты стилизуются напрямую.

Для чекбоксов/радиокнопок:

Скрываем нативный элемент (display: none; или opacity: 0; position: absolute;).

Используем псевдоэлементы (::before, ::after) или соседние элементы (<label>), чтобы визуально отобразить состояние (выбран/не выбран).

Стилизуем :checked состояние селектором input[type="checkbox"]:checked + label::before или аналогичным.

Для текстовых инпутов:

Стилизуем рамку (border, outline).

Настраиваем внутренние отступы (padding).

Меняем цвет текста (color) и фон (background-color).

Стилизуем плейсхолдер (::placeholder).

Особое внимание уделяем состояниям :focus и :disabled.

Пример стилизации чекбокса с использованием <label> и псевдоэлемента:

Пример стилизации текстового инпута:

Сессии и аутентификация. Управление доступом на основе ролей (RBAC) или атрибутов (ABAC). Валидация входящих данных. Защита от XSS, CSRF, SQL-инъекций. Безопасное хранение паролей (солирование и хеширование). Использование HTTPS. Обновление зависимостей и библиотек. Логирование событий безопасности. Регулярные аудиты кода и пентесты.

Пример валидации входящих данных:

Важные аспекты в таблице:

Виртуальный DOM был создан для оптимизации обновления пользовательского интерфейса (UI) в веб-приложениях, написанных на JavaScript. Основная проблема, которую он решает, заключается в высокой стоимости прямых манипуляций с реальным DOM.

Обновление реального DOM включает:

Пересчет стилей (Recalculate Style).

Перекомпоновку (Layout).

Перерисовку (Paint).

Эти операции могут быть трудоемкими, особенно при частых и комплексных изменениях UI, что приводит к снижению производительности и "торможению" приложения.

Виртуальный DOM представляет собой легковесное JavaScript-представление реального DOM. При изменении состояния приложения создается новый виртуальный DOM, который сравнивается с предыдущим (процесс, называемый diffing). Затем вычисляется минимальный набор изменений, необходимый для обновления реального DOM в наиболее эффективным образом.

Преимущества виртуального DOM:

Оптимизация производительности: Минимизация прямых манипуляций с реальным DOM.

Абстракция от реального DOM: Упрощает разработку, позволяя разработчикам думать о состоянии UI.

Кросс-платформенность: Может использоваться для рендеринга не только в браузере, но и на других платформах (например, React Native).

GET предназначен для запроса данных с сервера. Данные передаются в URL в виде параметров запроса (query parameters). Запросы GET идемпотентны и могут кэшироваться. POST используется для отправки данных на сервер для создания или обновления ресурса. Данные передаются в теле запроса. Запросы POST не идемпотентны и не кэшируются по умолчанию.

Предпочитаю использовать CSS-модули для компонентно-ориентированной разработки.

Преимущества CSS-модулей:

Локальная область видимости: Каждый файл CSS-модуля создает уникальные классы, избегая конфликтов стилей между компонентами.

Отсутствие побочных эффектов: Изменения в стилях одного компонента не влияют на другие.

Читаемость и поддерживаемость: Четко видно, какие стили применяются к конкретному компоненту.

Интеграция с JavaScript: Классовые имена импортируются как объекты, что удобно для динамического применения стилей.

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

Другие способы, такие как CSS-in-JS или BEM, также имеют свои плюсы, но для большинства проектов CSS-модули обеспечивают оптимальный баланс между изоляцией, производительностью и удобством разработки.

Основные виды промисов в Node.js:

Basic Promise: Представляет собой контейнер для будущих значений или ошибок. Имеет три состояния: pending (ожидание), fulfilled (выполнено успешно), rejected (отклонено с ошибкой). Создается с помощью конструктора new Promise().

// Создание базового промиса

const myPromise = new Promise((resolve, reject) => {

// Асинхронная операция

setTimeout(() => {

const success = true;

if (success) {

resolve("Операция успешно завершена");

} else {

reject("Произошла ошибка");

}

}, 1000);

});

Promise.resolve(): Создает промис, который немедленно переходит в состояние fulfilled с заданным значением.

// Создание resolved промиса

const resolvedPromise = Promise.resolve("Это уже выполненный промис");

Promise.reject(): Создает промис, который немедленно переходит в состояние rejected с заданной причиной ошибки.

// Создание rejected промиса

const rejectedPromise = Promise.reject(new Error("Это отклоненный промис"));

Promise.all(): Принимает массив промисов и возвращает новый промис. Этот новый промис переходит в состояние fulfilled, когда все промисы в массиве успешно завершены. Значением будут массивы результатов в том же порядке, что и входные промисы. Если любой из промисов в массиве отклоняется, Promise.all() немедленно отклоняется с причиной первого отклоненного промиса.

// Использование Promise.all()

const promise1 = Promise.resolve(1);

const promise2 = Promise.resolve(2);

const promise3 = new Promise((resolve, reject) => setTimeout(() => resolve(3), 100));

Promise.all([promise1, promise2, promise3])

.then((values) => {

// values будет [1, 2, 3]

})

.catch((error) => {

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

});

Promise.allSettled(): Принимает массив промисов и возвращает новый промис. Этот новый промис переходит в состояние fulfilled, когда все промисы в массиве либо успешно завершены (fulfilled), либо отклонены (rejected). Значением будет массив объектов, описывающих результат каждого промиса (состояние и значение/причина). В отличие от Promise.all(), немедленно не отклоняется.

// Использование Promise.allSettled()

const promiseA = Promise.resolve("успех");

const promiseB = Promise.reject("ошибка");

Promise.allSettled([promiseA, promiseB])

.then((results) => {

// results будет массивом объектов:

// [{status: 'fulfilled', value: 'успех'}, {status: 'rejected', reason: 'ошибка'}]

});

Promise.race(): Принимает массив промисов и возвращает новый промис. Этот новый промис переходит в состояние fulfilled или rejected с результатом/причиной первого промиса из массива, который либо успешно завершился, либо отклонился.

// Использование Promise.race()

const promiseFast = new Promise((resolve, reject) => setTimeout(() => resolve("Я быстрый!"), 50));

const promiseSlow = new Promise((resolve, reject) => setTimeout(() => resolve("Я медленный..."), 500));

Promise.race([promiseFast, promiseSlow])

.then((value) => {

// value будет "Я быстрый!"

});

Promise.any(): Принимает массив промисов и возвращает новый промис. Этот новый промис переходит в состояние fulfilled с значением первого промиса из массива, который успешно завершился. Если все промисы в массиве отклоняются, Promise.any() возвращает ошибку AggregateError, содержащую причины всех отклоненных промисов. Доступен начиная с Node.js 12.

// Использование Promise.any()

const promiseFail1 = Promise.reject("Ошибка 1");

const promiseSuccess = Promise.resolve("Успех!");

const promiseFail2 = Promise.reject("Ошибка 2");

Promise.any([promiseFail1, promiseSuccess, promiseFail2])

.then((value) => {

// value будет "Успех!"

})

.catch((error) => {

// Обработка AggregateError, если все промисы отклонены

});

Node.js — это кроссплатформенная среда выполнения JavaScript, основанная на движке V8 от Google Chrome. Она позволяет запускать JavaScript на стороне сервера.

Ключевые компоненты работы:

Движок V8: Интерпретирует и компилирует JavaScript-код. V8 написан на C++ и преобразует JS в машинный код для более быстрого выполнения.

Цикл событий (Event Loop): Основной поток Node.js, который обрабатывает асинхронные операции. Он работает в одном (!) потоке, что делает Node.js неблокирующим.

Библиотека libuv: Предоставляет кроссплатформенную абстракцию для выполнения асинхронных операций ввода/вывода (файловые операции, сетевые запросы и т.д.). libuv управляет пулом потоков для выполнения этих операций, не блокируя основной поток.

Неблокирующий ввод/вывод: Node.js по умолчанию использует неблокирующий ввод/вывод. Когда Node.js инициирует операцию ввода/вывода, он не ждет ее завершения, а переходит к обработке следующих запросов. Когда операция завершается, срабатывает callback-функция.

Как это работает в целом:

Когда Node.js получает запрос, он передает его обработчику. Если обработчик требует выполнения блокирующей операции (например, чтения из файла), Node.js не ждет эту операцию в основном потоке. Он просто отсылает запрос на выполнение этой операции в libuv, а сам продолжает обрабатывать другие запросы. Как только операция ввода/вывода завершена, libuv уведомляет цикл событий, который затем выполняет callback-функцию, связанную с этой операцией.

Пример асинхронного чтения файла:

Преимущества такого подхода:

Масштабируемость: Благодаря неблокирующему вводу/выводу, Node.js может обрабатывать большое количество параллельных запросов с меньшими ресурсами по сравнению с традиционными многопоточными моделями.

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

Недостатки:

Интенсивные CPU-операции: Блокирующие операции, интенсивно использующие CPU (например, сложные вычисления), могут блокировать цикл событий и снижать производительность приложения. Для таких задач используются worker threads.

Node.js эффективен для построения быстрых и масштабируемых сетевых приложений, API, сервисов реального времени (например, чатов) благодаря его асинхронной, событийно-ориентированной архитектуре.

Процесс обработки данных в pipeline (например, в контексте потоков Node.js) основан на принципах чейннинга и перенаправления.

Источники (Sources): Начинается с потока-источника (readable stream), который генерирует данные.

Преобразователи (Transforms): Данные из источника могут передаваться через один или несколько промежуточных потоков-преобразователей (transform streams). Каждый преобразователь выполняет какую-то операцию над данными (например, фильтрация, преобразование формата).

Назначения (Destinations): Обработанные данные поступают в поток-назначение (writable stream), который их потребляет (например, записывает в файл, отправляет по сети).

Метод pipe():

Центральным методом для построения pipeline является pipe(). Он связывает выход одного потока с входом другого:

Поток событий:

Когда источник готов выдать данные, он инициирует событие data, передавая чанк данных.

Этот чанк передается в следующий поток в pipeline (или напрямую в назначение, если преобразователей нет).

Промежуточные потоки обрабатывают чанк и, если готовы, инициируют свое событие data с преобразованными данными.

Этот процесс повторяется до достижения потока-назначения.

Когда источник перестает генерировать данные, он инициирует событие end, сигнализируя об окончании потока. Это событие также проходит по pipeline, и последний поток-назначение также инициирует end.

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

Ошибки в любом потоке pipeline могут прервать его выполнение. Важно обрабатывать событие error на каждом потоке или использовать библиотеку для управления pipeline'ами, которая упрощает обработку ошибок.

Пример:

Ссылки (тег <a>) используются для перехода на другую страницу или ресурс. Кнопки (тег <button>) выполняют действия на текущей странице, например, отправку формы или вызов JavaScript-функции.

Если элемент выполняет действие, но выглядит как ссылка, его следует оформлять как <button>. И наоборот, если элемент ведет на другую страницу, но выглядит как кнопка, лучше использовать <a>.

Обоснование с точки зрения доступности:

Семантика: Скринридеры и другие вспомогательные технологии используют семантику тегов для понимания назначения элемента. Кнопка воспринимается как интерактивный элемент, который можно активировать, ссылка — как навигационный.

Клавиатурное взаимодействие: По умолчанию кнопки активируются клавишей Enter или Space, ссылки — только Enter. Пользователи, использующие клавиатуру для навигации, ожидают такого поведения.

Стилизация и JavaScript: Использование правильных тегов упрощает применение стилей и обработчиков событий.

Пример оформления ссылки как кнопки:

Пример оформления кнопки, имитирующей ссылку (редко, но возможно для специфических UX решений):

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

Для указания различных форматов изображений в веб-разработке используются следующие подходы:

Элемент <picture>: Позволяет браузеру выбрать наиболее подходящий формат изображения из набора, основываясь на правилах, указанных в элементах <source>.

<picture>

<source srcset="image.webp" type="image/webp"> // Предлагаем WebP

<img src="image.jpg" alt="Описание изображения"> // Фоллбэк для старых браузеров

</picture>

Атрибут srcset элемента <img>: Используется для указания списка источников изображений с различными размерами или плотностями пикселей, а также для предложения разных форматов.

<img src="image.jpg"

srcset="image.webp 1x, image@2x.webp 2x"

alt="Описание изображения">

В данном случае srcset предлагает image.webp для обычных дисплеев и image@2x.webp для ретина-дисплеев, при этом браузер может выбрать WebP, если поддерживает.

Заголовки HTTP: Сервер может использовать заголовок Accept из запроса клиента, чтобы понять, какие форматы изображений поддерживает браузер, и отправить наиболее оптимальный формат в ответе с заголовком Content-Type.

CSS: С помощью медиа-запросов @media можно использовать разные фоновые изображения для разных контекстов (например, для экранов с поддержкой высокого DPI). Хотя это не прямой способ указать разные форматы, в сочетании с серверной логикой или элементами <picture> может использоваться для адаптивной загрурки изображений.

Какой метод использовать, зависит от конкретной задачи, поддерживаемых браузеров и инфраструктуры. <picture> и srcset являются стандартными HTML-подходами для клиента, тогда как управление форматами на сервере через HTTP-заголовки обеспечивает большую гибкость.

Замыкание — это функция, которая помнит окружение, в котором она была создана, даже после того, как это окружение завершило свою работу. Она сохраняет доступ к переменным из внешней (охватывающей) функции.

Это возможно благодаря тому, что при создании функции в JavaScript создается "лексическое окружение", которое содержит все переменные и функции, доступные в этот момент. Даже когда внешняя функция завершается, это лексическое окружение сохраняется в памяти, если на него есть ссылка из созданной внутри функции.

Основные применения:

Сохранение состояния: Можно создавать функции, которые помнят определенные значения между вызовами.

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

Каррирование и частичное применение: Создание новых функций с предустановленными аргументами.

Если последовательно забиндеровать несколько контекстов (например, в Node.js с использованием Function.prototype.bind), итоговый контекст будет тот, который был передан в последнем вызове bind.

Каждый вызов bind создаёт новую функцию с фиксированным значением this. Если вызвать bind повторно на уже забинденной функции, новый контекст перезапишет предыдущий.

Пример:

Однако, важно отметить, что в JavaScript повторный bind на уже забинденной функции не изменяет контекст — сохраняется первый bind. То есть, первый bind фиксирует контекст навсегда.

Если же речь о других механизмах биндинга контекста (например, в библиотеках с вложенными контекстами), то обычно последний применённый контекст имеет приоритет.

Промисы создаются для каждого выражения справа от await. Если выражение уже является промисом, await ожидает его завершения. Если выражение не является промисом, await обернет его в выполненный промис.

Пример с асинхронной функцией:

В этом случае someAsyncOperation1() и someAsyncOperation2() могут возвращать промисы или обычные значения. await всегда работает с промисами.

Cookies - это небольшие фрагменты данных, хранящиеся в браузере пользователя, которые веб-сервер отправляет при посещении веб-страницы.

Используются для:

Идентификации пользователя: Запоминание входа в систему.

Персонализации: Сохранение настроек пользователя (язык, тема).

Отслеживания: Аналитика поведения пользователя на сайте.

Типы cookies:

Сеансовые (Session): Хранятся до закрытия браузера.

Постоянные (Persistent): Хранятся до установленной даты истечения.

Сервер устанавливает cookie с помощью заголовка Set-Cookie:

Браузер отправляет cookie серверу с заголовком Cookie:

В JavaScript существуют два типа сравнения для обеспечения гибкости и контроля над преобразованием типов:

Строгое сравнение (=== и !==): Сравнивает значения и их типы без приведения типов. Результат true, только если и значение, и тип совпадают.

// Строгое сравнение

console.log(5 === '5'); // false (разные типы)

console.log(5 === 5); // true

Нестрогое сравнение (== и !=): Сравнивает значения, выполняя приведение типов, если типы операндов различаются.

// Нестрогое сравнение

console.log(5 == '5'); // true (строка '5' приводится к числу 5)

console.log(0 == false); // true (false приводится к 0)

console.log('' == 0); // true ('' приводится к 0)

console.log(null == undefined); // true (специальное правило)

Наличие двух типов позволяет:

Упростить некоторые операции при нестрогом сравнении (хотя может привести к неочевидному поведению).

Избежать неожиданных преобразований типов при строгом сравнении, обеспечивая предсказуемость.

Строгое сравнение считается предпочтительным в большинстве случаев для ясности и предотвращения ошибок.

TCP (Transmission Control Protocol) — это протокол транспортного уровня, обеспечивающий надежную, ориентированную на установление соединения доставку данных. Он гарантирует порядок доставки пакетов и повторную передачу потерянных.

HTTP (Hypertext Transfer Protocol) — это протокол прикладного уровня, построенный поверх TCP. Он используется для передачи гипертекстовых документов, таких как веб-страницы. HTTP является протоколом типа "запрос-ответ".

Основные отличия:

Уровень: TCP — транспортный, HTTP — прикладной.

Надежность: TCP надежен, HTTP зависит от надежности TCP.

Состояние: TCP ориентирован на соединение (stateful), HTTP по умолчанию без сохранения состояния (stateless), но использует механизмы вроде cookie для поддержки состояния.

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

На примере веб-запроса:

Браузер (HTTP-клиент) устанавливает TCP-соединение с веб-сервером (HTTP-сервером).

Браузер отправляет HTTP-запрос по установленному TCP-соединению.

Сервер обрабатывает запрос и отправляет HTTP-ответ по тому же TCP-соединению.

TCP гарантирует, что пакеты HTTP-запроса и ответа будут доставлены в правильном порядке и без потерь.

После завершения обмена (или по таймауту) TCP-соединение может быть закрыто.

Примитивные типы:

String: Строки текста.

Number: Числа (целые и с плавающей точкой).

Boolean: Логические значения (true или false).

Null: Отсутствие значения, намеренно установленное разработчиком.

Undefined: Отсутствие значения, которое еще не присвоено.

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

BigInt: Целые числа произвольной точности.

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

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

Кроме того, функции в JavaScript также являются объектами первого класса:

В своих проектах на Node.js я активно использовал Sequelize и TypeORM.

Sequelize — проверенная временем ORM, поддерживающая множество баз данных (PostgreSQL, MySQL, SQLite, SQL Server). Она отлично подходит для проектов, где не используется TypeScript, или когда требуется более традиционный подход к работе с базой данных.

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

TypeORM — более современная ORM, написанная на TypeScript и активно использующая декораторы. Она поддерживает большое количество баз данных и предлагает гибкий подход к работе с сущностями и связями. TypeORM прекрасно интегрируется с проектами на TypeScript, предоставляя сильную типизацию.

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

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

В первую очередь, с управлением жизненным циклом объектов (сервисов, компонентов). DI-контейнер создает экземпляры объектов и управляет их зависимостями на основе конфигурации.

Основные сценарии использования:

Внедрение зависимостей (Dependency Injection): Контейнер автоматически предоставляет зависимостям нужные объекты, избавляя от ручного создания и связывания.

Управление областями видимости (Scopes): Контейнер может управлять тем, как долго существует экземпляр объекта (например, синглтон, per-request, transient).

Аспекты (Aspects): Некоторые контейнеры поддерживают внедрение аспектов (например, логирование, кеширование) без изменения основного кода.

Модульное тестирование: Контейнер облегчает замену реальных зависимостей на моки или заглушки во время тестирования.

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

Пример схемы создания и разрешения зависимости:

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

Снижение связанности кода (coupling).

Повышение тестируемости.

Улучшение сопровождаемости.

Централизация конфигурации зависимостей.

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

Логические операторы в Node.js (||, &&, !) обладают особенностями, связанными с их поведением и возвращаемыми значениями:

|| (ИЛИ): Возвращает первый "истинный" операнд или последний операнд, если все они "ложные".

&& (И): Возвращает первый "ложный" операнд или последний "истинный" операнд, если все они "истинные".

! (НЕ): Преобразует операнд в булево значение и возвращает его инверсию.

Особенности связаны с "приведением к булеву значению" (truthiness/falsiness):

"Ложные" значения: false, 0, '' (пустая строка), null, undefined, NaN.

Все остальные значения считаются "истинными".

Операторы || и && являются "лексическими" (или "короткими"): они прекращают вычисление, как только результат становится известен.

Таблица с поведением || и &&:

Event Loop в Node.js работает в итерациях (тиках) и обрабатывает следующие фазы:

Timers: обрабатывает колбэки setTimeout() и setInterval().

Pending callbacks: выполняет колбэки I/O операций, отложенные в предыдущей итерации.

Idle, prepare: внутреннее использование Node.js.

Poll:

Выполняет колбэки I/O (в большинстве случаев).

Если нет готовых к выполнению I/O колбэков, может ждать новых соединений/событий или переходить к фазе Check.

Check: выполняет колбэки setImmediate().

Close callbacks: выполняет колбэки закрытия, например, для сокетов ('close').

Между основными фазами Event Loop, а также после выполнения каждой фазы, Node.js проверяет и выполняет следующие очереди микрозадач:

process.nextTick() queue: имеет наивысший приоритет и выполняется перед всеми другими микрозадачами и фазами Event Loop.

Promise queue: выполняет resolved промисы.

Пример:

Порядок вывода в консоль будет примерно таким:

(порядок read file и immediate может меняться в зависимости от выполнения I/O операции).

С помощью HTTP-запроса GET мы можем получить следующие данные:

Параметры строки запроса (Query Parameters): Данные, передаваемые после знака ? в URL, в формате "ключ=значение" и разделенные символом &. Используются для фильтрации, сортировки или передачи небольших объемов данных.

Заголовки запроса (Request Headers): Содержат метаинформацию о запросе, такую как тип клиента (User-Agent), предпочитаемые языки (Accept-Language), тип принимаемого контента (Accept) и информацию об аутентификации (Authorization).

Тело запроса (Request Body): Согласно спецификации HTTP, запросы GET не должны иметь тело запроса. GET предназначен только для получения информации без отправки данных на сервер в теле. Некоторые клиенты или серверы могут это проигнорировать, но это считается некорректным использованием метода.

URL-путь (Path Parameters): Часть URL, определяющая конкретный ресурс, к которому направлен запрос. Например, в /users/123 123 может быть ID пользователя. Эти параметры извлекаются сервером из пути запроса.

В Node.js с использованием фреймворков (например, Express.js), доступ к этим данным осуществляется следующим образом:

В Node.js (как и в JavaScript) к простым (примитивным) типам данных относятся:

String: Текстовые данные.

Number: Числа (целые и с плавающей точкой).

BigInt: Произвольно большие целые числа.

Boolean: Логические значения (true или false).

Undefined: Значение, присвоенное переменной, которая не была инициализирована.

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

null: Специальное значение, обозначающее преднамеренное отсутствие какого-либо объектного значения.

Если дочерний процесс завершится, родительский процесс получит уведомление (обычно через событие exit или close на объекте ChildProcess) и может обработать его. Статус завершения (код выхода) дочернего процесса будет доступен через это событие. Если родительский процесс не обрабатывает это событие, дочерний процесс просто завершится, а родительский продолжит работать. В некоторых случаях (зависит от операционной системы) завершившийся дочерний процесс может превратиться в "зомби-процесс" до тех пор, пока его не "соберет" родительский процесс (если родительский не ожидает его завершения). Node.js-модуль child_process обычно управляет этим.

Ключевые моменты:

Событие exit: Инициируется при завершении дочернего процесса. Передает код выхода и сигнал завершения.

Событие close: Инициируется после того, как завершились потоки stdout и stderr дочернего процесса.

Код выхода: Нулевой код (0) обычно означает успешное выполнение. Ненулевые коды указывают на ошибку.

Сигнал: Указывает, каким сигналом был завершен процесс (например, SIGKILL, SIGTERM).

Зомби-процессы: Возникают, если родитель не вызывает wait() (что в Node.js обычно управляется автоматически), но могут стать проблемой в определенных сценариях или при неправильном использовании.

Потоки (stdout, stderr): Завершение дочернего процесса также приводит к завершению его потоков ввода/вывода.

Promise представляет собой объект, представляющий завершение или сбой асинхронной операции и её результирующее значение. Async/Await являются синтаксическим сахаром над Promise, делая асинхронный код более читаемым и похожим на синхронный.

Основные отличия:

Пример с Promise:

Пример с Async/Await:

В конечном итоге, Async/Await не заменяет Promise, а использует их под капотом для упрощения написания асинхронного кода. Выбор между ними часто зависит от предпочтений разработчика и сложности логики. В большинстве случаев Async/Await предпочтительнее из-за своей читаемости.

На практике я применял следующие паттерны проектирования:

Фабричный метод (Factory Method): Для создания объектов разных типов без указания конкретного класса создания. Удобно при работе с различными источниками данных или конфигурациями.

// Пример использования фабричного метода

class DataReader {

read() {

throw new Error("Method not implemented");

}

}

class FileReader extends DataReader {

read() {

console.log("Reading data from file...");

}

}

class NetworkReader extends DataReader {

read() {

console.log("Reading data from network...");

}

}

class DataReaderFactory {

createReader(type) {

switch (type) {

case "file":

return new FileReader();

case "network":

return new NetworkReader();

default:

throw new Error("Unknown reader type");

}

}

}

const factory = new DataReaderFactory();

const fileReader = factory.createReader("file");

fileReader.read();

Одиночка (Singleton): Для гарантии существования только одного экземпляра класса и предоставления глобальной точки доступа к нему. Часто используется для менеджеров конфигурации или подключений к базам данных.

// Пример использования одиночки

class ConfigManager {

constructor() {

if (ConfigManager.instance) {

return ConfigManager.instance;

}

this.config = {};

ConfigManager.instance = this;

}

setConfig(key, value) {

this.config[key] = value;

}

getConfig(key) {

return this.config[key];

}

}

const instance1 = new ConfigManager();

const instance2 = new ConfigManager();

console.log(instance1 === instance2); // true

Наблюдатель (Observer): Для реализации связи "один-ко-многим", где при изменении состояния одного объекта (издателя) автоматически оповещаются все зависимые объекты (подписчики). Используется в системах уведомлений, реактивных интерфейсах.

// Пример использования наблюдателя

class Observable {

constructor() {

this.observers = [];

}

subscribe(observer) {

this.observers.push(observer);

}

unsubscribe(observer) {

this.observers = this.observers.filter(obs => obs !== observer);

}

notify(data) {

this.observers.forEach(observer => observer.update(data));

}

}

class Observer {

update(data) {

console.log("Received update:", data);

}

}

const observable = new Observable();

const observer1 = new Observer();

const observer2 = new Observer();

observable.subscribe(observer1);

observable.subscribe(observer2);

observable.notify("New data available!");

Стратегия (Strategy): Для определения семейства алгоритмов, инкапсуляции каждого из них и обеспечения их взаимозаменяемости. Позволяет алгоритму меняться независимо от клиентов, его использующих. Применяется для различных вариантов обработки данных или выполнения операций.

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

class PaymentStrategy {

pay(amount) {

}

}

class CreditCardPayment extends PaymentStrategy {

pay(amount) {

console.log(`Paying ${amount} using Credit Card`);

}

}

class PayPalPayment extends PaymentStrategy {

pay(amount) {

console.log(`Paying ${amount} using PayPal`);

}

}

class PaymentContext {

setStrategy(strategy) {

this.strategy = strategy;

}

executePayment(amount) {

this.strategy.pay(amount);

}

}

const context = new PaymentContext();

context.setStrategy(new CreditCardPayment());

context.executePayment(100);

context.setStrategy(new PayPalPayment());

context.executePayment(50);

Декоратор (Decorator): Для динамического добавления новой функциональности объекту без изменения его структуры. Обертывает существующий объект, предоставляя тот же интерфейс, что и обернутый объект, но с добавленной функциональностью. Удобно для логгирования, валидации, кеширования.

// Пример использования декоратора

class Component {

operation() {

return "Basic Component";

}

}

class Decorator {

constructor(component) {

this.component = component;

}

operation() {

return this.component.operation();

}

}

class ConcreteDecoratorA extends Decorator {

operation() {

return `ConcreteDecoratorA(${super.operation()})`;

}

}

class ConcreteDecoratorB extends Decorator {

operation() {

return `ConcreteDecoratorB(${super.operation()})`;

}

}

const simple = new Component();

console.log(simple.operation());

const decoratorA = new ConcreteDecoratorA(simple);

console.log(decoratorA.operation());

const decoratorB = new ConcreteDecoratorB(decoratorA);

console.log(decoratorB.operation());

Модуль (Module): Хотя в Node.js система модулей встроена (CommonJS, ES Modules), паттерн Module pattern в JavaScript (до появления нативных модулей) использовался для создания приватных членов и публичного интерфейса. Это является основой для построения инкапсулированного кода.

Эти паттерны помогали мне создавать более структурированный, масштабируемый и поддерживаемый код.

Функция call в JavaScript вызывает функцию, к которой она применяется, устанавливая специфическое значение this и передавая аргументы по отдельности.

Общий синтаксис:

thisArg: Значение, которое будет использоваться в качестве this при вызове func. Если thisArg является null или undefined, this будет заменено глобальным объектом в нестрогом режиме (или останется undefined в строгом режиме).

arg1, arg2, ... : Аргументы, передаваемые функции func.

Основные применения:

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

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

Эмуляция наследования (до классов): В старых версиях JavaScript можно было использовать call для вызова конструктора родителя с контекстом дочернего объекта.

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

Пример заимствования метода:

Для реализации выпадающего окна (например, для меню или модального окна) и механизма его закрытия на веб-странице, я бы использовал комбинацию HTML, CSS и JavaScript.

HTML:

Один элемент-триггер (кнопка, ссылка) и один элемент-контейнер для выпадающего содержимого.

CSS:

Скрытие выпадающего окна по умолчанию и стилизация.

JavaScript:

Обработка событий клика для открытия и закрытия окна.

Объяснение механизмов закрытия:

Кнопка закрытия внутри окна: Простейший способ, прямое событие click на кнопке.

Клик вне окна: Используется делегирование событий на уровне document. Проверяется, не был ли клик внутри самого выпадающего окна (dropdownContent.contains(event.target)) или триггера открытия (dropdownTrigger.contains(event.target)). Если клик был "снаружи", окно скрывается.

Нажатие клавиши Esc: Обрабатывается событие keydown на document. При нажатии клавиши "Escape" и если окно видимо, оно скрывается.

Этот подход обеспечивает несколько удобных способов закрытия, улучшая пользовательский опыт.

Моргание стилей (Flash of Unstyled Content - FOUC) происходит, когда браузер сначала отображает содержимое страницы без применения стилей, а затем применяет их. Это связано с порядком загрузки и обработки ресурсов.

Основные причины:

CSS загружается медленнее HTML: Браузер начинает парсить и рендерить HTML до того, как полностью загрузится и обработается CSS.

Стили объявлены после содержимого: Если теги <link> или <style> находятся после <script> или основного HTML-содержимого в секции <body>, браузер начнет отображение до их обработки.

@import в CSS: Директива @import заставляет браузер загружать дополнительные CSS-файлы последовательно, блокируя рендеринг стилей до завершения всех импортов.

JavaScript, модифицирующий стили: Скрипты, которые динамически загружают или применяют стили после первоначальной загрузки страницы, могут вызвать FOUC.

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

Способы предотвращения:

Размещать <link rel="stylesheet"> в <head>: Это гарантирует, что CSS начнет загружаться и обрабатываться как можно раньше.

Использовать инлайн-стили для критического CSS: Небольшой объем критических стилей, необходимых для первоначального отображения, можно встроить непосредственно в тег <style> в <head>.

Избегать @import: Предпочтительнее использовать множественные теги <link>.

Оптимизировать загрузку JavaScript: Отложенная загрузка или асинхронное выполнение скриптов (defer, async) могут помочь, если они не влияют на первоначальные стили.

Предварительная загрузка шрифтов: Использование <link rel="preload"> для важных шрифтов может уменьшить FOUC, связанный со шрифтами.

CommonJS и ECMAScript Modules (ESM).

CommonJS:

Использует require() для импорта модулей и module.exports или exports для экспорта.

Синхронно загружает модули.

Является стандартным для версий Node.js до 12.0.0.

ECMAScript Modules (ESM):

Использует операторы import и export.

Асинхронно загружает модули.

Является стандартом для более новых версий Node.js (начиная с 12.0.0 с флагом --experimental-modules, полностью с 14.0.0 по умолчанию с расширением .mjs или type: "module" в package.json).

Сравнение:

HttpOnly флаг: Запрещает JavaScript доступ кookie, снижая риск XSS-атак.

Secure флаг: Передает cookie только по защищенному HTTPS-соединению.

SameSite атрибут: Контролирует отправку cookie с межсайтовыми запросами, защищая от CSRF-атак. Значения:

Strict: Cookie отправляются только для запросов на тот же сайт с того же домена.

Lax: Cookie отправляются для межсайтовых запросов, когда пользователь переходит по внешней ссылке (например, GET-запрос).

None: Cookie отправляются со всеми межсайтовыми запросами (требует установки флага Secure).

Ограничение срока жизни: Использование атрибута Expires или Max-Age для автоматического удаления cookie.

Шифрование: Шифрование чувствительных данных, хранящихся в cookie.

Сокращение данных: Хранение в cookie только необходимой информации, такой как идентификатор сессии. Сами данные сессии лучше хранить на сервере.

Защита от подбора: Использование криптографически стойких идентификаторов сессий вместо легко предсказуемых.

Валидация на сервере: Всегда проверять и валидировать данные из cookie на сервере.

Promise.all - это статический метод объекта Promise, который принимает итерируемый объект промисов (например, массив) и возвращает новый промис. Этот новый промис разрешается только тогда, когда все промисы в итерируемом объекте разрешены, или отклоняется немедленно, как только один из промисов отклоняется.

Разрешение: Если все промисы успешно разрешены, промис, возвращаемый Promise.all, разрешается с массивом значений разрешенных промисов, сохраняя порядок исходного итерируемого объекта.

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

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

CSS-модули, scoped CSS (в прошлой спецификации), CSS-in-JS.

CSS-модули:

Каждый файл .css рассматривается как модуль.

Класс или идентификатор в CSS-файле преобразуется в уникальное имя (обычно хэшированное).

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

Пример CSS-модуля:

Scoped CSS (устаревшая спецификация, но принципы используются в фреймворках):

Стили внутри элемента <style scoped> применялись только к его родительскому элементу и его потомкам.

В чистых браузерах более не поддерживается. Современные фреймворки (например, Vue) реализуют аналогичный функционал.

Пример Scoped CSS (концептуально):

CSS-in-JS:

Стили пишутся непосредственно в JavaScript или TypeScript коде.

Существуют различные библиотеки (например, styled-components, Emotion).

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

Пример CSS-in-JS (styled-components):

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

Контекст выполнения функции (значение this) в JavaScript можно изменить несколькими способами:

Методы call(), apply(), bind()

Использование стрелочных функций

Сохранение контекста в переменную

Методы call(), apply() и bind()

Эти методы позволяют явно установить значение this для вызываемой функции.

Различия между call(), apply() и bind():

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

До появления стрелочных функций часто использовался паттерн сохранения контекста в отдельную переменную (например, that или self).

Объявление переменной с var внутри функции имеет функциональную область видимости. Это означает, что такая переменная доступна во всей функции, игнорируя блочные области видимости (например, внутри if или for).

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

Перезаписывание переменных: Переменная, объявленная с var в блоке, может перезаписать переменную с таким же именем, объявленную ранее в той же функции, но вне этого блока.

Поднятие (Hoisting): Объявление переменной с var поднимается в начало функции. Это означает, что можно использовать переменную до ее объявления, но ее значение на момент использования будет undefined.

Неожиданное поведение в циклах: В циклах for с асинхронными операциями (например, setTimeout), переменная, объявленная с var, будет иметь последнее значение из цикла для всех асинхронных колбэков, а не значение на момент итерации.

Пример поднятия и перезаписывания:

Пример с циклом и асинхронными операциями:

Из-за этих особенностей, для предотвращения непредвиденных ситуаций, рекомендуется использовать let или const, которые имеют блочную область видимости.

Оператор in в JavaScript используется для проверки наличия свойства в объекте или в его прототипной цепочке. Например:

Недостатки оператора in:

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

Не различает, является ли свойство перечисляемым или нет.

Если нужно проверить только собственные свойства объекта, лучше использовать метод hasOwnProperty:

Таким образом, in удобен для общего поиска свойства, но требует осторожности, если важна именно принадлежность свойства самому объекту.

Быстрый, неблокирующий ввод/вывод благодаря асинхронной модели событий и движку V8.

Возможность использования JavaScript на фронтенде и бэкенде (фулстек).

Огромная экосистема пакетов npm, упрощающая разработку.

Активное сообщество и поддержка.

Идеально подходит для микросервисной архитектуры и real-time приложений.

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

Контекст выполнения функции (this) в JavaScript определяет, как функция вызывается и какие значения доступны внутри нее. Контекст динамически изменяется в зависимости от способа вызова:

Глобальный контекст: Вне функций, this ссылается на глобальный объект (window в браузере, global в Node.js).// В браузере

console.log(this === window); // true

// В Node.js

console.log(this === global); // true

Контекст функции:

Стандартный вызов: В нестрогом режиме this внутри функции ссылается на глобальный объект (если функция не метод объекта). В строгом режиме ("use strict";) this будет undefined.function showThis() {

console.log(this);

}

showThis(); // В нестрогом: Global object; В строгом: undefined

Вызов как метод объекта: this ссылается на объект, частью которого является метод.const user = {

name: 'Alice',

greet: function() {

console.log("Hello, my name is " + this.name);

}

};

user.greet(); // Hello, my name is Alice

Вызов с new (конструктор): Создает новый объект, который становится значением this внутри функции-конструктора, и возвращает этот объект.function Person(name) {

this.name = name;

}

const person1 = new Person('Bob');

console.log(person1.name); // Bob

Явное указание контекста (call, apply, bind):

call(thisArg, arg1, arg2, ...): Вызывает функцию с указанным this и аргументами, перечисленными по отдельности.

apply(thisArg, [argsArray]): Вызывает функцию с указанным this и аргументами в виде массива.

bind(thisArg, arg1, arg2, ...): Возвращает новую функцию, у которой this навсегда привязан к thisArg и привязанным аргументам.

function introduce(greeting, punctuation) {

console.log(greeting + ", I am " + this.name + punctuation);

}

const person = { name: 'Charlie' };

introduce.call(person, "Hi", "!"); // Hi, I am Charlie!

introduce.apply(person, ["Hello", "."]); // Hello, I am Charlie.

const boundIntroduce = introduce.bind(person, "Hey");

boundIntroduce("?"); // Hey, I am Charlie?

console.log(this);

}

name: 'Alice',

greet: function() {

}

};

this.name = name;

}

}

Стрелочные функции: Не имеют собственного контекста this. Они захватывают this из объемлющей лексической области видимости (той, в которой они были определены).const obj = {

name: 'David',

sayLater: function() {

// this здесь ссылается на obj

setTimeout(() => {

// this здесь также ссылается на obj (унаследован из sayLater)

console.log("Later, I am " + this.name);

}, 1000);

}

};

obj.sayLater(); // Later, I am David

Семантика в программировании — это значение или интерпретация программных конструкций. Она определяет, что именно выполняет код, какие действия происходят при его выполнении.

Для Node.js разработчика понимание семантики важно для:

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

Предсказания поведения кода: Понимание семантики асинхронности, работы с I/O-операциями, модульной системы и других особенностей Node.js позволяет предвидеть, как будет вести себя приложение в различных ситуациях.

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

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

Примеры семантических аспектов в Node.js:

Семантика асинхронных операций: Понимание того, что операции ввода/вывода (чтение файла, сетевой запрос) не блокируют основной поток выполнения благодаря Event Loop.

Семантика this: Понимание, как значение ключевого слова this меняется в зависимости от контекста вызова функции (обычные функции, стрелочные функции, методы объектов).

Семантика модулей: Понимание, как работает система CommonJS или ES-модулей для импорта и экспорта кода.

Пример демонстрации семантики асинхронности:

Результат выполнения будет:

Это демонстрирует семантику неблокирующего выполнения setTimeout.

Инкапсуляцию в JavaScript можно реализовать с помощью замыканий, создавая приватные переменные и методы внутри функции, которые недоступны извне, но могут быть использованы функциями, объявленными внутри.

В данном примере:

count является приватной переменной, доступной только функциям, объявленным внутри createCounter.

Объект, возвращаемый функцией, содержит публичные методы (increment, decrement, getCount), которые имеют доступ к приватной переменной count благодаря замыканию.

Извне counter мы можем вызывать только эти публичные методы, но не можем напрямую получить доступ или изменить count.

Это позволяет скрыть внутреннее состояние объекта и предоставить контролируемый интерфейс для взаимодействия с ним.

Деплой (или развертывание) — это процесс переноса и запуска готового программного обеспечения (в нашем случае — Node.js приложения) на целевом сервере, где оно будет доступно конечным пользователям. Он включает сборку, развертывание, настройку и запуск приложения.

DOM позволяет программам (например, JavaScript) получать доступ к содержимому, структуре и стилям веб-документов (HTML, XML, SVG) и изменять их. Это API, которое представляет документ в виде структурированного дерева узлов.

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

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

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

Манипуляции: Дает возможность изменять содержимое, атрибуты и стили элементов, а также добавлять или удалять элементы.

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

Пример работы с DOM в JavaScript:

Node.js использует движок V8 от Google, написанный на C++, для выполнения JavaScript-кода. V8 компилирует JavaScript в машинный код перед его выполнением. Node.js также включает встроенные модули на C++ (например, для работы с файловой системой, сетью) и предоставляет API для работы с операционной системой. Основное преимущество Node.js — асинхронная неблокирующая модель ввода-вывода, реализованная с помощью event loop и библиотеки libuv.

Основные компоненты:

V8 Engine: Компилирует JavaScript в машинный код.

libuv: Предоставляет асинхронный ввод-вывод, поддерживает event loop, пул потоков.

Core Modules: Встроенные модули (fs, http, path и др.), написанные в основном на JavaScript и C++.

Процесс выполнения:

Код JavaScript передается в V8.

V8 парсит код, создает AST (Abstract Syntax Tree).

V8 компилирует AST в машинный код с помощью Just-In-Time (JIT)compilation.

Выполняется машинный код.

Операции ввода-вывода (файловые операции, сетевые запросы) передаются в libuv.

libuv обрабатывает эти операции асинхронно, используя пул потоков или нативные асинхронные механизмы ОС.

Event loop (управляемый libuv) ожидает завершения асинхронных операций.

При завершении асинхронной операции, libuv помещает колбэк в очередь событий.

Event loop забирает колбэки из очереди и выполняет их в основном потоке JavaScript.

Эта модель позволяет Node.js эффективно обрабатывать большое количество одновременных соединений при выполнении задач, ограниченных вводом-выводом, не блокируя основной поток выполнения.

А если вопрос прозвучит не так, как вы готовились?

Так бывает чаще всего. ИзиСобес слышит вопрос интервьюера и подсказывает ответ прямо во время разговора — его не видно ни в Zoom, ни при демонстрации экрана.

Посмотреть ИзиСобес