JS

JavaScript Fundamentals

25 lessons

Progress0%
1. Introduction to JavaScript
1What is JavaScript?2Setting Up Your Environment
2. Variables and Data Types
1Declaring Variables2Data Types3Type Conversion
3. Operators
Arithmetic OperatorsComparison OperatorsLogical Operators
4. Control Flow
Conditional StatementsLoops
5. Functions
Function Basics
6. Arrays & Iteration
Array MethodsSpread, Rest & Destructuring
7. Objects & JSON
Working with ObjectsJSON & Optional Chaining
8. OOP & Classes
Class BasicsInheritance & Private Fields
9. Modules & Modern JS
ES ModulesModern JavaScript Features
10. Async JavaScript
PromisesAsync/Await
11. Error Handling
Error Types & try/catchCustom Errors & Debugging
12. Iterators & Advanced
Iterators & GeneratorsMap, Set & WeakRefs
All Tutorials
JavaScriptModules & Modern JS
Lesson 18 of 25 min
Chapter 9 · Lesson 1

ES Modules

ES Modules

ES Modules (ESM) are the official, standardized module system built into JavaScript since ES2015. They allow you to split code across multiple files and explicitly control what is exported and imported.

Why Modules?

Before modules, all JavaScript ran in a single global scope. Variables from one file could collide with another. Modules give each file its own scope — nothing leaks unless you explicitly export it.

Named Exports

Export as many things as you like from a file:

js
// math.js
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export class Vector { ... }

Import specific names:

js
import { PI, add } from "./math.js";

Default Exports

Each file can have one default export — typically the main thing the module represents:

js
// greet.js
export default function greet(name) {
  return `Hello, ${name}!`;
}
js
import greet from "./greet.js";      // any name works for default
import sayHi from "./greet.js";      // also valid

Combining Named and Default

js
import defaultExport, { namedOne, namedTwo } from "./module.js";

Aliasing with as

js
import { add as sum } from "./math.js";
import * as MathUtils from "./math.js"; // namespace import

Re-exports

Re-export from a module without importing first:

js
export { add, PI } from "./math.js";
export { default as greet } from "./greet.js";

This is common in index.js barrel files that aggregate a library's public API.

Dynamic import()

import() is a function that returns a Promise, enabling lazy loading:

js
const module = await import("./heavy.js");
module.default();

Dynamic imports are critical for code-splitting in bundlers like Webpack and Vite — only load code when it is actually needed.

Module Characteristics

  • Modules are strict mode by default — no need for "use strict"
  • Modules are singletons — the same module imported twice returns the same cached instance
  • Top-level await is allowed in modules

Code Examples

Named and Default Exportsjavascript
// Simulating module exports in Node.js (CommonJS-compatible illustration)
// --- math.js ---
const PI = 3.14159265;

function add(a, b)      { return a + b; }
function multiply(a, b) { return a * b; }
function square(n)      { return n * n; }

// Named exports: { PI, add, multiply, square }
// Default export: the add function

// --- main.js (consumer) ---
// import { PI, add, multiply } from './math.js';
// import square from './math.js';  // default

// Demonstrated inline:
const math = { PI, add, multiply, square };
const { add: sum, multiply: mul } = math;

console.log("PI:", math.PI);
console.log("add(3, 4):", sum(3, 4));
console.log("multiply(6, 7):", mul(6, 7));
console.log("square(9):", math.square(9));

Named exports allow multiple exports per file. Consumers pick exactly what they need. Aliasing with 'as' avoids naming conflicts and allows renaming on import.

Dynamic import() for Lazy Loadingjavascript
// Dynamic import returns a Promise
// Useful for loading heavy code only when needed

async function loadChartLibrary() {
  console.log("Loading chart module...");
  // In a real app: const { Chart } = await import('./chart.js');
  // Simulated here:
  const chartModule = await Promise.resolve({
    default: class Chart {
      constructor(data) { this.data = data; }
      render() { return `Chart with ${this.data.length} points`; }
    }
  });
  return chartModule.default;
}

async function main() {
  console.log("App started");

  // Chart only loaded when this branch is hit
  const userWantsChart = true;
  if (userWantsChart) {
    const Chart = await loadChartLibrary();
    const chart = new Chart([1, 2, 3, 4, 5]);
    console.log(chart.render());
  }

  console.log("App running");
}

main();

Dynamic import() loads a module asynchronously, returning a Promise that resolves to the module object. This enables code-splitting — heavy dependencies are only fetched when actually needed, improving initial load time.

Namespace Imports and Barrel Filesjavascript
// Simulating a barrel file (index.js) pattern
// --- utils/string.js ---
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
const truncate   = (s, n) => s.length > n ? s.slice(0, n) + "..." : s;

// --- utils/number.js ---
const clamp  = (n, min, max) => Math.min(Math.max(n, min), max);
const round2 = (n) => Math.round(n * 100) / 100;

// --- utils/index.js (barrel) ---
// export { capitalize, truncate } from './string.js';
// export { clamp, round2 }       from './number.js';

// Consumer: import * as Utils from './utils/index.js';
const Utils = { capitalize, truncate, clamp, round2 };

console.log(Utils.capitalize("hello world"));
console.log(Utils.truncate("A very long title here", 12));
console.log(Utils.clamp(150, 0, 100));
console.log(Utils.round2(3.14159));

A barrel file (index.js) re-exports from multiple sub-modules, giving consumers a single import point. Namespace imports (import * as Utils) collect all exports under one object.

Quick Quiz

1. How many default exports can a single module file have?

2. What does import * as Utils from './utils.js' do?

3. What does import() (dynamic import) return?

Was this lesson helpful?

PreviousNext