Модуль это файл со своей областью видимости, который явно говорит, что отдаёт наружу и что берёт у других. В JavaScript живут две системы модулей: стандартные ES-модули (ESM) и CommonJS (CJS) из Node.js. На собесе спрашивают, чем они отличаются и что из этого следует для сборки и работы кода.
ES-модули
// math.js
export const PI = 3.14;
export function sum(a, b) { return a + b; }
export default class Calculator {}
// app.js
import Calculator, { sum, PI as pi } from './math.js';
import * as math from './math.js';
Свойства ESM:
- Статическая структура.
importиexportпишутся только на верхнем уровне, пути строковые литералы. Поэтому граф зависимостей известен до выполнения кода: на этом построены tree shaking и быстрые проверки сборщиков. - Строгий режим всегда,
thisна верхнем уровне равенundefined. - Своя область видимости: переменные модуля не попадают в
window. - Выполняется один раз: сколько бы модулей ни импортировали файл, он выполнится однажды, все получат один и тот же экземпляр.
- Живые привязки (live bindings): импорт это не копия значения, а ссылка на переменную модуля-экспортёра. Если экспортёр изменит
let-переменную, импортёр увидит новое значение. При этом импортёр сам присвоить ей ничего не может (TypeError). - Асинхронная загрузка: в браузере
<script type="module">ведёт себя какdefer: грузится параллельно, выполняется после разбора HTML. - Top-level await: в модуле можно писать
awaitна верхнем уровне, зависящие модули подождут.
Динамический import
import('./heavy.js') загружает модуль по требованию и возвращает промис с объектом модуля. Работает и в CommonJS. Сборщики выделяют такой модуль в отдельный чанк: это основа code splitting и React.lazy.
button.onclick = async () => {
const { openEditor } = await import('./editor.js');
openEditor();
};
import.meta содержит данные о текущем модуле: import.meta.url, а в Node 20.11+ ещё import.meta.dirname и import.meta.filename вместо __dirname.
CommonJS
// math.cjs
const PI = 3.14;
module.exports = { PI, sum: (a, b) => a + b };
// app.cjs
const { sum } = require('./math.cjs');
Свойства CJS:
requireсинхронный и динамический: его можно вызвать где угодно, хоть внутриif, с вычисленным путём.- Экспорт это копия значения на момент require (точнее, объект
module.exports): деструктурированная переменная потом не обновится. - Модуль кешируется после первой загрузки (
require.cache). - Есть
__dirname,__filename,module,exports. В ESM их нет.
Как Node решает, что за модуль
.mjsвсегда ESM,.cjsвсегда CommonJS..jsсмотрит на ближайшийpackage.json:"type": "module"означает ESM, иначе CJS.- ESM может импортировать CJS:
module.exportsприходит как default-экспорт. - CJS загружает ESM через
await import(), а начиная с Node 22.12 и синхронно черезrequire(), если в ESM-модуле нет top-level await. - Поле
exportsвpackage.jsonзадаёт публичные точки входа пакета и разные файлы дляimportиrequire(условные экспорты).
Циклические зависимости
Если a импортирует b, а b импортирует a, один из модулей увидит другой недовыполненным. В ESM обращение к ещё не инициализированной let/const/class из другого модуля бросит ReferenceError (TDZ), объявления функций поднимаются и доступны. В CJS вы получите недозаполненный module.exports, часто просто {}. Лечится вынесением общего кода в третий модуль.
Tree shaking
Сборщик (Rollup, Vite, webpack, esbuild) выкидывает экспорты, которые никто не импортирует. Работает надёжно только с ESM из-за статической структуры. Мешают:
- CommonJS-зависимости;
- побочные эффекты при импорте (код на верхнем уровне модуля), поэтому в
package.jsonпишут"sideEffects": false; import * as libс динамическим обращениемlib[name].
Частые ошибки
- Ждать, что изменение импортированной переменной в CJS будет видно, как в ESM.
- Импорт без расширения файла в Node ESM:
import './utils'не сработает, нужно'./utils.js'. __dirnameв ESM: его нет.- Тяжёлые побочные эффекты на верхнем уровне модуля.
Что спросят на собесе
- Чем ESM отличается от CommonJS.
- Что такое live bindings.
- Что такое динамический импорт и зачем он нужен.
- Как работает tree shaking и почему ему нужен ESM.
- Что будет при циклической зависимости модулей.