JavaScript Interview Prep
Scope & Closures

Module Scope

Each File Gets Its Own Universe

LinkedIn Hook

For most of JavaScript's life, every <script> tag you loaded dumped its variables into one giant shared bucket — the global scope. Two libraries both using var user would silently clobber each other. The only escape was wrapping everything in an IIFE and hoping for the best.

Then ES6 modules arrived and quietly solved one of the ugliest problems in the language: every file is its own scope, by default, no ceremony required.

export opens the door to the outside world. Anything you don't export stays private. Strict mode is always on. this at the top level is undefined, not window. And import/export give you real, explicit dependency management instead of "make sure this script tag loads first."

In this lesson you'll learn the pre-ES6 IIFE module pattern that shaped jQuery and Underscore, how ES modules replaced it, and the precise difference between named and default exports.

Read the full lesson -> [link]

#JavaScript #ESModules #ModuleScope #InterviewPrep #Frontend #CodingInterview #WebDevelopment


Module Scope thumbnail


What You'll Learn

  • Why global-scope pollution was the #1 pre-ES6 problem and how the IIFE module pattern fixed it
  • How ES modules give every file its own private scope automatically
  • The practical difference between named exports and default exports
  • Why ES modules always run in strict mode and how that changes this at the top level

The Apartment-Building Model

Before ES modules, JavaScript had no built-in module system. Everything lived in the global scope, leading to name collisions, dependency chaos, and spaghetti code. Developers invented patterns to work around this. Today, ES modules solve this natively.

Think of module scope like apartments in a building. Each apartment (module) has its own private space. You can choose to leave something at the front door (export) for others to pick up, but no one can walk in and take what they want without your permission.

The Problem — Global Scope Pollution

// file1.js
var userName = "Rakibul";
var version = "1.0";

// file2.js
var userName = "Hassan"; // OVERWRITES file1's userName!
var version = "2.0";     // OVERWRITES file1's version!

// Both scripts share the global scope
// No encapsulation, no privacy, constant conflicts

Solution 1: IIFE Module Pattern (Pre-ES6)

The Immediately Invoked Function Expression creates a function scope to encapsulate everything:

// The Module Pattern using IIFE
const UserModule = (function () {
  // Private variables -- not accessible outside
  let users = [];
  let idCounter = 0;

  // Private function
  function generateId() {
    return ++idCounter;
  }

  // Public API -- only these are exposed
  return {
    addUser(name) {
      const user = { id: generateId(), name };
      users.push(user);
      return user;
    },
    getUsers() {
      return [...users]; // return a copy, not the original
    },
    getUserCount() {
      return users.length;
    }
  };
})();

UserModule.addUser("Rakibul");
UserModule.addUser("Hassan");
console.log(UserModule.getUsers());     // [{id: 1, name: "Rakibul"}, {id: 2, name: "Hassan"}]
console.log(UserModule.getUserCount()); // 2

// Private members are truly hidden
console.log(UserModule.users);      // undefined
console.log(UserModule.idCounter);  // undefined
console.log(UserModule.generateId); // undefined

Solution 2: ES Modules (Modern)

ES modules have their own scope by default — no IIFE needed:

// math.js -- each file is its own module scope
const PI = 3.14159;  // private to this module unless exported
let calculations = 0; // private counter

export function circleArea(radius) {
  calculations++;
  return PI * radius * radius;
}

export function circlePerimeter(radius) {
  calculations++;
  return 2 * PI * radius;
}

export function getCalculationCount() {
  return calculations;
}

// PI and calculations are NOT accessible from outside
// app.js
import { circleArea, circlePerimeter, getCalculationCount } from "./math.js";

console.log(circleArea(5));           // 78.53975
console.log(circlePerimeter(5));      // 31.4159
console.log(getCalculationCount());   // 2

// console.log(PI);           // ReferenceError -- not exported
// console.log(calculations); // ReferenceError -- not exported

Named vs Default Exports

// utils.js
// Named exports -- can have many per file
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export const VERSION = "1.0";

// Default export -- only ONE per file
export default function multiply(a, b) { return a * b; }
// importing.js
// Named imports use curly braces -- must match export names
import { add, subtract, VERSION } from "./utils.js";

// Default import -- no braces, can use any name
import multiply from "./utils.js";
// or: import myMultiplyFn from "./utils.js";

console.log(add(2, 3));      // 5
console.log(multiply(2, 3)); // 6
console.log(VERSION);        // "1.0"

Revealing Module Pattern (Variation)

const Calculator = (function () {
  let history = [];

  function add(a, b) {
    const result = a + b;
    history.push(`${a} + ${b} = ${result}`);
    return result;
  }

  function subtract(a, b) {
    const result = a - b;
    history.push(`${a} - ${b} = ${result}`);
    return result;
  }

  function getHistory() {
    return [...history];
  }

  function clearHistory() {
    history = [];
  }

  // "Reveal" only what you want to be public
  return { add, subtract, getHistory, clearHistory };
})();

console.log(Calculator.add(10, 5));      // 15
console.log(Calculator.subtract(10, 3)); // 7
console.log(Calculator.getHistory());    // ["10 + 5 = 15", "10 - 3 = 7"]

Module Scope Comparison

FeatureGlobal ScriptIIFE ModuleES Module
Own scopeNoYes (function scope)Yes (module scope)
Private variablesNoYes (via closure)Yes (unexported = private)
Explicit exportsNo (everything global)Return objectexport keyword
Dependency managementScript order in HTMLManualimport/export
Runs in strict modeNo (unless "use strict")No (unless declared)Always (automatically)

Module Scope visual 1


Common Mistakes

  • Assuming top-level vars inside an ES module go on the window object. They don't — module scope is its own universe, so var x = 1 at the top of a module is module-private, not global.
  • Mixing named and default imports incorrectly: import multiply from "./utils.js" grabs the default export, while import { multiply } from "./utils.js" grabs a named export. Using the wrong form silently gets you undefined or a parse error.
  • Expecting an ES module to run the same as a classic <script>. Modules are always in strict mode, have their own this === undefined at the top level, and execute deferred by default — so DOM-ready timing and global assumptions can break.

Interview Questions

Q: What is module scope and why is it important?

Module scope is an isolated scope per file, giving each module its own private variables and a controlled public API via export/import. It prevents global-scope pollution, eliminates name collisions between files, and makes dependencies explicit instead of relying on script order.

Q: How does the IIFE module pattern achieve encapsulation?

The IIFE creates a function scope that runs immediately. Variables declared inside are private (trapped in the function scope). The IIFE returns an object containing only the functions/values you want to expose. This leverages closures — the public methods close over the private variables, giving controlled access without exposing internals.

Q: What's the difference between named and default exports in ES modules?

A file can have many named exports, each identified by name, and importers must use the matching name (or an alias): import { add } from "./utils.js". A file can have at most one default export, and the importer can give it any name without braces: import whatever from "./utils.js". Named is preferred for libraries with multiple entities; default is common for the single "main thing" of a file.

Q: Do ES modules run in strict mode?

Yes, always. ES modules automatically run in strict mode. You don't need to write "use strict" — it's enabled by default. This means undeclared variables throw errors, this in module scope is undefined (not window), and other strict mode rules apply.

Q: Can two ES modules have variables with the same name? Why?

Yes. Each ES module has its own scope. Variables in one module don't conflict with variables in another module, even if they have the same name. This is because each module creates a separate scope — unlike global scripts where everything shares the global scope.


Quick Reference — Cheat Sheet

MODULE SCOPE -- QUICK MAP

Why it exists:
  Pre-ES6 scripts all shared the global scope.
  Collisions, no privacy, script-order hell.

IIFE pattern (pre-ES6):
  (function() {
    let private = ...;
    return { publicAPI: ... };
  })();
  -> function scope + closure = encapsulation

ES modules (modern):
  - Each file = its own scope
  - `export` declares what's public
  - `import` pulls specific names
  - Always strict mode
  - Top-level `this` is undefined
  - Executes deferred by default

Export flavors:
  named   -> many per file, match names in import {}
  default -> one per file, any name, no braces

Previous: Block Scope vs Function Scope -> Where Your Actually Matters Next: Event Loop -> How JavaScript Runs Everything on One Thread


This is Lesson 2.5 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.

On this page