ЯдроКодаподготовка к экзаменам
Учебная платформа

Загружаем материалы

Подготавливаем материалы и навигацию по разделу.

Модули JavaScript: import, export и границы общего состояния

Автор: · Обновлено

ES modules разделяют программу на файлы с явными зависимостями и экспортами. Импорт связывает модули через публичные имена, поэтому внутренние детали можно менять без превращения…

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

Среда и полный пример из файлов

Для основного примера нужны Bash и Node.js 22 или новее. Выполните первый блок в терминале: он создаёт отдельную временную папку, записывает три небольших файла и запускает программу. Файлы имеют расширение .mjs, поэтому Node однозначно читает их как ES modules. В браузерной версии эти же зависимости подключаются через модульный скрипт и обслуживаются локальным HTTP-сервером; обычное открытие HTML через file: для такого эксперимента не подходит.

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

module_demo_dir=$(mktemp -d)
cat > "$module_demo_dir/duration.mjs" <<'JS'
export function totalMinutes(parts) {
  return parts.reduce((sum, part) => sum + part, 0);
}
JS
cat > "$module_demo_dir/progress.mjs" <<'JS'
export let completed = 0;
export function complete() { completed += 1; }
JS
cat > "$module_demo_dir/main.mjs" <<'JS'
import { totalMinutes } from './duration.mjs';
import { completed, complete } from './progress.mjs';
console.log(totalMinutes([12, 18]));
console.log(completed);
complete();
console.log(completed);
JS
node "$module_demo_dir/main.mjs"

Ожидаемый вывод: 30, 0, 1. Переменная completed в импортирующем файле отражает изменение экспортированного значения. Это живая привязка, а не копия числа на момент импорта. Однако импортирующий модуль не может выполнить completed = 10: изменять привязку должен экспортирующий модуль. Если экспортируется объект, ограничения на переназначение импорта сами по себе не запрещают менять его поля.

Имена, default и путь к зависимости

Именованный экспорт импортируют в фигурных скобках; имя должно соответствовать контракту, хотя локально его можно переименовать через as. Экспорт по умолчанию импортируют без фигурных скобок и выбирают локальное имя самостоятельно. У одного модуля может быть много именованных экспортов и не более одного экспорта по умолчанию. Выбор стиля полезно согласовать в проекте, чтобы читатель сразу понимал форму зависимости.

Относительный путь отсчитывается от файла, который содержит импорт, а не от папки запуска команды. В примере явно указано расширение. Браузер не умеет автоматически читать установленный пакет по одному имени вроде vue, если не настроен механизм разрешения такого имени. Сборщики и import maps решают эту задачу на своём уровне. Ошибка «не найден модуль» часто связана именно с правилами среды, а не с отсутствием экспортированной функции.

Статический import пишется на верхнем уровне модуля. Нельзя поместить его внутрь if как обычную команду. Граф зависимостей известен среде до выполнения тела; расположение объявления ниже другой строки не делает импорт поздним. Не стройте порядок инициализации на визуальном расположении импортов между командами: отделите объявление зависимостей от явной функции запуска приложения.

Выполнение и общий экземпляр

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

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

Модули автоматически используют строгий режим, а их верхнеуровневый this равен undefined. Объявления не становятся свойствами window. Поэтому вызов функции из HTML-атрибута по её локальному имени может не найти её: модульный код намеренно не публикует всё в глобальное пространство. Для интерфейса устанавливайте обработчики через код или средства фреймворка.

Динамическая загрузка и циклические зависимости

Выражение import() загружает модуль по требованию и возвращает Promise с пространством его экспортов. Оно подходит для необязательного редактора или большого инструмента, открываемого по действию пользователя. Необходимо обработать ошибку загрузки и показать состояние ожидания; подробная работа с Promise идёт в следующем уроке. Динамический импорт не гарантирует ускорения сам по себе: он переносит часть работы на более поздний момент.

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

Практика: заменить общий счётчик фабрикой

Временный модуль progress.mjs из основного примера хранит один счётчик. Требуется обслужить два независимых плана. Следующий блок — самостоятельная проверка фабрики в одном .mjs файле; при переносе в проект экспортируйте createProgress и импортируйте её в точке создания планов.

function createProgress() {
  let count = 0;
  return {
    complete() { count += 1; },
    read() { return count; },
  };
}
const left = createProgress();
const right = createProgress();
left.complete();
console.log(left.read(), right.read());

Вывод: 1 0. Независимость появляется при вызове фабрики, а не при двух импортах одного файла. Объясните, где именно приложение должно создавать эти экземпляры: в компоненте, сессии или запросе. Это решение задаёт срок хранения состояния и помогает избежать случайного смешивания данных.

Частые вопросы

Почему сервер отдаёт HTML вместо JavaScript?

Посмотрите URL, статус и Content-Type в Network. Неверный путь иногда попадает в fallback страницы приложения. Исправлять нужно маршрут ресурса; отключение проверки MIME скрывает проблему и ухудшает безопасность.

Чем модуль отличается от пакета?

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

Связан ли этот модуль с Math.abs или %?

Нет. Совпадает только русское слово. Арифметический модуль числа и остаток объясняются в отдельном уроке; здесь речь о структуре программы и разрешении зависимостей.

Источники