Skip to content
Lesson 15 of 15

Step 1 of 7 · Reading · ~4 min

Learn

Modern Syntax

ES Modules: import and export

Real programs are split across files. A module is a file that decides what it shares and what stays private. By the end of this lesson you can read and write import and export statements, explain when a module's code runs, and describe what a module system does behind the scenes.

A module is private by default

Variables and functions declared in a module are visible only inside it. Anything the module wants to share must be exported; anything another file wants to use must be imported.

Named exports

javascript
javascript

The names in the braces must match the exported names.

Default export

A module may mark one value as its main export. The importer chooses the name:

javascript
javascript

Namespace import and re-exports

javascript

Dynamic import

import("./big.js") is an expression that loads a module on demand and returns a Promise of its namespace. Use it to load code only when needed. A static import must sit at the top level of the file.

When does module code run?

Four rules explain nearly every module question:

  1. Evaluated once. The first import of a file runs its top-level code. Every later import, from any file, reuses the result. A module with a counter in it is therefore shared state, a singleton.
  2. Imports are hoisted. All static imports are resolved before any other code in the file runs, wherever you wrote them.
  3. Imports are live, read-only views. If the exporting module changes its exported variable, importers see the new value. An importer cannot assign to it.
  4. Cycles are allowed. If a imports b and b imports a, the second module to run sees the first one's exports as they are so far, possibly still empty. Avoid depending on that.

Modules also run in strict mode, and top-level variables are module-scoped, not global.

The older system: CommonJS

Node began with a different system. A file sets module.exports = {...} and others call require("./file"). Conceptually each file is a function that Node calls with a fresh module object and a require function; whatever the function leaves in module.exports is what require returns. require remembers every file it has loaded, so a second require of the same file returns the same object without running the file again, which is rule 1 above.

ES modules are the standard, and they work in browsers (<script type="module">) and in Node (files ending .mjs, or "type": "module" in package.json). Node treats .js files as CommonJS unless told otherwise, and mixing the two systems is a common source of cryptic errors. Relative paths need the ./ and, in Node, the file extension.

About this exercise

The practice runner executes your file as one CommonJS script, so real import lines cannot run here. Instead you will build the machinery under them: a tiny require that evaluates module functions once and shares their exports.

Common mistakes

  • Using braces for a default import, or none for a named one.
  • Forgetting ./ or the .js extension in a Node import path.
  • Trying to assign to an imported name.
  • Expecting a module to run again on every import. It runs once.
  • Destructuring a CommonJS export, const { count } = require(...), copies the value at that moment, so later changes inside the module are not seen. ES module imports are live.

Your exercise

Write the loader. The starter defines four module functions (two of them require each other) and prints, for each requested name, the keys it exports. You supply makeRequire.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…