Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Модулі JavaScript: import/export, CommonJS проти es модулів». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Модулі JavaScript** - це файли з власною областю видимості, які відкривають код через імпорти та експорти. CommonJS (`require`/`module.exports`) завантажує синхронно у рантаймі. ES Modules (`import`/`export`) парсяться статично і підтримують tree-shaking та top-level `await`. ```javascript export const PI = 3.14159; // ESM named export module.exports = { PI }; // CommonJS аналог ``` **Ключове:** ESM статичний (парситься до запуску), CommonJS динамічний (під час виконання). Для нового коду - тільки ESM.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Модулі JavaScript** розбивають код на окремі файли, кожен з яких має свою область видимості. Є дві системи: CommonJS (`require`/`module.exports`), стандарт Node.js до появи ESM, та ES Modules (`import`/`export`), сучасний веб-стандарт. ## Теорія ### TL;DR - Аналогія: модулі як кухні в ресторані. Кожна виставляє страви через меню (exports), а інші кухні беруть що треба, не маючи доступу до сировини одна одної. Глобальний scope залишається чистим. - CommonJS завантажує синхронно під час виконання. ES Modules парсяться статично під час завантаження. - ESM підтримує tree-shaking; CommonJS ні. - Node 14+ підтримує обидва формати. Браузери підтримують тільки ESM нативно. - Правило вибору: ESM для всього нового. CommonJS тільки для legacy-кодобаз на старому Node. ### Швидкий приклад Одні й ті самі математичні утиліти, два формати: ```javascript // CommonJS const PI = 3.14159; function add(a, b) { return a + b; } module.exports = { PI, add }; const math = require('./math.cjs'); console.log(math.add(math.PI, 1)); // 4.14159 // ES Modules export const PI = 3.14159; export function add(a, b) { return a + b; } import { PI, add } from './math.mjs'; console.log(add(PI, 1)); // 4.14159 ``` ESM дозволяє імпортувати тільки потрібне за іменем. CommonJS повертає весь об'єкт `module.exports`, а ти вже деструктуруєш його сам. ### Головна різниця ES Modules будують статичний граф залежностей під час парсингу. Бандлери на кшталт Webpack або Rollup читають цей граф до запуску коду, тому можуть видаляти невикористані експорти (tree-shaking). CommonJS виконує `require()` у рантаймі, і бандлер не може заздалегідь знати, що реально використовується. Саме ця різниця пояснює, чому перехід бібліотеки з CJS на ESM помітно зменшує розмір бандлу. ESM також підтримує top-level `await`; CommonJS ні, бо синхронне завантаження і async несумісні. ### Коли що використовувати - Браузер або Vite/Webpack: ESM. Нативна підтримка, tree-shaking, без зайвих налаштувань. - Node.js 14+: ESM з розширенням `.mjs` або `"type": "module"` у `package.json`. - Node.js старший за 14, або є потреба в `require(variable)` у рантаймі: CommonJS. - Змішані кодобази: динамічний `import()` працює і з ESM, і з CommonJS файлів. ### Таблиця порівняння | Властивість | ES Modules | CommonJS | |---|---|---| | Синтаксис | `import` / `export` | `require()` / `module.exports` | | Завантаження | Статичне, під час парсингу | Синхронне, під час виконання | | Підтримка браузерів | Нативна | Потрібен бандлер | | Tree-shaking | Так | Ні | | Top-level `await` | Так | Ні | | Default-експорт | `export default foo` | `module.exports = foo` | | Динамічні імпорти | `await import(variable)` | `require(variable)` напряму | | Стандарт Node.js | Ні (потрібна конфігурація) | Так (до v14) | | Коли використовувати | React 18+, Node 20+, новий код | Legacy Express 4.x, Node < 14 | ### Як це обробляє рушій V8 парсить ESM під час завантаження, будує статичний граф залежностей і прив'язує імена експортів до живих посилань (live bindings) до запуску коду модуля. Node.js спочатку перевіряє поле `"type"` у `package.json`, потім розширення файлу: `.mjs` примусово вмикає ESM, `.cjs` примусово CommonJS. CommonJS зберігає завантажені модулі у `require.cache`, тому повторний `require('./math')` повертає той самий закешований `module.exports` без повторного запуску файлу. ### Типові помилки **Забуте розширення `.js` у Node ESM:** ```javascript import { PI } from './math'; // Error: Cannot find module import { PI } from './math.js'; // Правильно ``` Node ESM вимагає явного розширення. CommonJS вгадує сам. Ця помилка трапляється майже у всіх, хто мігрує з CJS. **`require` у ESM-файлі:** ```javascript // .mjs або package.json з "type": "module" const fs = require('fs'); // SyntaxError: require is not defined in ES module scope import fs from 'node:fs'; // Правильно ``` **Кілька default-експортів:** ```javascript export default foo; export default bar; // SyntaxError: тільки один default на модуль ``` Якщо потрібно більше, використовуй named exports. **Експорт до визначення у CommonJS:** ```javascript module.exports.add = add; // ReferenceError: add is not defined function add(a, b) { return a + b; } // спочатку оголоси module.exports = { add }; // потім експортуй ``` ESM сам підіймає (hoisting) оголошення функцій. **Очікувати tree-shaking від CommonJS у бандлері:** ```javascript // Бандлер не видалить unusedFn у CommonJS module.exports.unusedFn = function() {}; module.exports.usedFn = function() {}; // Переходь на ESM named exports для оптимізації бандлу ``` ### Де зустрічається у продакшені - React 18+: `import React from 'react'` - tree-shaking, ESM стандарт. - Node 20 + Express 5: `"type": "module"`, `import express from 'express'`. - Vite (SvelteKit, Next.js): нативний ESM, динамічний `import()` для code-splitting роутів. - Webpack 5: ESM-вивід за замовчуванням, обробляє CommonJS на вході. - Deno і Bun: тільки ESM, конфігурація не потрібна. ### Питання на співбесіді **Q:** Як працює tree-shaking в ESM, але не в CommonJS? **A:** Експорти ESM - це статичні імена, відомі під час парсингу. Бандлер відстежує, які імена імпортуються, і видаляє решту. У CommonJS `module.exports` - звичайний об'єкт, що формується у рантаймі, тому бандлер не може без запуску визначити, що реально використовується. **Q:** Що відбувається при циклічних залежностях? **A:** ESM частково вирішує прив'язки до запуску коду, тому більшість циклічних імпортів працюють коректно. CommonJS може повернути порожній `{}` для ще не завантаженого модуля, що призводить до помилок у рантаймі, які складно відстежити. **Q:** Як увімкнути ESM у Node.js? **A:** Додай `"type": "module"` у `package.json` або використовуй розширення `.mjs`. Для явного CommonJS - `.cjs`. **Q:** Як працює динамічний `import()`? **A:** `const mod = await import('./math.js')` повертає Promise з простором імен модуля. Працює і в ESM, і в CommonJS файлах - це стандартний спосіб поєднати обидва формати в одному проєкті. **Q:** Чи можна використовувати ESM у браузері без бандлера? **A:** Так. `<script type="module" src="./main.js"></script>` підтримується у всіх сучасних браузерах. Потрібні відносні шляхи з явним розширенням (`./math.js`); bare-специфікатори (`math`) не підтримуються. **Питання рівня senior:** Що саме робить V8, коли зустрічає `import`, і чому це дозволяє top-level `await`? **A:** V8 створює ModuleRecord під час парсингу, асинхронно завантажує і вирішує всі залежності (будує граф), потім прив'язує кожне ім'я експорту до живого посилання до запуску будь-якого коду модуля. Оскільки вся фаза прив'язки асинхронна, рушій може призупинити виконання на `await` без блокування потоку. `require()` у CommonJS - це синхронний виклик: файл виконується одразу, повертає `module.exports` і виходить. Асинхронної фази немає, тому top-level `await` неможливий. ## Приклади ### Іменовані та default-експорти ```javascript // userService.js export const DEFAULT_ROLE = 'viewer'; // іменований експорт export function createUser(name, role = DEFAULT_ROLE) { return { name, role, id: Date.now() }; } export default class UserService { // default-експорт async getUser(id) { return { id, name: 'Alice', role: 'admin' }; } } ``` ```javascript // main.js import UserService, { createUser, DEFAULT_ROLE } from './userService.js'; const service = new UserService(); const user = await service.getUser(1); // { id: 1, name: 'Alice', role: 'admin' } const newUser = createUser('Bob'); // { name: 'Bob', role: 'viewer', id: 1718000000000 } ``` Default і named exports можуть бути в одному файлі. Default - для головного, що надає модуль; named - для допоміжного. ### Динамічні імпорти і barrel-файли ```javascript // services/index.js (barrel-файл) export { default as UserService } from './userService.js'; export { default as ProductService } from './productService.js'; export * from './utils.js'; // app.js - зрозумілі шляхи імпорту import { UserService, ProductService } from './services/index.js'; ``` Barrel-файли спрощують шляхи імпорту по всій кодобазі. Vite і Webpack 5 правильно проводять через них tree-shaking. Старіші бандлери можуть підтягнути більше, ніж очікується, тому перевіряй розмір бандлу після додавання нових barrel-файлів. Динамічний імпорт для завантаження за потребою: ```javascript // Важкий модуль завантажується тільки тоді, коли дійсно потрібен async function handleExport(data) { const { generatePDF } = await import('./pdfGenerator.js'); return generatePDF(data); } // React lazy loading - той самий механізм const ReportPage = React.lazy(() => import('./pages/ReportPage.js')); ``` ### Поведінка при циклічних залежностях Це те, що відрізняє кандидатів, які реально читали специфікацію, від тих, хто знає тільки синтаксис. ```javascript // a.mjs import { fnB } from './b.mjs'; export function fnA() { return fnB(); } // b.mjs import { fnA } from './a.mjs'; export function fnB() { return 'B called'; } // main.mjs import { fnA } from './a.mjs'; console.log(fnA()); // 'B called' - ESM справляється коректно ``` ESM вирішує це через живі прив'язки (live bindings). До моменту, коли `fnA` реально викликає `fnB`, прив'язка вже встановлена. У CommonJS те саме ламається: `require('./b')` під час завантаження `a` повертає `{}`, бо `b` ще не завершив завантаження. Я бачив, як цей патерн давав тихі помилки `undefined is not a function` в Express-додатках, де два сервісні файли імпортували один одного - такі баги знаходяться годинами.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.