Що таке «циклічна залежність»?
1. Визначення
Circular dependency (циклічна залежність) - це ситуація, коли два (чи більше) модулі залежать один від одного напряму чи опосередковано, утворюючи замкнене коло залежностей.
Простіше кажучи:
- Модуль A імпортує модуль B
- Модуль B імпортує модуль A
Або ще гірше - через ланцюжок:
A → B → C → A2. Простий приклад у Node.js
a.js
const b = require('./b');
console.log('Модуль A завантажено');
module.exports = 'A';b.js
const a = require('./a');
console.log('Модуль B завантажено');
module.exports = 'B';main.js
const a = require('./a');
const b = require('./b');Результат під час запуску:
Модуль B завантажено
Модуль A завантаженоЗверни увагу: порядок не той, якого ти очікував.
3. Що реально відбувається під капотом
Коли Node.js зустрічає require(), він:
- Починає завантажувати модуль
a; - Бачить, що
aвимагаєb; - Починає завантажувати
b; - Але
bтеж вимагаєa, аaще не встиг повністю завантажитися; - Node.js повертає частково завантажений (незавершений) об'єкт
aуb.
Тому за циклічної залежності один із модулів отримає неповний експорт (undefined чи частковий об'єкт).
4. Демонстрація частково завантаженого модуля
a.js
exports.loaded = false;
const b = require('./b');
console.log('b.loaded =', b.loaded);
exports.loaded = true;
console.log('a завантажено');b.js
exports.loaded = false;
const a = require('./a');
console.log('a.loaded =', a.loaded);
exports.loaded = true;
console.log('b завантажено');main.js
require('./a');Вивід:
a.loaded = false
b завантажено
b.loaded = true
a завантаженоТобто коли b імпортував a, він отримав ще не завершений об'єкт - a.loaded = false.
5. Чому це відбувається
Node.js кешує кожен модуль при require().
Коли модуль завантажується, він додається в require.cache, навіть якщо не встиг виконатися повністю.
Тому за циклічної залежності один із модулів "побачить" недозавантажену версію іншого.
6. У реальному житті
Такі цикли часто виникають не напряму, а через ланцюжок імпортів:
A → utils.js → logger.js → Aчи в архітектурі:
controller → service → model → controllerЗазвичай це ознака поганого проєктування (надто сильна зв'язаність між модулями).
7. Ознаки, що в тебе «circular dependency»
-
При імпорті отримуєш
undefined; -
Код виконується не в очікуваному порядку;
-
Під час запуску Node.js виводить попередження:
javascriptWarning: Accessing non-existent property 'X' of module exports inside circular dependency -
Поведінка нестабільна чи відрізняється між середовищами.
8. Як уникнути циклічних залежностей
Спосіб 1. Розірвати цикл через третій модуль
Створи "спільний" модуль, який зберігає те, що потрібно обом.
Погано:
A → B → AДобре:
A → Common
B → Commoncommon.js
module.exports = { sharedValue: 42 };a.js
const common = require('./common');b.js
const common = require('./common');Спосіб 2. Використовувати відкладене завантаження (require() усередині функції)
Якщо модуль потрібен лише під час виклику функції, можна викликати require() пізніше:
function useB() {
const b = require('./b'); // завантажиться лише під час виклику
b.doSomething();
}Це розриває "момент циклу" під час старту застосунку.
Спосіб 3. Змінити архітектуру
- Розділи відповідальність між модулями;
- Позбався "взаємних" залежностей - нехай один модуль керує іншим;
- Використовуй ін'єкцію залежностей (dependency injection).
Спосіб 4. Використовувати ESM (ES Modules)
В ES Modules (з import/export) циклічні залежності розв'язуються коректно,
тому що імпорт створює живі посилання (live bindings),
а не статичні значення, як у CommonJS.
Приклад (працює стабільно):
// a.mjs
import { b } from './b.mjs';
export const a = 'A';
console.log('a бачить b =', b);
// b.mjs
import { a } from './a.mjs';
export const b = 'B';
console.log('b бачить a =', a);9. Інтуїтивно: чому це проблема
Уяви два файли, які запускають один одного одночасно:
A запускає B → B запускає A → A запускає B → ...Node.js, щоб не піти в нескінченність, "зупиняє" цикл і повертає те, що встиг завантажити на момент циклу. Тому ти бачиш "половину об'єкта".
10. Інструменти для пошуку циклів
Якщо проєкт великий, можна використати утиліти:
npm install madge -g
madge src/Вона покаже, які файли утворюють циклічні залежності.
Коротке резюме
| Термін | Значення |
|---|---|
| Circular dependency | Коли модулі імпортують один одного (напряму чи через ланцюжок) |
| Причина | Модуль вимагає інший, який ще не встиг виконатися |
| Наслідок | Повернення undefined чи частково завантаженого модуля |
| Рішення | Розділення логіки, відкладені імпорти, рефакторинг, ESM |
Одним реченням
Циклічна залежність - це ситуація, коли модулі Node.js імпортують один одного, утворюючи замкнене коло, через що один із них завантажується не повністю, що призводить до помилок і
undefinedприrequire().
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.