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.

ELab Team
ELab Team
ELab Team
Master JavaScript Scope, Context, and Closures: From Basics to React

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 defined

When 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);
}
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaScriptfrontend developmentReactclosurescope
ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.