JavaScript · Синтаксис · Продвинутый уровень

Модули

10 задач

Работайте с ES-модулями: именованные и default-экспорты/импорты, реэкспорт, динамический `import()` и избегание циклических зависимостей.

Зачем модули, именованные/default экспорты, импорт, barrel

#
**Зачем существуют модули** До ES модулей каждый тег `<script>` разделял единое глобальное пространство имён. Библиотеки вынуждены были размещать всё под одну глобальную переменную (`jQuery`, `_`, `React`) чтобы избежать коллизий, а порядок загрузки имел критическое значение. Модули решают это давая каждому файлу собственную область — ничего не утекает наружу если явно не экспортировано: ```js // Без модулей: всё глобальное, риск коллизий var PI = 3.14; // коллизия если другой скрипт объявляет PI // С модулями: полностью изолировано const PI = 3.14; // видно только внутри этого файла export function add(a, b) { return a + b; } // явно расшарено ``` **Именованные и default-экспорты** ```js // --- math.js --- export function add(a, b) { return a + b; } // именованный export function sub(a, b) { return a - b; } // именованный export const PI = 3.14159; export default class Calculator { // default (один на модуль) add(a, b) { return a + b; } } ``` **Импорт** ```js import { add, PI } from './math.js'; // именованные import { add as sum } from './math.js'; // с переименованием import * as math from './math.js'; // namespace-объект import Calculator from './math.js'; // default import Calculator, { add, PI } from './math.js'; // оба ``` **Re-exports — barrel-файлы** ```js // --- src/utils/index.js --- export { add, PI } from './math.js'; export { default as Calculator } from './math.js'; export * from './strings.js'; // --- потребитель --- import { add, Calculator } from './src/utils'; // один чистый импорт ```

Динамический import(), top-level await, import.meta, кэширование

#
**Динамический `import()` — загрузка по требованию** Статические импорты выполняются при загрузке модуля. `import()` возвращает Promise и позволяет загрузить модуль только когда он действительно нужен: ```js // Условная загрузка async function loadChart() { if (!userWantsChart) return; const { default: Chart } = await import('./chart.js'); return new Chart(); } // Параллельная загрузка const [{ default: A }, { default: B }] = await Promise.all([ import('./a.js'), import('./b.js'), ]); // Динамический путь const locale = navigator.language.slice(0, 2); const { messages } = await import(`./locales/${locale}.js`); ``` **Обработка ошибок** ```js try { const mod = await import('./heavy-plugin.js'); mod.init(); } catch (err) { console.warn('Плагин не загрузился', err); } ``` **Top-level `await`** Внутри модуля можно `await` напрямую (не внутри функции). Граф модулей ждёт пока он не разрешится прежде чем запустить любого импортера: ```js // --- db.js --- const connection = await connectToDatabase(config); export { connection }; // --- main.js --- import { connection } from './db.js'; // гарантированно готово ``` **`import.meta` — метаданные модуля** ```js import.meta.url; // полный URL этого файла модуля import.meta.env.DEV; // true в режиме разработки (Vite) const dataPath = new URL('./data.json', import.meta.url).pathname; ``` **Кэширование модулей** Модуль выполняется только один раз, независимо от числа импортёров. Все они разделяют один экземпляр: ```js // counter.js let count = 0; export const inc = () => ++count; export const get = () => count; // a.js: inc(); inc(); // b.js: get(); // 2 — тот же экземпляр ```

Модуль vs Скрипт, живые связывания, циклические зависимости, tree-shaking

#
**Модуль vs Скрипт** | Свойство | Скрипт | Модуль | |---|---|---| | Область видимости | Глобальная | Локальная для модуля | | `this` на верхнем уровне | `window` | `undefined` | | Strict mode | по выбору | всегда включён | | `import`/`export` | не разрешено | разрешено | | Выполнение | раз на тег `<script>` | раз, затем кэшируется | | HTML | `<script>` | `<script type="module">` | | Отложенность | нет (блокирует HTML) | да (отложено по умолчанию) | **Импорт — живое связывание, не копия** В отличие от CommonJS, ES-импорты являются живыми связываниями. Когда экспортирующий модуль меняет значение — импортёры видят обновление: ```js // counter.js export let count = 0; export function inc() { count++; } // main.js import { count, inc } from './counter.js'; console.log(count); // 0 inc(); console.log(count); // 1 — живое связывание, не снимок ``` **Циклические зависимости** ``` ПЛОХО: a.js --> b.js --> a.js (цикл) ХОРОШО: a.js \ --> shared.js b.js / ``` Если в выводе бандлера есть цикл — вынесите общий код в третий файл. Циклы — запах дизайна, не ограничение бандлера. **Tree-shaking** Бандлеры (Vite, webpack, Rollup) удаляют неиспользуемые экспорты из финального бандла поскольку структура ES модулей статична: ```js // utils.js export function used() { ... } export function neverImported() { ... } // удаляется бандлером // main.js import { used } from './utils.js'; // только 'used' в бандле ``` Tree-shaking работает только с именованными статическими импортами. Динамический `import()` и `export default` (один объект) мешают ему.
01

Именованные экспорты и импорты

#

Напишите модуль `math.js`, экспортирующий две именованные функции: `add(a, b)` и `multiply(a, b)`. В `main.js` импортируйте обе и используйте их.

// math.js
export function add(a, b) {
  return a + b;
}

// завершите multiply и экспортируйте её
function multiply(a, b) {
  return a * b;
}

// main.js
import { add, multiply } from './math.js';
console.log(add(2, 3));       // 5
console.log(multiply(4, 5));  // 20
Решение
// math.js
export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

// main.js
import { add, multiply } from './math.js';
console.log(add(2, 3));       // 5
console.log(multiply(4, 5));  // 20
02

Default-экспорт и импорт

#

Создайте модуль `greet.js` с default-экспортом — функцией `greet(name)`, возвращающей `'Hello, <name>!'`. Импортируйте её в `main.js` под любым именем и вызовите.

// greet.js
// добавьте default export здесь
function greet(name) {
  return `Hello, ${name}!`;
}

// main.js
import greet from './greet.js';
console.log(greet('Alice')); // Hello, Alice!

// можно импортировать под другим именем:
import sayHello from './greet.js';
console.log(sayHello('Bob')); // Hello, Bob!
Решение
// greet.js
export default function greet(name) {
  return `Hello, ${name}!`;
}

// main.js
import greet from './greet.js';
console.log(greet('Alice')); // Hello, Alice!

import sayHello from './greet.js';
console.log(sayHello('Bob')); // Hello, Bob!
03

Псевдонимы импорта с `as`

#

Модуль `utils.js` экспортирует `formatDate` и `formatCurrency`. Импортируйте оба с псевдонимами: `formatDate` как `dateStr`, `formatCurrency` как `money`. Также импортируйте весь модуль как объект пространства имён `utils` и используйте `utils.formatDate` и `utils.formatCurrency`.

// utils.js
export function formatDate(d) {
  return d.toISOString().slice(0, 10);
}
export function formatCurrency(n) {
  return '$' + n.toFixed(2);
}

// main.js — импорты с псевдонимами:
import { formatDate as dateStr, formatCurrency as money } from './utils.js';
console.log(dateStr(new Date('2024-01-15'))); // 2024-01-15
console.log(money(9.9));                      // $9.90

// импорт пространства имён:
import * as utils from './utils.js';
console.log(utils.formatDate(new Date('2024-01-15')));
console.log(utils.formatCurrency(9.9));
Решение
// utils.js
export function formatDate(d) {
  return d.toISOString().slice(0, 10);
}
export function formatCurrency(n) {
  return '$' + n.toFixed(2);
}

// main.js
import { formatDate as dateStr, formatCurrency as money } from './utils.js';
console.log(dateStr(new Date('2024-01-15'))); // 2024-01-15
console.log(money(9.9));                      // $9.90

import * as utils from './utils.js';
console.log(utils.formatDate(new Date('2024-01-15')));
console.log(utils.formatCurrency(9.9));
04

Реэкспорт: barrel index файл

#

У вас есть два модуля: `circle.js` (экспортирует `area` и `perimeter`) и `rect.js` (экспортирует `area` и `perimeter`). Создайте `index.js`, реэкспортирующий всё из обоих, переименовывая для избежания коллизий. Затем импортируйте только из `index.js`.

// circle.js
export const area = r => Math.PI * r * r;
export const perimeter = r => 2 * Math.PI * r;

// rect.js
export const area = (w, h) => w * h;
export const perimeter = (w, h) => 2 * (w + h);

// index.js — реэкспорт с переименованием:
export { area as circleArea, perimeter as circlePerimeter } from './circle.js';
// добавьте реэкспорты rect здесь

// main.js
import { circleArea, rectArea } from './index.js';
console.log(circleArea(5).toFixed(2));  // 78.54
console.log(rectArea(3, 4));             // 12
Решение
// circle.js
export const area = r => Math.PI * r * r;
export const perimeter = r => 2 * Math.PI * r;

// rect.js
export const area = (w, h) => w * h;
export const perimeter = (w, h) => 2 * (w + h);

// index.js
export { area as circleArea, perimeter as circlePerimeter } from './circle.js';
export { area as rectArea, perimeter as rectPerimeter } from './rect.js';

// main.js
import { circleArea, rectArea } from './index.js';
console.log(circleArea(5).toFixed(2));  // 78.54
console.log(rectArea(3, 4));             // 12
05

Динамический import()

#

Используйте `import()` для условной загрузки модуля. Если `useLargeLib` равно `true`, динамически импортируйте `heavy.js` (экспортирующий default-функцию `process`). Иначе пропустите загрузку. Покажите, как использовать загруженный модуль.

// heavy.js
export default function process(data) {
  return data.toUpperCase();
}

// main.js
async function run(useLargeLib) {
  if (useLargeLib) {
    // динамически импортируйте heavy.js здесь
    // вызовите default-экспорт с 'hello'
  } else {
    console.log('Пропущен тяжёлый модуль');
  }
}

run(true);
Решение
// heavy.js
export default function process(data) {
  return data.toUpperCase();
}

// main.js
async function run(useLargeLib) {
  if (useLargeLib) {
    const { default: process } = await import('./heavy.js');
    console.log(process('hello')); // HELLO
  } else {
    console.log('Пропущен тяжёлый модуль');
  }
}

run(true);
06

Модуль как singleton

#

Создайте модуль `config.js`, экспортирующий один мутируемый объект `config`. Покажите, что импортирование его в двух местах даёт один и тот же объект — мутации в одном месте видны в другом.

// config.js
export const config = { debug: false, theme: 'light' };

// a.js
import { config } from './config.js';
config.debug = true;  // мутируем
console.log('In a.js:', config.debug); // true

// b.js
import { config } from './config.js';
console.log('In b.js:', config.debug); // тоже true — тот же объект!
Решение
// config.js
export const config = { debug: false, theme: 'light' };

// a.js
import { config } from './config.js';
config.debug = true;
console.log('In a.js:', config.debug); // true

// b.js
import { config } from './config.js';
console.log('In b.js:', config.debug); // true (тот же экземпляр)
07

Понять риск циклической зависимости

#

`a.js` импортирует из `b.js`, а `b.js` импортирует из `a.js` — круговая зависимость. Покажите что происходит: `a.js` экспортирует `A = 'A'`, импортирует `B` из `b.js`. `b.js` экспортирует `B = 'B'`, импортирует `A` из `a.js`. В `main.js` импортируйте оба и выведите. Затем перепишите для разрыва цикла через общий `constants.js`.

// --- КРУГОВАЯ (проблемная) ---
// a.js
import { B } from './b.js';
export const A = 'A';
console.log('In a.js, B =', B);  // может быть undefined!

// b.js
import { A } from './a.js';
export const B = 'B';
console.log('In b.js, A =', A);  // может быть undefined!

// --- ИСПРАВЛЕНИЕ: разрыв цикла ---
// constants.js
// экспортируйте A и B здесь, без импортов
Решение
// КРУГОВАЯ — a.js импортирует b, b импортирует a
// При первом вычислении a.js, b.js ещё не завершён,
// поэтому B может быть undefined в момент выполнения кода верхнего уровня a.js.

// --- ИСПРАВЛЕНИЕ ---
// constants.js
export const A = 'A';
export const B = 'B';

// a.js
import { B } from './constants.js';  // без цикла
console.log('In a.js, B =', B);

// b.js
import { A } from './constants.js';  // без цикла
console.log('In b.js, A =', A);
08

Динамический import с обработкой ошибок

#

Напишите асинхронную функцию `loadPlugin(name)`, динамически импортирующую `./plugins/${name}.js` и вызывающую его default-exported функцию `init()`. Если импорт не удастся (модуль не найден), перехватите ошибку и выведите `'Plugin not found: <name>'`.

async function loadPlugin(name) {
  try {
    // динамически импортируйте плагин
    // вызовите функцию init()
  } catch (err) {
    console.log(`Plugin not found: ${name}`);
  }
}

await loadPlugin('analytics');
await loadPlugin('nonexistent');  // Plugin not found: nonexistent
Решение
async function loadPlugin(name) {
  try {
    const { default: init } = await import(`./plugins/${name}.js`);
    init();
  } catch (err) {
    console.log(`Plugin not found: ${name}`);
  }
}

await loadPlugin('analytics');
await loadPlugin('nonexistent');  // Plugin not found: nonexistent
09

Top-level await в модулях

#

В модуле `data.js` используйте top-level `await` для получения данных перед их экспортом. Симулируйте через функцию `delay`. Экспортируйте константу `data` после разрешения. В `main.js` импортируйте и используйте — никакого дополнительного await не нужно.

// data.js
const delay = ms => new Promise(r => setTimeout(r, ms));

await delay(10);  // top-level await
export const data = { loaded: true, items: [1, 2, 3] };

// main.js
import { data } from './data.js';
// к моменту запуска data уже разрешена
console.log(data.loaded);  // true
console.log(data.items);   // [1, 2, 3]
Решение
// data.js
const delay = ms => new Promise(r => setTimeout(r, ms));

await delay(10);
export const data = { loaded: true, items: [1, 2, 3] };

// main.js
import { data } from './data.js';
console.log(data.loaded);  // true
console.log(data.items);   // [1, 2, 3]
10

Модуль vs скрипт: ключевые отличия

#

Продемонстрируйте три ключевых отличия между ES-модулями и классическими скриптами: 1. Модульный `this` на верхнем уровне — `undefined`. 2. Модули имеют собственный scope — переменные не глобальные. 3. `import`-объявления всплывают (можно импортировать сверху, использовать снизу). Покажите каждое с комментарием об ожидаемом выводе.

// module.js (как модуль — type='module' в HTML или .mjs в Node)

// 1. this на верхнем уровне
console.log(typeof this); // 'undefined' (в модуле)
                          // 'object' (window) в классическом скрипте

// 2. scope модуля — x НЕ является глобальным
const x = 42;
console.log(typeof globalThis.x); // 'undefined' в модуле

// 3. import всплывает — можно использовать до текстовой позиции
console.log(typeof add);  // 'function'
import { add } from './math.js';
Решение
// module.js

// 1. this на верхнем уровне модуля — undefined
console.log(typeof this); // 'undefined'

// 2. const в scope модуля — не прикреплён к globalThis
const x = 42;
console.log(typeof globalThis.x); // 'undefined'

// 3. import всплывает — безопасно ссылаться до текстовой позиции
console.log(typeof add);  // 'function'
import { add } from './math.js';