← Back to list

JavaScript Modules: Everything You Need to Know

From global scope chaos to clean, isolated, production-grade code

Akisha · 2026-04-21 07:23 · 1 claps · 5.8 min read
#javascript #es6-module #deep-learning
Open on Medium ↗
Wiki topics: ML · Machine Learning EDU · Education & Learning 🌐 · Web Development

JavaScript Modules: Everything You Need to Know

From global scope chaos to clean, isolated, production-grade code

The Problem — Global Scope Pollution

Before modules existed, every JavaScript file shared one global object: window. Every variable, every function you declared landed there automatically.

// math.js
var count = 0;
function reset() { count = 0; }
// app.js (loaded after math.js)
var count = 100;        // silently overwrites math.js's count
function reset() {      // silently overwrites math.js's reset
  console.log("different reset!");
}

No error. No warning. Just silent destruction. This is global scope pollution — and it gets catastrophic at scale.

Two Mechanisms That Fix Everything

The entire module pattern is built on exactly two JavaScript features.

1. Functions Create a New Scope

var x = "global";
function myFunc() {
  var x = "local"; // completely separate from the global x
  console.log(x);  // "local"
}
myFunc();
console.log(x); // "global" — untouched

Variables declared inside a function cannot leak out. This is the privacy wall.

2. Closures — Inner Functions Remember Their Birth Scope

function outer() {
  let secret = 42;
  function inner() {
    console.log(secret); // inner "closes over" secret
  }
  return inner;
}
const fn = outer(); // outer() has finished running...
fn(); // 42 — but secret is still alive because inner holds a reference

Even after outer() finishes, secret is not garbage collected because inner still references it. This is a closure — a function bundled together with its surrounding state.

The Classic IIFE Module Pattern

Combine both mechanisms and you get the original module pattern:

const Counter = (function () {
  let count = 0;       // private — nobody outside can touch this
  function log() {
    console.log(`Count: ${count}`);
  }
  return {
    increment() { count++; log(); },
    decrement() { count--; log(); },
    getCount()  { return count; }
  };
})(); // ← IIFE: runs immediately, then self-destructs

Here’s what happens step by step:

  1. The IIFE runs immediately
  2. count and log are created in a private scope
  3. An object with 3 methods is returned — each method is a closure
  4. The IIFE’s scope disappears… but count and log stay alive because the 3 methods still reference them
  5. The returned object is stored in Counter
Counter.increment(); // Count: 1
Counter.increment(); // Count: 2
Counter.decrement(); // Count: 1
console.log(Counter.count);     // undefined — truly private
console.log(Counter.getCount()); // 1 — only via the method

The Revealing Module Variant

A cleaner version — define everything privately, then selectively reveal what you want:

const UserModule = (function () {
  let _name = "Guest";
  function _validate(name) {
    return name.length > 2;
  }
  function setName(name) {
    if (_validate(name)) _name = name;
  }
  function getName() {
    return _name;
  }
  return { setName, getName }; // only these are public
})();

The Mental Model

Think of it like a vending machine:

  • You see buttons (public API — increment, getCount)
  • You can’t reach inside and grab the coins directly (private state — count)
  • The machine’s internal logic is hidden from you (private functions — log)

Classic Script vs Module — How the Browser Handles Each

Classic <script> — Blocks the Page

html

<body>
  <h1>Hello</h1>
  <script src="app.js"></script>   <!-- parser stops here -->
  <p>This won't render until app.js finishes</p>
</body>

The moment the HTML parser hits a <script> tag it stops everything. The browser will not render content below it, will not parse any more HTML — nothing — until the script is fully downloaded and executed.

The full classic script pipeline:

HTML parser hits <script>
        ↓
HTML parsing PAUSES
        ↓
Network request for file (blocking)
        ↓
File downloads
        ↓
Executes immediately — top to bottom
in the GLOBAL scope (no wrapper)
        ↓
window.* gets populated
        ↓
HTML parsing RESUMES

Every variable lands on window automatically. No scoping, no protection.

<script type="module"> — Never Blocks

<body>
  <h1>Hello</h1>
  <script type="module" src="app.js"></script>  <!-- parser keeps going -->
  <p>This renders immediately — no waiting</p>
</body>

The HTML parser never stops. The module is fetched in the background and only executes after the entire HTML document is parsed. defer is automatic and built-in for modules.

The full ES module pipeline:

File found
      ↓
Own scope created (not global)
      ↓
Imports scanned — static analysis
      ↓
Dependency graph built
      ↓
All files fetched in parallel
      ↓
Exports registered (lookup table)
      ↓
Imports linked as live bindings
      ↓
Execution — deepest dependency first
      ↓
Module cached — never runs again

async on a Module

<script type="module" async src="app.js"></script>

The module runs as soon as it downloads — even if HTML isn’t fully parsed yet. Useful for independent scripts like analytics that don’t depend on the DOM.

The Three Behaviors at a Glance

Normal <script>      Fetch ──► Execute ──► HTML resumes
                     (HTML blocked the whole time)
<script defer>       HTML keeps parsing ──────────────► Execute
or type="module"     Fetch happens in background ──────►
<script async>       HTML keeps parsing ──► maybe pause ──► resume
or module async      Fetch in background ──► Execute when ready

The window Object — One World vs Many Bubbles

This is one of the most important practical differences between classic scripts and modules.

Normal Scripts — Everyone Shares One window

// math.js
var count = 0;
function add(a, b) { return a + b; }
// user.js
var count = 999;    // overwrites math.js's count
var name = "Alice";
// From anywhere:
console.log(window.count); // 999 — math.js's count is gone
console.log(window.add);   // function — from math.js

All files write to the exact same window bucket. Last writer wins.

Modules — Every File Gets Its Own Scope Bubble

// math.js (module)
let count = 0;               // stays HERE only
function add(a, b) { return a + b; }
// user.js (module)
let count = 999;             // completely separate — no conflict
let name = "Alice";          // stays HERE only
// From anywhere:
console.log(window.count);  // undefined — not on window
console.log(window.add);    // undefined — not on window

Same variable name count in two files — zero conflict. They don't even know each other exists.

There is still only one window — modules don't create multiple windows. But module variables sit in their own scope, completely invisible to window and to other modules.

Modules Never Touch window — Unless You Force It

// math.js (module)
let count = 0;          // NOT on window
window.count = 0;       // ✅ manually put on window — now it's global
  • Normal script: everything lands on window automatically
  • Module: nothing lands on window unless you manually write window.x = ...

How Modules Share Data — export and import

Since they don’t share window, modules share data explicitly and deliberately:

// math.js
let count = 0;               // private
export function add(a, b) {  // explicitly shared
  return a + b;
}
// app.js
import { add } from './math.js';
console.log(add(2, 3));  // 5 ✅
console.log(count);      // ReferenceError — not exported

Sharing is opt-in in modules. In normal scripts it’s impossible to opt out.

Limitations You Should Know

No module system is perfect. Here’s an honest breakdown.

Limitations of the Classic IIFE Pattern

No real dependency management. You manually control load order via script tags. In a 50-file project, one wrong order crashes everything silently.

Everything still needs a global entry point. Even with IIFE modules, the module itself lives on window:

const Counter = (function() { ... })(); // Counter is still window.Counter

You avoided polluting globals with internals — but the module itself is still global.

Privacy is convention, not enforcement. There’s no private keyword. If you accidentally expose something in the returned object, anyone can call it.

Hard to test. Internal functions locked inside closures are completely unreachable from test runners. You can only test the public API.

No lazy loading. The entire IIFE runs immediately at startup — even if the user never needs that code.

Limitations of ES Modules

Static imports only at the top level. You can’t conditionally import:

if (userIsLoggedIn) {
  import { Dashboard } from './dashboard.js'; // SyntaxError
}

Dynamic import adds complexity. The workaround exists but returns a Promise:

const module = await import('./dashboard.js'); // now you're in async land

Circular dependencies are silent and dangerous.

// a.js imports b.js
// b.js imports a.js  ← circular
// No error thrown — but one gets `undefined` at the wrong time

Live bindings can surprise you. Imported values aren’t snapshots — they’re live wires:

// counter.js
export let count = 0;
export function increment() { count++; }
// app.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1 — changed under your feet

Needs a bundler in most real projects. Older browsers don’t support ES Modules natively. Node.js requires exact file extensions without a bundler.

Key Takeaways

  • The module pattern exists because JavaScript has no built-in privacy — closures simulate it
  • Classic scripts are a shared battlefield; modules are isolated rooms
  • ES Modules are the module pattern baked into the language itself
  • Modules never block HTML parsing — defer is automatic
  • window still exists and is still shared — modules just don't write to it automatically
  • No module system is perfect; bundlers like Vite and Webpack paper over the remaining gaps in ES Modules

Understanding modules is understanding JavaScript’s evolution from a scripting toy to a production engineering language. Every framework, every build tool, every modern pattern you use is built on these foundations.


메타데이터
post_id
f2f63d0e2f32
slug
javascript-modules-everything-you-need-to-know-f2f63d0e2f32
url
https://medium.com/@anjujm132/javascript-modules-everything-you-need-to-know-f2f63d0e2f32
canonical_url
https://medium.com/@anjujm132/javascript-modules-everything-you-need-to-know-f2f63d0e2f32
author_url
https://medium.com/@anjujm132
status
ok
fetched_at
2026-07-14 00:04:08