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

Модулі

10 завдань

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

Навіщо модулі, іменовані/default експорти, імпорт, barrel

#
**Навіщо існують модулі** До ES модулів кожен тег `<script>` ділив єдиний глобальний простір імен. Бібліотеки мусили розміщувати всe під одну глобальну змінну (`jQuery`, `_`, `React`) щоб уникнути колізій, а порядок завантаження мав критичне значення. Модулі вирішують це давши кожному файлу власний простір — нічого не витікає назовні якщо явно не експортовано: ```js // Без модулів: всe глобальне, ризик колізій 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';