JavaScript · Синтаксис · Продвинутый уровень
Модули
Работайте с ES-модулями: именованные и default-экспорты/импорты, реэкспорт, динамический `import()` и избегание циклических зависимостей.
Краткое введение в тему и пояснения перед упражнениями (упражнения ниже):
Динамический import(), top-level await, import.meta, кэширование
#Модуль vs Скрипт, живые связывания, циклические зависимости, tree-shaking
#Упражнения:
Именованные экспорты и импорты
#Напишите модуль `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
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!
Псевдонимы импорта с `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));
Реэкспорт: 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
Динамический 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);
Модуль как 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 (тот же экземпляр)
Понять риск циклической зависимости
#`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);
Динамический 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
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]
Модуль 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';