import & export
Learn how JavaScript modules share named exports, default exports, renamed imports, re-exports, live bindings, and strict module scope without leaking globals.
- 01Choose export shapesImport named, default, namespace, and renamed exports without guessing.
- 02Read module graphsUse barrel files and re-exports while understanding collisions and local bindings.
- 03Trust live bindingsExplain why imports update, why import assignments fail, and why modules run in strict scope.
What import and export mean
A JavaScript module is a file with a private top level and an explicit public surface. Exports are the values a file chooses to share. Imports are the names another file uses to read those shared values.
This is the module version of a team saying, "Here is the part of my work you may use; everything else stays inside my file." That matters because large apps are not one long script. They are hundreds of small files that need clear contracts.
export publishes bindings from one module, and import creates live read-only bindings to them in another module.
If scope, strict mode, or hoisting feel fuzzy, keep the Scope & lexical environments, Strict mode, and Hoisting & the temporal dead zone lessons nearby. Modules combine all three ideas.
Named vs default exports
INTERACTIVEA named export has a public name such as price or formatPrice. Importers ask for that exact export name inside braces. A default export is the special export whose name is default. When you import it, you choose the local name.
Imagine a tiny shop. The shelf has labels, so you ask for "tea" or "size" by name. The cafe also has one house special: you can order it and call it "lunch," "special," or "todayMeal" at your table.
- In real life: Labeled items on a shelf
- In JavaScript: Named exports such as
teaandsize - In real life: Asking for an item by label
- In JavaScript:
import { tea } from './shop.js' - In real life: The house special
- In JavaScript: The default export, imported with any local name
- In real life: A menu with several items
- In JavaScript: A module may have many named exports and one default export
Where the analogy stops: A real shop can guess what you meant. JavaScript modules cannot. If you ask for a named export that does not exist, module linking fails before the body runs.
// shelf.jsexport const price = 5;export default "house special";export { price as cost }; // order.jsimport { price, cost } from "shelf";import special from "shelf";import * as shelf from "shelf";console.log(price, cost, special, shelf.default);The namespace form, import * as shelf, gives you a module namespace object. It is read-only, non-extensible, and has a null prototype. You normally use it for clarity in demos or plugin-style APIs, not as a bag to mutate.
export const tea = "green";export default "house special"; import { tea } from "shop";console.log(tea);The sandboxed module iframe is loading the selected code.
Choose an exporter shape and an import form. The code runs in a sandboxed iframe with a real script type=module and an import map.
Try a mismatch in the lab: choose "default export only" and import { tea } from 'shop';. The iframe shows the real browser error. Static imports are checked before ordinary statements run.
Renaming with as
CONCRETERenaming does not change the exporter. It only gives the importing file a local label. You use it when two modules export the same name, when a name would clash with a local variable, or when the imported name needs more context.
Bring home two products both labeled "name," and you might put your own sticker on one: "userName." Renaming imports is the code version of that tidy habit.
- In real life: The product says
coston the package - In JavaScript: The exporter publishes
cost - In real life: You add a home sticker that says
price - In JavaScript:
import { cost as price } - In real life: The shop shelf label did not change
- In JavaScript: Other importers still ask for
cost
Where the analogy stops: A sticker can hide the old label. JavaScript keeps both ideas separate: the export name is still cost; your local binding is price.
// prices.jsconst cost = 12;export { cost as priceInDollars }; // cart.jsimport { priceInDollars as itemPrice } from "./prices.js";const price = "shown in the UI";console.log(itemPrice, price);You can rename on export, on import, or both. Read from right to left: priceInDollars as itemPrice means "ask for the exported name priceInDollars, then call it itemPrice in this file."
Re-exports and barrel files
INTERACTIVEA module can forward exports from other modules. A file that mostly re-exports from nearby files is often called a barrel, because importers can roll up to one place instead of visiting every file directly.
In a building, reception can send you to payroll, security, or the design team. Reception makes the path easier; it does not do every job itself.
- In real life: The reception desk
- In JavaScript:
index.jsor another barrel file - In real life: Forwarding you to the math office
- In JavaScript:
export * from './math.js' - In real life: Choosing the right specialist
- In JavaScript:
export { shout } from './strings.js' - In real life: A department directory
- In JavaScript:
export * as strings from './strings.js'
Where the analogy stops: The receptionist does not become the specialist. A forwarding export does not create a local variable you can use inside the barrel.
// math.jsexport const add = (a, b) => a + b;export const name = "math"; // strings.jsexport const shout = (text) => text.toUpperCase();export const name = "strings"; // index.jsexport * from "math";export { shout } from "strings";export * as strings from "strings"; // app.jsimport { add, shout, strings } from "index";console.log(add(2, 3), shout("ok"), strings.name);export * from forwards named exports, but not a module's default export. If two star exports contain the same name, that name becomes ambiguous and is not forwarded. Choose one explicitly with export { name } from './file.js'; when that happens.
export * from "math";export { shout } from "strings";export * as strings from "strings"; import { add, shout, strings } from "index";console.log(add(2, 3), shout("ok"), strings.name);The sandboxed module iframe is loading the selected code.
The barrel forwards public names from focused modules. Importers get one stable doorway without moving the implementation.
Live bindings
STEP THROUGHImports are live bindings. That phrase means the importing module has a read-only view of the exporter's binding, not a frozen copy of the current value.
A photo captures one moment. A window keeps showing what is happening now. Imported names behave like windows into exported bindings.
- In real life: Looking through a window into a room
- In JavaScript: Reading an imported binding
- In real life: Someone in the room moves the chair
- In JavaScript: The exporting module changes
count - In real life: You see the chair in its new spot
- In JavaScript: The importer reads the new value
- In real life: You cannot move the chair through the glass
- In JavaScript: The importer cannot assign to the import
Where the analogy stops: A real window shows objects. A module binding may point at any JavaScript value. If you copy the value into a local variable, that local variable is a snapshot.
Imports are live views into exported bindings. Step through the read, the mutation, and the failed assignment.
script
// counter.jsexport function increment() { count += 1;} // app.jsimport { count, increment } from "counter";console.log(count);increment();console.log(count);count = 10;The exporter used export let count, so it may change count. The importer's name count is still read-only. To change the value, call an exported function such as increment() or expose another exported setter on purpose.
Live bindings make cycles possible, but not magically safe. If a.js imports b from b.js, and b.js reads a before a.js initializes it, you can hit the same temporal dead zone rules from the Hoisting & TDZ lesson.
// a.jsimport { b } from "./b.js";export const a = "A"; // b.jsimport { a } from "./a.js";console.log(a); // may throw if a is still uninitializedexport const b = "B";Module scope and strict mode
INTERACTIVEModules are not classic scripts with nicer syntax. They run in strict mode automatically, keep top-level declarations scoped to the module, make top-level this be undefined, and evaluate a module URL once.
// visits.jsconsole.log(this === undefined);let visits = 0;visits += 1;try { leaked = "global"; }catch (error) { console.log(error.name); }export const visitsSeen = visits;export const globalHasVisits = "visits" in globalThis; // app.jsimport { visitsSeen, globalHasVisits } from "visits";import { visitsSeen as again } from "visits";console.log(visitsSeen, again, globalHasVisits);The sandboxed module iframe is loading the selected code.
Top-level this is undefined; assigning an undeclared name throws; visits is not a global property; and importing the same URL twice shares the one evaluated module.
This is why modules fixed many old script problems from the var and the global object lesson. A top-level let visits in a module is private to that module. It does not become globalThis.visits.
Where you will use this
SORTERUse named exports for a set of peer utilities, a default export for a file with one obvious main value, and re-exports for a small public doorway into a feature folder. Consistency matters more than fashion: pick a style your team can read at a glance.
| Form | How you import it | Best use |
|---|---|---|
| Named export | import { price } from './shop.js' | Many public values with meaningful names. |
| Default export | import anything from './shop.js' | One obvious main value from the file. |
| Namespace import | import * as shop from './shop.js' | Inspect a module as a read-only namespace object. |
| Re-export | export { price } from './shop.js' | Forward another module's public API. |
- A utility file has
parsePrice,formatPrice, andtaxRate. - A file represents one main
Buttoncomponent. features/cart/index.jsforwards the public cart API.- A validators file exposes
isEmailandisStrongPassword. - A theme file exports the one design theme object used by an app.
ui/index.jsgathersButton,Modal, andToastfrom separate files.
Choose the export style that communicates each module design most clearly.
Common misconceptions
| Mistake | Reality |
|---|---|
| Default means unnamed | The export name is literally default; the importer chooses a local name. |
| Imports are copies | Imports are live read-only bindings to the exporter's binding. |
| A barrel owns the values | A re-export forwards bindings; it does not make local variables. |
| Modules are like old scripts | Modules are strict, scoped, deferred, and evaluated once per URL. |
- "Default exports are unnamed." The export name is
default; the local import name is flexible. - "Import order can go anywhere because imports are hoisted." Import declarations are hoisted, but they must still be at the top level of a module.
- "Namespace objects are normal objects." Module namespace objects are read-only views with a null prototype.
- "A barrel is always better." Barrels help public APIs, but too many barrels can hide where code really lives.
- "Modules only matter in browsers." Browsers and many other JavaScript runtimes use ES modules; hosts decide resolution details.
Practice exercises
4 EXERCISESPredict the exact output. This object stands in for a module namespace.
const module = { price: 5, default: "house special" };
const { price } = module;
const special = module.default;
console.log(price + " / " + special);The named value is 5 and the default value is house special, so the log joins them as 5 / house special.
The cart file wants a local variable named price, but the exporter only publishes cost. Type the fixed import clause.
// prices.js
export const cost = 12;
// cart.js
import { price } from "./prices.js"; // fix this lineimport { cost as price } from "./prices.js";Ask for the real exported name cost, then rename it locally to price.
Write the two printed lines separated by a comma.
let count = 0;
function increment() { count += 1; }
console.log(count);
increment();
console.log(count);The logs are 0 and then 1. A live import would behave like reading count again after the exporter changed it.
In a module, what error name appears if code tries leaked = "global" without declaring leaked?
Assigning to an undeclared name in a module throws a ReferenceError, because modules are strict and do not create accidental globals.
Check your understanding
7 QUESTIONSQuestion 1 of 7Which statement best describes a default export?
Choose an answer to see the explanation.
Question 2 of 7What does the named/default import example print?
Read the code, then predictconst module = { price: 5, default: "house special" }; const { price } = module; const special = module.default; console.log(price + " / " + special);Choose an answer to see the explanation.
Question 3 of 7Why write
import { cost as price }?Choose an answer to see the explanation.
Question 4 of 7What does the live binding model print?
Read the code, then predictlet count = 0; function increment() { count += 1; } console.log(count); increment(); console.log(count);Choose an answer to see the explanation.
Question 5 of 7What happens if an importing module assigns to an imported binding?
Choose an answer to see the explanation.
Question 6 of 7What does
export * fromdo when two source modules export the same name?Choose an answer to see the explanation.
Question 7 of 7Which module-scope statement is true?
Choose an answer to see the explanation.
Key takeaways
- Named exports are imported by their exported names; a default export is the export named
default. asrenames locally without changing the exporter.- Re-exports forward bindings; star exports omit ambiguous duplicate names.
- Imports are live read-only bindings, not assignable local variables.
- Modules are strict, scoped, and evaluated once per module URL.
Final definition.import and export connect module files through explicit, live bindings while keeping each module's private scope intact.
Up next: Dynamic import & top-level await.