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';