JavaScript · Syntax · Advanced
Modules
Work with ES modules: named and default exports/imports, re-exports, dynamic `import()`, and avoiding circular dependencies.
Quick topic start and explanations before exercises (exercises below):
Dynamic import(), top-level await, import.meta, module caching
#Module vs Script, live bindings, circular deps, tree-shaking
#Exercises:
Named exports and imports
#Write a module `math.js` that exports two named functions: `add(a, b)` and `multiply(a, b)`. In `main.js`, import both and use them.
// math.js
export function add(a, b) {
return a + b;
}
// complete multiply and export it
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
Solution
// 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 export and import
#Create a module `greet.js` with a default export — a function `greet(name)` that returns `'Hello, <name>!'`. Import it in `main.js` under any name and call it.
// greet.js
// add default export here
function greet(name) {
return `Hello, ${name}!`;
}
// main.js
import greet from './greet.js';
console.log(greet('Alice')); // Hello, Alice!
// you can also import under a different name:
import sayHello from './greet.js';
console.log(sayHello('Bob')); // Hello, Bob!
Solution
// 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!
Import aliasing with `as`
#A module `utils.js` exports `formatDate` and `formatCurrency`. Import them both aliased: `formatDate` as `dateStr`, `formatCurrency` as `money`. Also import the entire module as a namespace object `utils` and use `utils.formatDate` and `utils.formatCurrency`.
// utils.js
export function formatDate(d) {
return d.toISOString().slice(0, 10);
}
export function formatCurrency(n) {
return '$' + n.toFixed(2);
}
// main.js - aliased imports:
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
// namespace import:
import * as utils from './utils.js';
console.log(utils.formatDate(new Date('2024-01-15')));
console.log(utils.formatCurrency(9.9));
Solution
// 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));
Re-export: barrel index file
#You have two modules: `circle.js` (exports `area` and `perimeter`) and `rect.js` (exports `area` and `perimeter`). Create an `index.js` that re-exports everything from both, renaming to avoid name collisions. Then import from `index.js` only.
// 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 — re-export with renames:
export { area as circleArea, perimeter as circlePerimeter } from './circle.js';
// add rect re-exports here
// main.js
import { circleArea, rectArea } from './index.js';
console.log(circleArea(5).toFixed(2)); // 78.54
console.log(rectArea(3, 4)); // 12
Solution
// 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
Dynamic import()
#Use `import()` to load a module conditionally. If `useLargeLib` is `true`, dynamically import `heavy.js` (which exports a default `process` function). Otherwise skip the load. Show how to use the loaded module.
// heavy.js
export default function process(data) {
return data.toUpperCase();
}
// main.js
async function run(useLargeLib) {
if (useLargeLib) {
// dynamically import heavy.js here
// call the default export with 'hello'
} else {
console.log('Skipped heavy module');
}
}
run(true);
Solution
// 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('Skipped heavy module');
}
}
run(true);
Module as a singleton
#Create a `config.js` module that exports a single mutable `config` object. Show that importing it in two places gives the same object reference — mutations in one place are visible in the other.
// config.js
export const config = { debug: false, theme: 'light' };
// a.js
import { config } from './config.js';
config.debug = true; // mutate
console.log('In a.js:', config.debug); // true
// b.js
import { config } from './config.js';
console.log('In b.js:', config.debug); // also true — same object!
Solution
// 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 (same module instance)
Understand circular dependency risk
#`a.js` imports from `b.js`, and `b.js` imports from `a.js` — circular. Show what happens: `a.js` exports `A = 'A'`, imports `B` from `b.js`. `b.js` exports `B = 'B'`, imports `A` from `a.js`. In `main.js`, import both and log them. Then rewrite to break the cycle using a shared `constants.js`.
// --- CIRCULAR (problematic) ---
// a.js
import { B } from './b.js';
export const A = 'A';
console.log('In a.js, B =', B); // may be undefined!
// b.js
import { A } from './a.js';
export const B = 'B';
console.log('In b.js, A =', A); // may be undefined!
// --- FIX: break the cycle ---
// constants.js
// export A and B from here, no imports
Solution
// CIRCULAR — a.js imports b, b imports a
// When a.js is first evaluated, b.js is not yet done,
// so B may be undefined when a.js runs its top-level code.
// --- FIX ---
// constants.js
export const A = 'A';
export const B = 'B';
// a.js
import { B } from './constants.js'; // no cycle
console.log('In a.js, B =', B); // 'B' (always defined)
// b.js
import { A } from './constants.js'; // no cycle
console.log('In b.js, A =', A); // 'A' (always defined)
Dynamic import with error handling
#Write an async function `loadPlugin(name)` that dynamically imports `./plugins/${name}.js` and calls its default-exported `init()` function. If the import fails (e.g. module not found), catch the error and log `'Plugin not found: <name>'`.
async function loadPlugin(name) {
try {
// dynamically import the plugin
// call the init() function
} catch (err) {
console.log(`Plugin not found: ${name}`);
}
}
await loadPlugin('analytics');
await loadPlugin('nonexistent'); // Plugin not found: nonexistent
Solution
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 in modules
#In a module `data.js`, use top-level `await` to fetch data before exporting it. Simulate this with a `delay` function. Export the resolved `data` const. In `main.js`, import and use it — no extra await needed.
// 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';
// by the time this runs, data is already resolved
console.log(data.loaded); // true
console.log(data.items); // [1, 2, 3]
Solution
// 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]
Module vs script: key differences
#Demonstrate three key differences between ES modules and classic scripts: 1. Module-scoped `this` is `undefined` at top level. 2. Modules have their own scope — variables are not global. 3. `import` declarations are hoisted (you can import at top, use at bottom). Show each with a comment explaining what the output would be.
// module.js (as a module — type='module' in HTML or .mjs in Node)
// 1. this at top level
console.log(typeof this); // 'undefined' (in module)
// 'object' (window) in classic script
// 2. module scope — x is NOT global
const x = 42;
console.log(typeof globalThis.x); // 'undefined' in module
// '42' if classic script
// 3. imports are hoisted — you can use them before their textual position
console.log(typeof add); // 'function' (import is hoisted)
import { add } from './math.js';
Solution
// module.js
// 1. this at top level in a module is undefined
console.log(typeof this); // 'undefined'
// 2. module-scoped const — not attached to globalThis
const x = 42;
console.log(typeof globalThis.x); // 'undefined'
// 3. import declarations are hoisted — safe to reference before textual position
console.log(typeof add); // 'function'
import { add } from './math.js';