export и import

модули≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

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

Разделы темы:·

Модуль — обычный файл с кодом, но с одной особенностью: всё, что в нём объявлено, снаружи по умолчанию не видно. Наружу выходит только помеченное словом export, остальное остаётся внутренней кухней модуля.

Пометить можно прямо при объявлении — export const, export function, export class — или собрать список в конце файла: export { hi, NAME };. Второй способ удобен, когда хочется одним взглядом увидеть весь публичный список. Импорт перечисляет нужные имена в фигурных скобках, и они должны совпадать с экспортированными буква в букву.

Песочница на этой странице выполняет код как один файл, поэтому строки с import и export в примерах закомментированы, а рядом — то же самое живьём.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

default в модуле не больше одного, но он спокойно уживается с именованными экспортами в том же файле. У свободы имени есть обратная сторона: опечатку никто не заметит, один и тот же модуль в разных местах проекта окажется User, Person и u. Поэтому многие команды договариваются использовать только именованные экспорты.

Именованныйexport default
Объявлениеexport const a = 1;export default 1;
Импортimport { a } from "./m.js";import a from "./m.js";
Имяфиксировано, меняется через asвыбирает импортирующий
Сколько в файлесколько угодноне больше одного

Рано или поздно два модуля предложат одинаковое имя. Спасает слово as: import { format as formatDate } from "./date.js". Переименовать можно и на стороне экспорта — export { format as formatDate };, — но тогда имя выбирает автор модуля, а не тот, кто им пользуется.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Когда имён много, их забирают одним махом: import * as math from "./math.js". Получается объект-пространство имён, где каждый именованный экспорт — свойство, а default лежит в свойстве default. Обращаться придётся через точку, зато сразу видно, из какого модуля приехала функция.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Импортированное имя не копирует значение, а ссылается на переменную внутри модуля. Если модуль экспортирует let count и сам его увеличивает, импортирующий файл увидит новое число, хотя ничего не перезапрашивал. Присвоить импортированному имени нельзя — оно доступно только для чтения, менять его вправе только сам модуль.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Когда модулей становится десяток, неудобно помнить, что hi живёт в hi.js, а bye — в bye.js. Поэтому делают файл-витрину, обычно index.js, который ничего не придумывает сам, а пропускает чужое наружу: export { hi } from "./hi.js";.

Формы такие же гибкие, как у обычного экспорта: export { bye as farewell } from "./bye.js" переименует, export * from "./math.js" пропустит все именованные экспорты, export { default as Bye } from "./bye.js" вытащит default и даст ему имя. Внутренние файлы после этого можно свободно переставлять — снаружи виден только адрес витрины.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

export ... from — это не импорт: внутри самого index.js имя hi так и не появится. Нужно им пользоваться — импортируйте отдельной строкой.

import и export разрешены только на верхнем уровне файла, а путь обязан быть строкой-литералом: ни переменной, ни склейки, ни if вокруг. Такая жёсткость даёт главное — граф зависимостей известен ещё до того, как выполнится первая строка. Поэтому импорты «поднимаются»: модуль-сосед загрузится, даже если строка import стоит в самом низу файла.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Из той же статичности растёт tree shaking. Сборщик читает исходники, не запуская их, и видит: из библиотеки на сто функций проект берёт три. Остальные девяносто семь в бандл не попадут. Форма импорта при этом почти не важна: import { debounce } from "lodash-es" и import * as _ с вызовом _.debounce() дают одинаковый бандл — esbuild и webpack видят, к какому свойству пространства имён обращается код. Важно другое: библиотека должна раскладывать код на отдельные экспорты, а не отдавать его одним большим объектом.

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Сборщик осторожен. Если на верхнем уровне модуля есть побочный эффект — регистрация обработчика, подключение стилей, запись в глобальный объект, — выбросить такой модуль нельзя: вдруг весь смысл в этом эффекте. Библиотеки помечают себя честными через поле sideEffects в package.json, и тогда неиспользуемые файлы отбрасываются смело.

Второй частый случай — старый формат require(). CommonJS-модуль сборщик выполняет целиком: какие свойства module.exports понадобятся, видно только в рантайме, поэтому в бандл едет весь файл — даже если из него взяли одну функцию. Бывает и хуже: путь в require() склеен из переменных, и тогда неизвестно даже то, какой файл нужен.

  • export открывает имена наружу, import забирает их по тем же именам — буква в букву; всё непомеченное остаётся приватным
  • Именованных экспортов сколько угодно, export default — не больше одного, и имя ему даёт тот, кто импортирует
  • as переименовывает и при импорте, и при экспорте; import * as собирает весь модуль в объект-пространство имён
  • export ... from делает файл-витрину: значения проходят насквозь, внутри самой витрины их имена не появляются
  • Импорты статичны — пути известны до выполнения, поэтому сборщик строит граф и выбрасывает неиспользованное; для условной загрузки есть import()

1/3Сколько export default может быть в одном модуле?

2/3Что делает import * as utils from "./utils.js"?

3/3Почему нельзя написать if (flag) { import { x } from "./x.js"; }?

Витрина пакета

Есть два файла. В hi.js — именованный экспорт: export function hi(name) { ... }. В bye.js — export default function bye(name) { ... }. Соберите index.js так, чтобы соседний файл мог написать одну строку: import { hi, bye } from "./index.js".

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Обе строки — реэкспорт, без промежуточного импорта. Второй случай интересен тем, что default превращается в именованный экспорт:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Внутри самого index.js имена hi и bye при этом не появятся — файл только пропускает их через себя.

Два одинаковых имени

Модули date.js и money.js оба экспортируют функцию с именем format. Нужно использовать обе в одном файле. Напишите строки импорта.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Каждое имя приезжает под своим псевдонимом:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Альтернатива без переименования — забрать модули целиком: import * as date from "./date.js", и тогда вызывать date.format(...). Конфликта не будет, но каждое обращение станет длиннее.