Урок по треку JavaScript 7 мин

Модули за 7 минут

ESM и CommonJS, live bindings, динамический import, циклические зависимости.

Модуль это файл со своей областью видимости, который явно говорит, что отдаёт наружу и что берёт у других. В 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.
  • Что будет при циклической зависимости модулей.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.