JS engineConsolecounter.js runscounter.js:60main.js:21main.js:4undefinedmain.js:5TypeErrormain.js:61export let count = 0;2const step = 1;3export function add() {4 count = count + step;5}6console.log("counter.js runs");1import { count, add } from "./counter.js";2console.log(count);3add();4console.log(count);5console.log(typeof step);6count = 5;counter.js scopecountstepaddmain.js scopecountadd0110?5
Back: the JavaScript engine and the consoleLeft: the two files; right: each module's scope, with main's imports linked to counter's boxesTokens: the files, instructions, links, the values 0, 1 and 5, and the errorAt the back, the JavaScript engine and the console. On the left, the two files as cards. On the right, each module's own scope: counter.js holds count, step and add; main.js holds only its two imports, linked to counter's boxes. The engine loads both files and links the imports before any line runs, then runs counter.js first and main.js after it. main.js reads count through the link, so it sees every change add makes; it cannot see step, and it cannot assign to count. To run the two files with Node.js, put them in one folder with a package.json that says "type": "module" (or name them .mjs) and run node main.js. In a browser, load main.js with <script type="module" src="main.js"> from a web server: module scripts opened from a file:// address are blocked.

Files with their own scope

  1. A module is one JavaScript file with its own scope. Names made at its top level stay private to that file. export shares chosen names; import uses names another module shares. Here main.js imports from counter.js.

Load and link

Run, dependencies first

Live, private, read-only

What happens

// step 1/10 · files
module   one file, with its own top-level scope
export   shares a name with other modules
import   uses a name another module shares
counter.js   exports count and add, keeps step
main.js      imports count and add

Imports are hoisted: the engine reads every import line and links it before the module's first line runs, wherever the import statements are in the file. Module code is always in strict mode. The boxes are a simplification: each module has a module environment record, and main.js's imports are indirect bindings that point at counter.js's own bindings (CreateImportBinding in ECMA-262).