← Назад к вопросам
Что такое циклические зависимости модулей в Node.js и как с ними работать?
2.3 Middle🔥 111 комментариев
#Node.js и JavaScript#Архитектура и паттерны
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Циклические зависимости (circular dependencies) — это когда модули ссылаются друг на друга прямо или косвенно, создавая цикл. Это может привести к проблемам: undefined переменные, незаполненные экспорты, и ошибки выполнения.
Пример циклической зависимости
// file-a.js
const b = require('./file-b');
function funcA() {
b.funcB();
}
module.exports = {funcA};
// file-b.js
const a = require('./file-a');
function funcB() {
a.funcA();
}
module.exports = {funcB};
// index.js
const a = require('./file-a');
a.funcA(); // Проблема: b.funcB undefined!
Как Node.js обрабатывает циклические зависимости
Node.js имеет встроенный механизм для предотвращения бесконечных циклов:
// file-a.js
console.log('Loading A');
const b = require('./file-b');
console.log('b:', b);
module.exports = {name: 'A'};
console.log('A loaded');
// file-b.js
console.log('Loading B');
const a = require('./file-a');
console.log('a:', a);
module.exports = {name: 'B'};
console.log('B loaded');
// index.js
const a = require('./file-a');
// Вывод:
// Loading A
// Loading B
// a: {} (пустой объект — A ещё не загрузился полностью)
// B loaded
// b: {name: 'B'}
// A loaded
Node.js возвращает неполный экспорт для предотвращения бесконечного цикла.
Типы циклических зависимостей
1. Прямая циклическая зависимость
// user.js
const role = require('./role');
class User {
constructor(name, roleId) {
this.name = name;
this.roleId = roleId;
}
getRole() {
return role.getRoleById(this.roleId);
}
}
module.exports = User;
// role.js
const User = require('./user'); // ЦИКЛИЧЕСКАЯ ЗАВИСИМОСТЬ
class Role {
getRoleById(id) {
return {id, name: 'Admin'};
}
}
module.exports = Role;
2. Косвенная циклическая зависимость
// a.js → b.js → c.js → a.js
const b = require('./b'); // a.js
// b.js
const c = require('./c');
// c.js
const a = require('./a'); // Цикл!
Проблемы циклических зависимостей
1. Undefined переменные
// database.js
const logger = require('./logger');
const db = {
query: (sql) => {
logger.log(`Executing: ${sql}`);
}
};
module.exports = db;
// logger.js
const db = require('./database');
const logger = {
log: (msg) => {
// db undefined здесь!
console.log(`[LOG] ${msg}`);
}
};
module.exports = logger;
2. Race condition с async
// servicea.js
const serviceB = require('./serviceb');
async function init() {
// serviceB может быть не полностью инициализирован
await serviceB.getData();
}
module.exports = {init};
Решения для циклических зависимостей
1. Рефакторинг — создать отдельный модуль
// common/types.js (нейтральный модуль)
module.exports = {
UserType: 'user',
RoleType: 'role'
};
// user.js
const {UserType} = require('./common/types');
const role = require('./role');
class User {
constructor(name) {
this.name = name;
this.type = UserType;
}
}
module.exports = User;
// role.js
const {RoleType} = require('./common/types');
class Role {
constructor(name) {
this.name = name;
this.type = RoleType;
}
}
module.exports = Role;
// Нет циклических зависимостей!
2. Ленивая загрузка (Lazy Loading)
// user.js
class User {
constructor(name) {
this.name = name;
}
getRole() {
// Загружаем только когда нужно
const role = require('./role');
return role.getRoleById(this.roleId);
}
}
module.exports = User;
// role.js
class Role {
constructor(name) {
this.name = name;
}
getUser() {
// Загружаем только когда нужно
const User = require('./user');
return new User('Unknown');
}
}
module.exports = Role;
3. Dependency Injection (инъекция зависимостей)
// user.js
class User {
constructor(name, roleService) {
this.name = name;
this.roleService = roleService; // Передаем как параметр
}
getRole() {
return this.roleService.getRoleById(this.roleId);
}
}
module.exports = User;
// role.js
class Role {
getRoleById(id) {
return {id, name: 'Admin'};
}
}
module.exports = Role;
// app.js (главный модуль)
const User = require('./user');
const roleService = require('./role');
const user = new User('Иван', roleService);
console.log(user.getRole());
4. Отделение интерфейсов
// interfaces/user-interface.js
class IUser {
getRole() {}
}
module.exports = IUser;
// user.js
const IUser = require('./interfaces/user-interface');
class User extends IUser {
getRole() {
const role = require('./role');
return role.getRoleById(this.roleId);
}
}
module.exports = User;
Обнаружение циклических зависимостей
# Использование инструментов
npm install -D madge
madge --extensions js,ts --image out.svg .
# Или
npm install -D depcheck
depcheck --extra-extensions ts,jsx
Best Practices
-
Используй архитектурные слои
domain/ (бизнес-логика, БЕЗ зависимостей) application/ (use cases, сервисы) infrastructure/ (БД, API, утилиты) presentation/ (HTTP handlers, views) -
Избегай циклов с помощью Event Emitter
// events.js const EventEmitter = require('events'); module.exports = new EventEmitter(); // user.js const events = require('./events'); events.emit('user:created', user); // role.js const events = require('./events'); events.on('user:created', (user) => {}); -
Думай о зависимостях с самого начала
Вывод
Циклические зависимости можно избежать:
- Правильной архитектурой с разделением слоев
- Ленивой загрузкой модулей
- Инъекцией зависимостей
- Использованием Event Emitter для коммуникации
- Регулярной проверкой инструментами как madge