Scope & lexical environments
Understand global, function, and block scope, plus how JavaScript walks the scope chain to find each variable name.
- 01Predict which name is usedFollow how JavaScript looks for a name: first inside, then step by step outward.
- 02Tell lexical from dynamic scopeExplain why calling a function doesn't give it the caller's variables.
- 03Use the smallest scopeChoose global, module, function, or block scope on purpose for each variable.
Scope decides what names you can use
A name is the word you use for a variable, a constant, a parameter, or a function, like total. Scope is the part of a program where a name can be found.
Say you write console.log(total). Before JavaScript can print anything, it must answer one question. Which total can this line see? There might be several variables called total in your program, or none at all. Scope gives the answer.
This lesson covers:
- Global scope: names at the top level of a script.
- Function scope: names made inside a function.
- Block scope: names made inside a pair of braces,
{ }. - The scope chain: how JavaScript searches outward, one scope at a time, to find a name.
You met some of this before. The Variables lesson showed that let stays inside its braces. The functions lesson showed a function reading a variable from outside it. Here you'll learn the full rule.
You are in a bedroom. You can look through the door into the hall. From the hall, you can see the lobby. But a person in the lobby can't see into your bedroom.
Scope works one way, too. Code inside can use names from outside. Code outside can't use names from inside.
- 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 scope or the module scope
- In real life: Things in each room
- In JavaScript: The names each scope holds
Lexical scope means this: the outer variables a function can use depend on where the function is written in your code. They don't depend on where it is called from.
Lexical scope: where code is written
Lexical means "about the written text". In JavaScript, the place where you write a function decides which outside variables it can use. If you write a function inside another function, the inner one can use the outer one's variables.
The Closures lesson builds on this. A function can only remember outer variables that it can see. So first you need to know which outer variables a function can see.
Scope tells JavaScript where to search for a name. That's all. Other lessons cover other questions:
- Can the value change? (Variables)
- Can you read the name yet? (Hoisting & the temporal dead zone)
- Does the value stay around after a function ends? (Closures)
Each function call has its own function scope. It holds the function's parameters and the variables made inside it.
function greet(user) {
const message = "Hi, " + user;
return message;
}
console.log(greet("Asha"));Line 1 gives greet a parameter called user. Line 2 makes a constant called message. Both exist only while greet runs. Line 6 gets back the finished string, Hi, Asha. It can't use user or message.
An ES module is a JavaScript file that uses import and export. Each module has its own module scope. Names at the top of a module belong to that file only. Other files can use them only if the module exports them.
// profile.mjs
const user = "Asha";
export function getUser() {
return user;
}
// app.mjs
import { getUser } from "./profile.mjs";
console.log(getUser());This example is two files, so the page can't run it in one editor. In profile.mjs, line 2 makes user, and line 3 exports getUser. In app.mjs, line 9 calls getUser(), which prints Asha. The app never sees user itself. It is not a global name for the whole page.
Global scope is the outermost scope. A name written at the top level of a normal script, outside every function and block, is global. Code anywhere in that script can use it.
const siteName = "CompleteFrontend";
function showSite() {
console.log(siteName);
}
showSite();Line 1 makes siteName at the top level. Line 4 is inside showSite, and it can still read siteName. Line 7 calls the function, so it prints CompleteFrontend.
A global name can be read and changed by any code in the script. That makes mistakes easy, so keep globals few and make each one on purpose.
JavaScript also has a global object: one shared object that every script can reach. Its name is globalThis. It works in browsers and in Node.js.
const user = "Asha";
globalThis.cartCount = 3;
console.log(user);
console.log(globalThis.cartCount);Line 1 makes a normal variable, user. Line 2 puts a property called cartCount on the global object. Line 4 prints Asha, and line 5 prints 3.
Sharing a value this way can help two scripts work together. But any other code can change it too, so a module that exports the value is usually safer.
A common mistake is giving a value to a name you never declared. To declare a name means to make it with let, const, or var. The line "use strict" turns on strict mode, a set of safer rules. In strict mode, giving a value to an undeclared name throws a ReferenceError.
function saveScore() {
"use strict";
const score = 10;
console.log(score);
}
saveScore();Line 2 turns on strict mode for saveScore. Line 3 declares score with const, so it belongs to saveScore only. Line 7 calls the function, and line 4 prints 10. Without the const, strict mode would throw a ReferenceError instead.
Lexical vs dynamic scope
INTERACTIVECalling a function doesn't give it your variables. In JavaScript, a function uses the variables from the place where it was written. Some other languages work differently. They use dynamic scope: the function uses the variables of whoever called it. JavaScript does not do that.
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
Dynamic scope would print: caller local
The caller has its own label, but readLabel was not written inside the caller, so it never looks there.
defined in makeReaderreadLabel is called from callFromAnotherPlace, which has label = "caller local". But readLabel uses the label from where it was written, inside makeReader. A language with dynamic scope would print caller local instead.
Line 13 makes a label inside the caller, callFromAnotherPlace. But readLabel was written inside makeReader, on lines 5 to 7. So it reads the label from line 4. The playground also shows what dynamic scope would print. That line is only for comparison. JavaScript never prints it.
label. readLabel was written inside makeReader, so it reads the box in makeReader. The caller's box is never used, even though the caller is the one that runs readLabel().this?The word this follows a different rule. Its value usually depends on how a function is called, not where it is written. So this is not part of scope. The this keyword lesson covers it.
Block scope
PLAYGROUNDA block is code between braces, { }. It can be the body of an if, the body of a loop, or just a pair of braces on their own. Each block has its own block scope.
letandconstbelong to the nearest block around them.- The older
varskips normal blocks. It belongs to the function around it, or to the whole script.
let, const, and var inside an if blockif (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 blockinside ifinside ifLine 7, console.log(functionVar), prints var ignores this block.
After the block, blockLet and blockConst are gone. Reading them throws a ReferenceError.
var functionVar belongs to the whole script, not to the if block. So line 7 can still read it after the block.
blockLet and blockConst live only inside the if block. functionVar is written inside the block too, but var puts it outside, so line 7 can still read it.There are four kinds of scope. Read each row as: "this kind of scope is made by this, and it is good for this."
| Kind of scope | What makes it? | What is it good for? |
|---|---|---|
| Global | The top level of a script makes it. | Use it only for names the whole script truly must share. |
| Module | Each ES module file makes one. | Use it for names that belong to one file and shouldn't become global. |
| Function | Each function call makes a new one. | Use it for the parameters and variables of that one call. |
| Block | Braces { } make one for the let and const inside them. | Use it for short-lived values in an if, a for loop, or a plain block. |
Give each variable the smallest scope it needs. If a value is only used inside an if or a for loop, declare it there. Don't declare it at the top of the whole function.
The scope chain
STEP THROUGHWhen JavaScript reads a name, it first looks in the current scope. If the name isn't there, it looks in the scope just outside. It keeps stepping outward, one scope at a time, until it finds the name. This path from the inside out is called the scope chain.
If the search reaches the global scope and still finds nothing, reading the name throws a ReferenceError.
room. JavaScript looks in inner first and finds nothing. It steps out to outer, finds "outer hall", and stops. The global room is never reached.Guess which room line 7 will use. Then step through and watch the search go from inner outward.
script
function outer() { const room = "outer hall"; function inner() { // no local room here return room; } return inner();} console.log(outer());Now pick the setting that adds const room = "inner desk" on line 6, and step through again. Only one line changed. But now inner has its own room, so the search stops right there and returns "inner desk". This is the main rule: the nearest name wins.
Shadowing
SORTERShadowing happens when an inner scope makes a name that an outer scope already has. Inside the inner scope, the inner name hides the outer one. This is allowed, and sometimes useful. But it can also hide the value you meant to read.
Your bedroom and the hallway both have a lamp called "light". When you say "turn on the light", you use the one beside you. The hallway lamp is still there. You just don't use it from your desk.
- In real life: A lamp on your desk
- In JavaScript: A name in the inner scope
- In real life: A lamp in the hallway
- In JavaScript: The same name in an outer scope
- In real life: Using the closest lamp first
- In JavaScript: The nearest name wins
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 piece of code, choose which x the line console.log(x) reads.
Some same-name code is not allowed at all. JavaScript stops it before running anything, with a SyntaxError. Here, var skips the block, so it tries to make x in the same scope where let already made x.
let x = "outer";
{
var x = "same function scope";
}Here is shadowing that works:
let color = "blue";
{
let color = "green";
console.log(color);
}
console.log(color);Line 1 makes color with the value blue. Line 3 makes a second color inside the block, with the value green. Line 4 is inside the block, so it prints green. Line 6 is after the closing brace. The inner color is gone, so line 6 prints blue. Shadowing hides the outer name for a while. It never changes it.
color. Inside the block, the nearer box ("green") is used. After the closing brace, that box is gone, and line 6 uses the outer box ("blue").Lexical environment records
The specification is the official rule book for JavaScript. It describes each scope as a lexical environment record. A record has two parts:
- A list of the names in that scope, with their values.
- A link to the scope just outside it. The spec calls this the outer reference.
Each function call gets a record. Each block gets a record for its let and const names. Each module gets a record. The scope chain is just these outer links, followed one after another.
function makeGreeter(name) {
const punctuation = "!";
return function greet() {
return "Hello, " + name + punctuation;
};
}
const greetAda = makeGreeter("Ada");
console.log(greetAda());Line 8 calls makeGreeter("Ada"). That call gets a record holding name and punctuation. Line 3 returns the greet function, which keeps a link to that record. So when line 9 calls it, line 4 can still read both names, and it prints Hello, Ada!. The next lesson, Closures, explains this fully.
Environment records describe how JavaScript behaves. Real engines, like the one in your browser, may store things in faster ways. They still give the same results.
Where you use this
Scope rules help you make everyday choices:
- Declare a value that only a loop uses inside that loop. Then later code can't use it by mistake.
- Keep helper functions in the same module as the data they read.
- Declare every name with
let,const, orvar, so you never make a global by accident. - If an inner name hides an outer one and makes the code confusing, rename one of them.
Declare each variable in the smallest scope that holds every line that uses it. If you later need it somewhere else, move it outward on purpose.
const cart = { items: [] };
function addToCart(item) {
const itemCount = cart.items.push(item);
return itemCount;
}
console.log(addToCart("tea"));
console.log(cart.items);Line 1 makes cart outside the function, so every call to addToCart adds to the same list. Line 4 makes itemCount inside the function. The array method push adds an item and gives back the new length. Each call gets a fresh itemCount, so it never becomes shared data. Line 8 prints 1, and line 9 prints ["tea"].
Common misconceptions
“The caller decides which outer variables a function uses.”
No. A function uses the outer variables from where it is written. The caller can pass arguments, but it can't give the function its own variables.
“A block keeps var inside it.”
No. var skips normal blocks. Use let or const for a name that should stay inside a block.
“Shadowing changes the outer variable.”
No. Shadowing makes a second variable with the same name. The outer variable still exists, with its old value.
“Environment records show how memory really looks.”
No. They are the spec's way to describe behavior. Engines may store things differently and still give the same results.
“Using a name without declaring it is fine if it works.”
No. Outside strict mode, it quietly makes a global by accident. In strict mode, it throws a ReferenceError.
Practice exercises
5 EXERCISESThere are three console.log lines. Predict what each one prints, in order.
const label = "global";
function outer() {
const label = "outer";
{
const label = "block";
console.log(label);
}
console.log(label);
}
outer();
console.log(label);Line 6 is inside the block, so it prints block. Line 8 is inside outer, so it prints outer. Line 11 is at the top level, so it prints global.
This program prints 10, but only because line 2 makes a global score by accident. Fix it so score belongs to saveScore only.
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. Then it belongs to the function only, and it is not added to the global object.
This greeting ignores the name you pass in, because it uses a nearer name instead. 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));Line 3 makes name = "friend", and line 4 uses it instead of the value passed in. Remove line 3 and return "Hi, " + userName. Now the function greets Ada.
Change the code 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"]);Each loop turn uses label once. Declaring it inside the loop keeps it away from the rest of the function.
In one sentence, explain why an inner function can still read an outer variable after the outer function has finished. Then name the next lesson, which explains it in depth.
The inner function keeps a link to its outer record, so it can still read those names. The Closures lesson explains this.
Quiz
7 QUESTIONSQuestion 1 of 7What does lexical scope mean?
Choose an answer to see the explanation.
Question 2 of 7What does the inner 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 keywords make a variable that stays inside its block?
Choose an answer to see the explanation.
Question 4 of 7What does this code 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 7In the lesson's
readLabelexample, what would dynamic scope do?Choose an answer to see the explanation.
Question 7 of 7What is a lexical environment record?
Choose an answer to see the explanation.
Key takeaways
- Lexical scope means a function uses the outer variables from where it is written.
- To find a name, JavaScript looks in the current scope first, then steps outward along the scope chain.
letandconststay inside their block.varskips normal blocks.- Shadowing means a nearer name hides an outer name with the same spelling. The outer one is unchanged.
- The spec describes each scope as an environment record: a list of names plus a link to the outer scope.
Final definition: Scope is the set of places JavaScript searches, from the inside out, to find a name.
Up next: Closures, where a function keeps its outer variables after the outer function has finished.