Master JavaScript Scope, Context, and Closures: From Basics to React
This article explains JavaScript’s scope types, execution context, activation objects, and the lifecycle of functions, illustrating how closures retain memory, how garbage collection works, and provides practical solutions for common closure pitfalls in React functional components.
Scope & Context
1. Scope
Scope is the accessible range of functions and variables in JavaScript, divided into global scope, function (local) scope, and block scope. In a web environment the global object is window, while in Node it is global. Function scope creates an isolated environment that disappears after execution. Block scope is created by let or const and exists only within the block.
var a = 10; // global variable
if (true) {
console.log(b) // error: must be defined before use
let b = 20; // block variable
console.log(b) // 20
console.log(a) // 10
}
console.log(a) // 10
console.log(b) // error: not defined
function add(a, b) {
var c = 0; // local variable
console.log(c) // 0
return a + b + c;
}
console.log(c) // error: not definedWhen the engine detects a block scope, it creates a temporal dead zone that stores the names of all let / const declarations. Accessing a variable in this zone throws an error until the declaration is evaluated and the name is removed from the zone.
2. Context
The execution context’s this value refers to the object that invoked the function; it can be altered with call, apply, or bind.
2. Function Execution
During execution, an Execution Context is created, which builds an Activation Object (AO) at the top of the scope chain and sets the this value. The AO stores variables, parameters (initially undefined), and inner function declarations.
function add(a, b) {
debugger
var temp1 = 100;
function validateNum(n) {
return typeof n === "number";
}
var validateNum = 100;
return a + b;
}
console.log(add(1, 2))The function’s actual parameters are assigned before the function body runs, while variables declared inside the function are initialized to undefined.
JavaScript uses lexical scoping: a function’s scope is determined when it is declared and stored in the hidden [[scope]] property.
function add(a: number, b: number): number {
return a + b;
}The [[scope]] of add contains the global window object. If a function is defined inside another function, its [[scope]] includes both the outer function’s AO and the global object.
function add(a, b) {
function validateNum(n) {
console.log(a, b)
return typeof n === "number";
}
debugger
if (!validateNum(a) || !validateNum(b)) {
throw new Error('type error');
}
return a + b;
}
console.log(add(1,2))
console.log(add(2,3))3. Function End and Memory Release
When a function finishes, its Activation Object is eventually garbage‑collected. However, the function object retains its [[scope]], so inner functions that reference the outer AO keep that memory alive.
4. Closures
A closure is a memory region that remains referenced by at least one variable, preventing its release. If a function’s inner function captures variables from the outer AO, that AO stays alive.
var el = document.getElementById('id');
function add(a, b) {
function validateNum(n) {
return typeof n === "number";
}
el.onclick = function clickHandle() {
console.log(a, b);
};
if (!validateNum(a) || !validateNum(b)) {
throw new Error('type error');
}
return a + b;
}
console.log(add(1, 2))Because clickHandle is stored on the DOM element, its [[scope]] retains the outer AO, causing a memory leak.
5. When No Closure Is Formed
If an inner function does not reference any outer variables, no closure is created.
function add(a, b) {
function validateNum(n) {
return typeof n === "number";
}
debugger
if (!validateNum(a) || !validateNum(b)) {
throw new Error('type error');
}
return a + b;
}
console.log(add(1, 2))The resulting scope chain contains only the global object and the function’s own AO.
6. React Functional Component Closures
const [value, setValue] = useState([]);
useEffect(() => {
notificationCenter.on(EVENT_NAME, eventListener);
return () => {
notificationCenter.off(EVENT_NAME, eventListener);
};
}, []);
function eventListener(chatId?: string) {
console.log(value);
}The event listener captures the initial value because it is created only once during the first render; subsequent state updates do not affect the captured value.
Two solutions:
Add the needed state variables to the useEffect dependency array so the listener is re‑registered when they change.
Store the mutable value in a ref and read ref.current inside the listener, ensuring the latest data is accessed.
const [value, setValue] = useState([]);
const valueRef = useRef([]);
useEffect(() => {
notificationCenter.on(EVENT_NAME, eventListener);
return () => {
notificationCenter.off(EVENT_NAME, eventListener);
};
}, []);
function eventListener(chatId?: string) {
console.log(valueRef.current);
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
