Understanding the Scope Chain in JavaScript
The scope chain is what JavaScript uses to resolve every variable read in your code. Whenever you reference a name, the engine walks outward through enclosing lexical environments until it finds a binding or fails with ReferenceError. This lesson covers how the chain is built, how lookup works, and how shadowing interacts with it.
Where this lesson lives in the cluster: this lesson canonically owns lexical scoping, scope chain mechanics, and variable shadowing. For var/let/const declaration semantics, hoisting, and the TDZ, see Scope and Hoisting. For the execution context that holds the lexical environment, see Context and Call Stack. For the practical use of these mechanics in functions that outlive their parent scope, see Closures.
Introduction to Execution Context
What is an Execution Context?
An execution context is an abstract concept that holds information about the environment within which the current code is being executed. It contains:
- Variable Environment: Where variables and functions are stored.
- Lexical Environment: The environment containing the references to the variables and functions within the code.
- This Binding: The value of the
thiskeyword.
An execution context is created when the JavaScript engine prepares to execute a piece of code.
Types of Execution Contexts
-
Global Execution Context (GEC)
- Created when your script first runs.
- There is only one GEC per window or global object.
- Sets up the global scope, where global variables and functions reside.
-
Function Execution Context (FEC)
- Created whenever a function is invoked.
- Each function call has its own execution context.
- Contains its own variable and lexical environments.
-
Eval Execution Context
- Created when code is executed inside an
eval()function. - Not commonly used due to security and performance concerns.
- Created when code is executed inside an
Variable Environments and Lexical Environments
Variable Environment
- A component of the execution context that holds variables and function declarations defined within that context.
- Stores variables declared with
var,let, andconst.
Lexical Environment
- A structure that holds identifier-variable mappings (a map of variable names to where they are stored).
- Each lexical environment has an outer reference to its parent lexical environment, forming a chain.
Note: In JavaScript, functions are executed in the context in which they were defined, not where they are called. This is known as lexical scoping.
Understanding Scope
Scope
Scope determines the accessibility of variables and functions at various parts of your code.
Types of Scope
Global Scope
- Variables declared outside any function or block are in the global scope.
- Accessible from anywhere in your code.
Example:
var globalVar = 'I am global'
function foo() {
console.log(globalVar) // Accessible here
}
foo() // Outputs: I am global
Function Scope
- Variables declared within a function are scoped to that function.
- Not accessible outside the function.
Example:
function foo() {
var functionVar = 'I am inside a function'
console.log(functionVar) // Accessible here
}
foo() // Outputs: I am inside a function
console.log(functionVar) // Error: functionVar is not defined
Block Scope (ES6)
- Introduced in ES6 with
letandconst. - Variables declared within a block
{}are scoped to that block.
Example:
{
let blockVar = 'I am inside a block'
console.log(blockVar) // Accessible here
}
console.log(blockVar) // Error: blockVar is not defined
Note: Variables declared with var are not block-scoped; they are function-scoped.
Scope Chain Lookup
How the Scope Chain Works
- When a variable is accessed, JavaScript starts looking in the current scope.
- If not found, it looks up the scope chain to the outer scopes.
- The chain continues up to the global scope.
- If the variable is not found in any scope, a
ReferenceErroris thrown.
Visualization:
[Current Scope]
↑
[Parent Scope]
↑
[Global Scope]
Variable Lookup Process
Example:
var globalVar = 'Global'
function outerFunction() {
var outerVar = 'Outer'
function innerFunction() {
var innerVar = 'Inner'
console.log(innerVar) // Found in current scope
console.log(outerVar) // Found in parent scope
console.log(globalVar) // Found in global scope
console.log(nonExistent) // ReferenceError
}
innerFunction()
}
outerFunction()
Explanation:
innerVaris found ininnerFunction's scope.outerVaris not ininnerFunction, so it looks up toouterFunction.globalVaris not ininnerFunctionorouterFunction, so it looks up to the global scope.nonExistentis not found in any scope, so aReferenceErroris thrown.
Variable Shadowing
What is Variable Shadowing?
Variable shadowing occurs when a variable declared within a certain scope (e.g., a function) has the same name as a variable in an outer scope. The inner variable shadows the outer one.
Examples of Variable Shadowing
Example 1: Function Scope Shadowing
var value = 'Global'
function shadowingExample() {
var value = 'Local'
console.log(value) // Outputs: Local
}
shadowingExample()
console.log(value) // Outputs: Global
Explanation:
- The
valuevariable insideshadowingExampleshadows the globalvalue. - Inside the function,
valuerefers to the local variable. - Outside the function,
valuerefers to the global variable.
Example 2: Block Scope Shadowing with let
let number = 10
if (true) {
let number = 20
console.log(number) // Outputs: 20
}
console.log(number) // Outputs: 10
Explanation:
- The
numbervariable inside the block shadows the outernumber. - Inside the block,
numberis20. - Outside the block,
numberis10.
Shadowing with Parameters
Example:
var name = 'Global'
function greet(name) {
console.log('Hello, ' + name)
}
greet('Alice') // Outputs: Hello, Alice
Explanation:
- The parameter
nameshadows the globalname. - Inside
greet,namerefers to the parameter.
Best Practices for Managing Scope
Avoid Global Variables
-
Reason: Reduces the risk of variable name collisions and unintended interactions.
-
Solution: Use function or block scope to encapsulate variables.
Use let and const Instead of var
-
Reason:
letandconsthave block scope, which is more predictable. -
Example:
for (let i = 0; i < 5; i++) { // i is scoped to this block } // console.log(i); // Error: i is not defined
Be Mindful of Variable Shadowing
-
Reason: Can lead to confusion and bugs if not handled carefully.
-
Solution:
- Use different variable names for clarity.
- Avoid re-declaring variables in inner scopes unless intentional.
Limit the Use of Global Scope
-
Reason: Keeps the global namespace clean.
-
Solution: Encapsulate code within modules or immediately-invoked function expressions (IIFEs).
Example:
;(function () {
// Code here is scoped to this function
let localVar = 'I am local'
})()
console.log(localVar) // Error: localVar is not defined
Understand Hoisting
-
Reason: Variable and function declarations are hoisted to the top of their scope.
-
Example:
console.log(a) // Outputs: undefined var a = 10 -
Explanation: The declaration of
ais hoisted, but its assignment is not.
Avoid Polluting the Global Namespace
- Solution: Use modules (
import/export) to manage scope and dependencies.
Exercises
Exercise 1: Scope Chain Lookup
Question:
Predict the output of the following code:
var x = 10
function outer() {
var x = 20
function inner() {
console.log(x)
}
inner()
}
outer()
console.log(x)
Answer:
20
10
Explanation:
- Inside
inner,xis not found, so it looks up toouterand findsx = 20. - Outside,
console.log(x)outputs the globalx = 10.
Exercise 2: Variable Shadowing
Question:
What will be the output of the following code?
let name = 'Global'
function printName() {
console.log(name)
}
function shadowName() {
let name = 'Local'
printName()
}
shadowName()
Answer:
Global
Explanation:
printNameuses thenamevariable from its lexical scope, which is the global scope, as it was defined there.- The
namevariable inshadowNamedoes not affectprintNamebecause of lexical scoping.
Exercise 3: Block Scope
Question:
Predict the output and explain any errors in the following code:
if (true) {
var a = 5
let b = 10
const c = 15
}
console.log(a)
try {
console.log(b)
} catch (err) {
console.log(err.message)
}
try {
console.log(c)
} catch (err) {
console.log(err.message)
}
Answer:
5
b is not defined
c is not defined
Explanation:
var ais function-scoped (or globally scoped if not inside a function), soaleaks out of theifblock and is accessible afterwards.let bandconst care block-scoped — they exist only inside theifblock's{}. Accessing them outside throwsReferenceError: <name> is not defined.- Without the
try/catchwrappers, the first error would halt execution and the thirdconsole.logwould never run. Thrown errors interrupt control flow; they don't skip individual lines. Modern JS code should default tolet/constto avoid the leakage shown fora.
Exercise 4: Hoisting and Temporal Dead Zone
Question:
What will be the output of the following code?
console.log(x)
let x = 10
Answer:
ReferenceError: Cannot access 'x' before initialization
Explanation:
- Variables declared with
letare hoisted but not initialized, leading to a Temporal Dead Zone (TDZ) from the start of the block until the declaration is processed. - Accessing
xbefore it's initialized throws aReferenceError.
Exercise 5: Function Scope and Closure
Question:
Consider the following code:
function makeCounter() {
let count = 0
return function () {
count += 1
console.log(count)
}
}
const counter1 = makeCounter()
counter1() // ?
counter1() // ?
const counter2 = makeCounter()
counter2() // ?
Answer:
1
2
1
Explanation:
counter1andcounter2are separate instances of the inner function with their owncountvariables.- Each call to
makeCountercreates a new execution context with its owncount. counter1increments itscountto1and2on subsequent calls.counter2starts its owncountat0, so the first call outputs1.
Understanding execution context and scope chains is crucial for writing predictable and efficient JavaScript code. By knowing how JavaScript looks up variables and how scope affects variable accessibility, you can avoid common pitfalls like variable shadowing and unintended global variables. Mastery of these concepts not only improves your coding skills but also prepares you for technical interviews and real-world development challenges.
Practice Problems
Variable Shadowing
Lexical Capture Output Prediction
Scope Chain Lookup Mechanics
Lexical vs Dynamic Scoping
Block Scope vs Function Scope
Let's continue exploring the next page. Take your time, and proceed when you're ready.