Scope & lexical environments
Understand global, function, and block scope, plus how JavaScript walks the scope chain to find each variable name.
- 01Predict name lookupTrace how JavaScript searches from inner code outward.
- 02Separate lexical from dynamicExplain why callers do not change a function's outer variables.
- 03Use the smallest scopeChoose global, module, function, or block scope intentionally.
Scope decides what names you can use
Scope is the part of a program where a name can be found. If you write console.log(total), JavaScript must answer a simple question before it can print anything: which binding named total is visible from this line?
The curriculum summary for this lesson is our map: global, function, and block scope, and how lookups climb the scope chain. You already saw block-scope basics in Variables and a first look at outer variables in Declaring & calling functions. Execution contexts gave names a home; Hoisting & the temporal dead zone explains why some names exist before a line can safely read them.
From inside a bedroom, you can look through the doorway into the hall, then the lobby. People in the lobby cannot see into your bedroom. Scope has the same one-way feel: inner code can use outer names, but outer code cannot reach inner locals.
- In real life: Bedroom
- In JavaScript: An inner function or block
- In real life: Hall outside the bedroom
- In JavaScript: The outer function
- In real life: Lobby
- In JavaScript: The global or module scope
- In real life: Name tags in each room
- In JavaScript: Bindings in an environment record
Where the analogy stops: Rooms are physical places. JavaScript scopes are rules attached to code and function calls.
Lexical scope means a function's available outer variables are decided by where that function is written in the source code.
Lexical scope: where code is written
Lexical comes from words and text. In JavaScript, ordinary variable lookup is tied to the written shape of your code. If a function is written inside another function, it can read the outer function's variables because that is where the function was built.
This is why the Closures lesson opens with nested functions. This lesson is the foundation for that one: before a function can remember outer variables, you need to know which outer variables it can see.
Scope decides where to search for a name. It does not say whether the value is mutable, whether the binding is in the temporal dead zone, or whether a closure will keep it alive. Those are related ideas, but not the definition of scope.
Lexical vs dynamic scope
INTERACTIVEA caller does not get to lend its local variables to a function just by calling it. In JavaScript, the function reads the environment from where it was defined. Dynamic scope, used by some languages and tools, would instead ask who called the function.
const label = "global";
function makeReader() {
const label = "defined in makeReader";
return function readLabel() {
return label;
};
}
const readLabel = makeReader();
function callFromAnotherPlace() {
const label = "caller local";
return readLabel();
}
console.log(callFromAnotherPlace());JavaScript prints: defined in makeReader
Hypothetical dynamic scope: caller local
The caller has a variable with the same name, but it is not on readLabel's lexical chain.
defined in makeReaderreadLabel was called from a function with label = "caller local", but it reads the label from where it was defined. A dynamic-scope language would choose the caller local value instead.
The caller has its own label, but readLabel was created inside makeReader, so it reads that environment. The dynamic result in the playground is deliberately labeled hypothetical.
this?JavaScript's this is the dynamic-ish part: it is usually decided by how a function is called. That is a different rule from lexical variable lookup and belongs in The this keyword lesson.
Block scope
PLAYGROUNDA block is code between braces: an if body, a loop body, or even a plain { ... } block. let and const live in the nearest block. The older var ignores ordinary blocks and belongs to the surrounding function or script.
let, const, and varif (true) {
let blockLet = "inside if";
const blockConst = "inside if";
var functionVar = "var ignores this block";
}
console.log(functionVar);
// console.log(blockLet); // ReferenceError outside the blockvar ignores this blockif block recordinside ifinside ifconsole.log(functionVar) prints var ignores this block.
blockLet and blockConst are not available outside the block.
var functionVar belongs to the surrounding function or script, so it is still readable after the if block.
| Scope kind | What creates it | What it is good for |
|---|---|---|
| Global | A script's top level | Names that truly must be shared by the whole script |
| Module | An ES module file | File-level names that should not become globals |
| Function | Every function call | Parameters and variables used by one call |
| Block | { ... } around let and const | Temporary values for an if, for, or plain block |
Prefer the smallest useful scope. A temporary value used only inside an if or for block should usually be declared there, not at the top of a whole function.
The scope chain
STEP THROUGHWhen JavaScript reads an identifier, it starts in the current environment. If the name is not there, it walks outward one environment at a time. That outward path is the scope chain.
Predict which room line 7 will use. Then step through the lookup from inner to outer scopes.
script
function outer() { const room = "outer hall"; function inner() { // no local room here return room; } return inner();} console.log(outer());Turn on the inner declaration and replay. One line changes, but the lookup result changes because the closest room now has its own name tag. That is the core rule: nearest binding wins.
Shadowing
SORTERShadowing happens when an inner scope declares a name already used by an outer scope. It is legal and often useful, but it can hide the value you meant to read.
If your bedroom and the hallway both have a lamp called light, you use the one beside you. The hallway lamp is still there; it is just hidden by the nearer name.
- In real life: A lamp on your desk
- In JavaScript: An inner binding
- In real life: A lamp in the hallway
- In JavaScript: An outer binding
- In real life: Using the closest lamp first
- In JavaScript: Nearest scope wins
Where the analogy stops: A real lamp does not prevent the hallway lamp from existing. Shadowing also hides only while you are inside the inner scope.
let x = "global"; console.log(x);let x = "global"; function run() { let x = "outer"; console.log(x); }let x = "global"; { let x = "block"; console.log(x); }let x = "global"; function run() { console.log(x); }let x = "global"; { let x = "block"; } console.log(x);function outer() { let x = "outer"; function inner() { console.log(x); } }
For each snippet, choose the binding that console.log(x) reads.
JavaScript also has early errors for some confusing redeclarations. This one is a syntax error because var tries to create a function-scoped binding where let already declared the same name.
let x = "outer";
{
var x = "same function scope";
}Lexical environment records
The specification models scope with lexical environments. You can picture each environment as a record of bindings plus a reference to an outer environment. Function calls create function records; blocks create block records for let and const; modules create module records.
function makeGreeter(name) {
const punctuation = "!";
return function greet() {
return "Hello, " + name + punctuation;
};
}
const greetAda = makeGreeter("Ada");
console.log(greetAda());In the model, the returned greet function keeps a reference to the environment record that has name and punctuation. That is exactly the bridge to the next lesson, Closures.
Real engines optimize aggressively. Environment records are the language's precise model for behavior, not a promise that every engine stores boxes with these labels in memory.
Where you use this
Scope rules guide everyday decisions:
- Put loop-only values inside the loop so later code cannot misuse them.
- Keep helper functions near the data they read, especially in modules.
- Avoid accidental globals by declaring every name with
let,const, orvar. - Rename shadowed values when the duplicate name makes code harder to read.
Declare a variable in the smallest scope that contains every line that needs it. If you later need it elsewhere, move it outward on purpose.
Common misconceptions
“The caller decides outer variables.”
No. Ordinary names are lexical. The call site can pass arguments, but it cannot rewrite the callee's scope chain.
“Blocks hide var.”
var ignores ordinary blocks. Use let or const for block-local names.
“Shadowing changes the outer variable.”
Shadowing creates another binding with the same name. The outer binding still exists.
“Environment records are engine screenshots.”
They are the spec's behavior model. Engines may represent optimized code differently.
“An undeclared assignment is fine if it works.”
In sloppy scripts it can create an accidental global. In strict mode it throws a ReferenceError.
Practice exercises
5 EXERCISESRead the three console.log calls and predict the output order.
const label = "global";
function outer() {
const label = "outer";
{
const label = "block";
console.log(label);
}
console.log(label);
}
outer();
console.log(label);The block prints block, then outer prints its function-scoped label, then the script prints the global label.
The program prints 10 in sloppy mode by creating an accidental global. Fix it so score is local.
function saveScore() {
score = 10;
}
saveScore();
console.log(score);function saveScore() {
const score = 10;
console.log(score);
}
saveScore();Declare score with const or let inside the function. The value stays local instead of leaking onto the global object in sloppy mode.
This greeting ignores its argument because a nearer name gets used. Fix it.
const user = { name: "Ada" };
function greet(userName) {
const name = "friend";
return "Hi, " + name;
}
console.log(greet(user.name));const user = { name: "Ada" };
function greet(userName) {
return "Hi, " + userName;
}
console.log(greet(user.name));The local name = "friend" shadows the value passed in. Removing it and using userName makes the function greet Ada.
Refactor the starter so label is declared only where it is needed.
function format(items) {
let label;
const prefix = "Item: ";
for (const item of items) {
label = prefix + item;
console.log(label);
}
}function format(items) {
const prefix = "Item: ";
for (const item of items) {
const label = prefix + item;
console.log(label);
}
}
format(["scope"]);label is used only for one loop iteration, so declaring it inside the loop keeps it out of the rest of the function.
In one sentence, explain why an inner function can read an outer variable after the outer function has returned. Which next lesson goes deeper?
Closures explains why an inner function can keep access to an outer environment record after the outer function returns.
Quiz
7 QUESTIONSQuestion 1 of 7What does lexical scope mean?
Choose an answer to see the explanation.
Question 2 of 7What does the nested function print?
Read the code, then predictconst x = "global"; function outer() { const x = "outer"; function inner() { console.log(x); } inner(); } outer();Choose an answer to see the explanation.
Question 3 of 7Which declaration is block-scoped?
Choose an answer to see the explanation.
Question 4 of 7What does the block-scope snippet print?
Read the code, then predictlet topic = "global"; { let topic = "block"; } console.log(topic);Choose an answer to see the explanation.
Question 5 of 7What is shadowing?
Choose an answer to see the explanation.
Question 6 of 7What would dynamic scope do in the lesson's contrast?
Choose an answer to see the explanation.
Question 7 of 7What is a lexical environment record in this lesson's model?
Choose an answer to see the explanation.
Key takeaways
- Lexical scope is decided by where code is written.
- Lookup starts locally and walks outward along the scope chain.
letandconstare block-scoped;varis not.- Shadowing means a nearer binding hides an outer binding with the same name.
- Environment records are the spec model: bindings plus an outer reference.
Final definition: Scope is the set of lexical environments JavaScript searches to resolve a name.
Up next: Closures.