JavaScript Scope Isn't Magic — It's a Lookup Rule You Can Debug in Five Questions
Let's first look at a very simple piece of JavaScript code:
function foo() {
var a = 2;
}
foo();
console.log(a);
After running it, we get the following result: The variable a has clearly been declared inside the foo function, and foo has already been executed. Why does the last line still throw an error?
Does the variable
a disappear after the function finishes executing?
Let's look at another piece of code:
var a = 1;
function foo() {
var a = 2;
function bar() {
console.log(a);
}
bar();
}
foo();
This time, the bar function clearly does not declare the variable a internally, yet it can output normally: 2
These two seemingly contradictory phenomena are actually related to a very important concept in JavaScript — Scope
1. How is JavaScript Code Executed?
After we get a piece of JavaScript code, the computer cannot understand it directly.
Computers can ultimately only execute instructions that the machine can recognize, and the JavaScript code we write is a high-level language. Therefore, between the JavaScript code and the computer, a tool is needed that can "read" and execute JavaScript, which is the JavaScript Engine.
Different runtime environments may use different JavaScript engines. For example:
- Browsers like Chrome and Edge mainly use the V8 engine
- Firefox uses the SpiderMonkey engine
- Safari uses the JavaScriptCore engine
- Node.js also uses the V8 engine
After the JavaScript engine gets the code, it does not immediately start executing from the first line. Instead, it needs to analyze the code first to figure out what variables, functions, and structural relationships exist within the code.
This process can be simply summarized as:
JavaScript Source Code
↓
Lexical Analysis
↓
Syntax Analysis
↓
Generate AST
↓
Generate and Execute Code
1.1 Lexical Analysis
First, the JavaScript engine breaks down a whole piece of code into meaningful lexical units.
For example:
var a = 10;
It will roughly be broken down into:
var
a
=
10
;
The engine will recognize:
varis the keyword for declaring a variableais the variable name=is the assignment operator10is a numeric value;indicates the end of a statement
This process is called Lexical Analysis.
1.2 Syntax Analysis
Just recognizing these lexical units is not enough; the engine also needs to understand what they express when combined.
For the following line of code:
var a = 10;
The JavaScript engine needs to understand:
A variable named
ais declared here, and the numeric value10is assigned to it.
Subsequently, the engine generates an AST, or Abstract Syntax Tree, based on the code's grammatical structure.
The full name of AST is:
Abstract Syntax Tree
You can simply understand AST as:
The code structure diagram organized by the JavaScript engine.
Through this structure diagram, the engine can know which variables are declared, which functions are defined, and where these codes are located.
1.3 Generate and Execute Code
After completing lexical analysis and syntax analysis, the JavaScript engine generates executable code based on the AST and starts running the program.
Therefore, the execution of JavaScript code is not simply "see one line and immediately execute one line."
Before actual execution, the JavaScript engine has already analyzed the code. It is during this process that the engine determines the location of variable declarations and the scope to which the variables belong.
Next, we can further discuss: What is scope, and why some variables, even though declared, still cannot be accessed.
2. Declaring a Variable and Assigning a Value to a Variable Are Two Different Things
Look at the following code:
var a;
console.log(a);
a = 10;
It will output:
Why not 10?
Because the code is executed in the following order:
var a; // Declare variable a
console.log(a); // Read variable a
a = 10; // Assign a value to variable a
The first line only declares the variable a; no specific value has been actively set for it yet.
After declaring a variable using var, the variable is initialized to: undefined
So when executing the following line of code: console.log(a);
The variable a already exists, but its value is temporarily undefined.
Until the execution of:
a = 10;
The value of variable a will then become 10.
You can think of variable declaration and assignment separately:
var a; // Declaration
a = 10; // Assignment
You can also write them together:
var a = 10;
var, let, and const can all be used to declare variables, but they differ in scope and initialization rules, which will be introduced in detail later.
3. Variable Hoisting with var
Look at the following code:
console.log(a);
var a = 10;
The output result is:
undefined
This is because JavaScript processes var declarations before executing the code. The above code can be approximately understood as:
var a;
console.log(a);
a = 10;
It's important to note that what is hoisted is the variable declaration, not the assignment. Therefore, accessing a does not yield 10, but undefined. This phenomenon is called Variable Hoisting.
4. What is Scope?
Scope determines where a variable can be accessed.
You can think of scope as the "effective activity range" of a variable.
For example:
function foo() {
var a = 2;
}
foo();
console.log(a);
The variable a is declared inside the foo function, so it can only be accessed inside the foo function.
When the code executes:
console.log(a);
It is already outside the foo function.
External code cannot directly access variables declared inside a function, so JavaScript will throw an error:
ReferenceError: a is not defined
It's important to note that the issue is not whether the function has been executed.
Even if foo() has been called, the local variables inside the function do not become external variables because of it.
function foo() {
var a = 2;
}
foo();
foo();
foo();
console.log(a);
No matter how many times it's called, the last line will throw an error.
Because whether a variable can be accessed does not depend on how many times the function has been executed, but on whether the accessing code is within the variable's scope.
There are three common types of scope in JavaScript:
- Global Scope
- Function Scope
- Block Scope
5. Global Scope
Variables declared outside of functions and code blocks are usually located in the outermost scope.
For example:
var a = 1;
console.log(a);
Here, the variable a is declared in the outermost layer, so subsequent code can access it.
Functions can usually also access variables declared externally:
var a = 1;
function foo() {
console.log(a);
}
foo();
The output result is: 1
The foo function does not declare the variable a internally, so JavaScript looks for it outside the function and finds:
var a = 1;
Therefore, the function can output 1.
Variables declared in the outermost layer generally have a larger accessible range, and such variables are often called global variables.
However, more global variables are not necessarily better.
For example:
var username = "Tom";
var page = 1;
var theme = "dark";
If many functions in the project can read and modify these variables, the following problems may arise:
- Variables are accidentally modified
- Different code uses the same variable name
- It's difficult to determine where a variable has changed
- Dependencies between code become increasingly complex
Therefore, in actual development, you should generally try to minimize the scope of variables.
6. Function Scope
Each function creates its own scope.
Variables declared inside a function can generally only be accessed within that function.
function foo() {
var a = 2;
console.log(a);
}
foo();
This code can output normally: 2
Because the code reading the variable a is also located inside the foo function.
But if console.log is placed outside the function:
function foo() {
var a = 2;
}
foo();
console.log(a);
The code will throw an error.
You can think of a function as a room.
External Area
└── foo Room
└── Variable a
The variable a is placed inside the foo room.
Code inside the room can use it, but code outside the room cannot directly enter the room and take this variable.
Function Parameters Also Belong Inside the Function
Let's look at a simple addition function:
function foo(a, b) {
return a + b;
}
foo(1, 2);
console.log(foo(2, 3));
The output result is: 5
In the function definition:
function foo(a, b)
a and b are the function's formal parameters.
When calling the function:
foo(2, 3);
2 and 3 are the actual arguments passed into the function.
When executing the function, it can be approximately understood as:
function foo(a, b) {
// a receives 2
// b receives 3
return a + b;
}
The formal parameters a and b are only valid inside the foo function.
The following code cannot access them directly outside the function:
function foo(a, b) {
return a + b;
}
foo(1, 2);
console.log(a);
Running this will throw:
ReferenceError: a is not defined
Because a here is a parameter inside the foo function and does not belong to the function's exterior.
7. How Do Inner Functions Look Up Variables?
Functions can also define functions inside them:
var a = 1;
function foo() {
var a = 2;
function bar() {
console.log(a);
}
bar();
}
foo();
This code will ultimately output: 2
However, the bar function does not declare the variable a internally:
function bar() {
console.log(a);
}
Why can it access a?
When JavaScript looks up a variable, it starts from the current scope.
For this line of code in the bar function:
console.log(a);
The lookup process can be simply understood as:
First look for a in bar
↓
Not found in bar
↓
Look in the outer foo
↓
Found var a = 2
Therefore, the final output is:
2
Although there is also:
var a = 1;
in the outermost layer, JavaScript has already found a in the closer foo function, so it will not continue looking outward.
This shows that JavaScript follows a simple principle when looking up variables:
First look in the current scope; if not found, then look in the outer scope.
Once the variable is found, the lookup stops.
We don't need to delve into more concepts here; just remember that the direction of variable lookup is from inside to outside.
8. Block Scope
Besides functions, code regions enclosed by a pair of curly braces {} can also limit the access range of variables.
For example:
if (true) {
let b = 2;
}
console.log(b);
Running this will throw an error:
ReferenceError: b is not defined
Because b is declared using let and can only be accessed within the code block where it resides.
Inside the code block, it can be read normally:
if (true) {
let b = 2;
console.log(b);
}
The output result is:
2
But once leaving the curly braces, the variable b can no longer be accessed.
Outer Scope
└── if Code Block
└── let b = 2
When declaring variables in a code block using let and const, a block scope is formed.
Common code blocks include:
if (true) {
// Code block
}
for (let i = 0; i < 3; i++) {
// Code block
}
while (condition) {
// Code block
}
You can also write a standalone code block:
{
let a = 10;
console.log(a);
}
console.log(a);
Inside the code block, it can output 10; outside the code block, it will throw an error.
9. Why Can var Leave a Code Block?
var and let behave differently within code blocks.
Look at the following example:
if (true) {
let b = 2;
var c = 3;
}
console.log(c);
The output result is: 3
The variable c is clearly declared inside the if code block, so why can it still be accessed after leaving the code block?
The reason is:
vardoes not have block scope.
Although c is written inside {}, this code block does not confine the variable declared with var internally.
Therefore:
if (true) {
var c = 3;
}
console.log(c);
can output 3 normally.
But let is different:
if (true) {
let b = 2;
}
console.log(b);
will directly throw an error.
You can use a simple comparison table to remember:
| Declaration Keyword | Function Scope | Block Scope |
|---|---|---|
var |
Yes | No |
let |
Yes | Yes |
const |
Yes | Yes |
"Has function scope" here means: after a variable is declared inside a function, it cannot be directly accessed from outside the function.
10. The Temporal Dead Zone of let and const
let and const not only have block scope but also form a Temporal Dead Zone (TDZ) before the declaration statement is executed.
Look at the following code:
var a = 100;
if (true) {
console.log(a);
let a = 10;
}
console.log(a);
You might think that when executing console.log(a) inside the if block, the inner a hasn't been declared yet, so JavaScript should look outward and output:
100
But the actual runtime result is:
ReferenceError: Cannot access 'a' before initialization
This is because let a = 10 creates a new variable binding in the current code block. The scope of this binding covers the entire if code block, not just starting from the position of the declaration statement.
Therefore, upon entering the if code block, the inner variable a has already shadowed the outer a:
Enter if code block
↓
Inner a is already bound to the current scope
↓
But a has not yet been initialized, it is in the temporal dead zone
↓
Execute let a = 10
↓
Temporal dead zone ends, a can be accessed normally
When the code executes:
console.log(a);
What JavaScript finds is the a in the current code block, not the outer a with the value 100. But at this point, the inner a has not yet been initialized, so it directly throws an error and will not continue to look for a variable with the same name outward.
If the code order is adjusted:
var a = 100;
if (true) {
let a = 10;
console.log(a);
}
console.log(a);
The output result is:
10
100
Inside the code block, the local variable declared by let is accessed; after leaving the code block, the outer variable is still accessed.
const also has the same temporal dead zone:
var a = 100;
if (true) {
console.log(a);
const a = 10;
}
This code will also throw an error.
Therefore, the core of the temporal dead zone is not just "cannot access a variable before declaration," but also includes:
letandconstbind the variable to the current block scope. Before the declaration is complete, the variable is in the temporal dead zone; even if a variable with the same name exists in an outer scope, JavaScript will not access the outer variable instead.
11. What is the Difference Between var and let When Accessed Before Declaration?
First, look at var:
console.log(a);
var a = 10;
The output result is:
undefined
Now look at let:
console.log(a);
let a = 10;
The runtime result is:
ReferenceError: Cannot access 'a' before initialization
The main difference between the two can be simply summarized as:
var
The variable is initialized to undefined before the declaration statement is executed.
console.log(a); // undefined
var a = 10;
let
The variable is recorded by the current scope, but it is not initialized before the declaration statement is executed, so it cannot be accessed.
console.log(a); // ReferenceError
let a = 10;
Therefore, in actual development, do not rely on the behavior of var returning undefined before declaration.
The clearer way to write is always:
let a = 10;
console.log(a);
Declare and initialize the variable first, then use it.
12. What is the Difference Between const and let?
Both const and let have block scope and both have a temporal dead zone.
Their main difference lies in whether the variable can be reassigned.
Variables declared with let can be reassigned:
let a = 10;
a = 20;
console.log(a);
The output result is: 20
Variables declared with const cannot be reassigned:
const a = 10;
a = 20;
Running this will throw an error:
TypeError: Assignment to constant variable
Additionally, const must be provided with an initial value immediately upon declaration.
The following writing is not allowed:
const a;
It must be written as:
const a = 10;
So you can simply remember:
let: The variable needs to be reassigned laterconst: The variable does not need to be reassigned later
In actual development, you can prioritize using const.
Only use let when you are certain the variable needs to be reassigned later.
const Objects Can Still Have Their Properties Modified
It's important to note that const restricts the reassignment of the variable, which does not mean the properties inside an object cannot be changed at all.
const user = {
name: "Tom"
};
user.name = "Jerry";
console.log(user.name);
The output result is:
Jerry
This is allowed because the variable user still points to the original object.
What is not allowed is making it point to another object:
const user = {
name: "Tom"
};
user = {
name: "Jerry"
};
This will cause a reassignment error.
13. How to Determine if a Variable Can Be Accessed?
When encountering a problem where a variable cannot be accessed, you can check by following these steps.
13.1 Where is the Variable Declared?
First, find the variable's declaration statement:
var a = 1;
let a = 1;
const a = 1;
Then determine if it is located in:
- The outermost layer
- Inside a function
- Inside a code block
13.2 Where is the Code Using the Variable?
Determine if the location using the variable is still within the variable's scope.
function foo() {
var a = 2;
}
console.log(a);
The variable is declared inside the function, and the usage location is outside the function, so it cannot be accessed.
13.3 Is var, let, or const Used?
If the variable is declared in a code block:
if (true) {
var a = 1;
let b = 2;
}
After leaving the code block:
acan still be accessedbcannot be accessed
Because var does not have block scope, while let does.
13.4 Is It Accessed Before Declaration?
console.log(a);
let a = 10;
a is in the temporal dead zone, so it cannot be accessed.
13.5 Is There a Variable with the Same Name in the Current Scope?
var a = 100;
if (true) {
console.log(a);
let a = 10;
}
The a inside the code block shadows the outer a, but it has not yet been initialized, so an error will be thrown.
14. Back to the Initial Problem
The code at the beginning of the article is:
function foo() {
var a = 2;
}
foo();
console.log(a);
Why can't the variable a be accessed?
Because it is declared inside the foo function and only belongs to foo's function scope.
Code outside the function cannot directly access variables inside the function.
And for the following code:
var a = 1;
function foo() {
var a = 2;
function bar() {
console.log(a);
}
bar();
}
foo();
Why does it output 2?
Because bar does not declare the variable a, JavaScript will look outward and first find in foo:
var a = 2;
Once found, the lookup stops, so it will not use the outermost variable with the value 1.
Seemingly mysterious variable access problems can actually be reduced to a few simple questions:
Where is the variable declared?
Where is the current code located?
How is the variable declared?
Is there a variable with the same name in the current scope?
Has the variable been initialized?
As long as you answer these questions in order, you can determine why a variable can be accessed, or why it suddenly throws an error.
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
Future prospects