Understanding JavaScript Closures and Their Memory‑Leak Mechanisms
This article demystifies JavaScript closures, explains how V8 builds scope chains and closure objects during compilation, and demonstrates why closures can cause memory leaks by retaining references, illustrated with multiple runnable code examples and practical debugging tips.
Many online explanations of JavaScript closures simplify the concept to "a function that accesses free variables," which is misleading; this article corrects those misconceptions and reveals the true mechanics of closures and their impact on memory.
Scope Chain and [[Scopes]]
Global variables reside in a Variable Object (VO) and each function has an Activation Object (AO). Every function carries a [[Scopes]] internal property that stores its lexical scope chain, which includes the AO of the current function and the [[Scopes]] of its parent functions.
// 1: global.VO = {t}
let t = 111
function fun(){
// 3: fun.AO = {a,b}
let a = 1
let b = 2
function fun1(){
// 5: fun1.AO = {c}
let c = 3
}
// 4: fun1.[[Scopes]] = [...fun.[[Scopes]], fun.AO]
}
// 2: fun.[[Scopes]] = [global.VO]
fun()When fun() is called, the engine pre‑compiles fun , creates its AO, and then executes the body, updating the VO with the values of a and b . The [[Scopes]] array works like a stack, always preferring the nearest AO for variable lookup.
Closure Object in V8
During the pre‑compilation phase V8 performs three byte‑code steps: CreateFunctionContext , PushContext , and CreateClosure . A Closure object is created for every function, storing references only to the variables that the function actually uses. This Closure replaces the parent AO in the child’s [[Scopes]] chain.
var t = 111
function fun(){
let a = 1, b = 2, c = 3
// 4: fun1.[[Scopes]] = [global.VO, fun.Closure]
function fun1(){
// arr added to Closure
console.log(a)
}
function method(){
console.log(b)
}
fun1()
}
fun()The closure is built at compile time, not at runtime, and all inner functions share the same Closure . The closure stores references, not copies, so changes to the original variable are reflected inside the closure.
How Closures Cause Memory Leaks
A closure cannot be reclaimed while any child function’s scope chain still references it. If a global reference (e.g., window.f ) holds a function whose scope includes a closure that captures a large object, that object remains in memory.
function fun(){
let arr = Array(10000000)
function fun1(){ console.log(arr) }
function fun2(){}
return fun2
}
window.f = fun()Even though fun2 does not use arr , the closure created for fun still contains arr , and because window.f keeps fun2 alive, the closure (and thus arr ) cannot be garbage‑collected.
Classic Leak Example
The following loop repeatedly creates a large object and keeps a reference to it via a closure, causing memory to grow without bound.
let theThing = null;
let replaceThing = function(){
let leak = theThing;
function unused(){ if(leak){} }
theThing = {
longStr: new Array(1000000),
someMethod(){ }
};
};
let index = 0;
while(index < 100){
replaceThing();
index++;
}Each iteration adds a new theThing object to the closure chain, preventing previous objects from being freed. Breaking the chain—by nullifying the global variable or the internal leak reference—allows the garbage collector to reclaim memory.
let theThing = null;
let replaceThing = function(){
let leak = theThing;
function unused(){ if(leak){} }
theThing = {
longStr: new Array(1000000),
someMethod(){ }
};
leak = null; // break the reference chain
};
let index = 0;
while(index < 100){
replaceThing();
index++;
}Summary
Every function gets a closure object during pre‑compilation.
The compiler scans inner functions and adds only the variables they actually use to the closure.
A closure is reclaimed only after all inner functions that reference it are destroyed; otherwise it leads to memory leaks.
Practical demos and Chrome’s Performance monitor can be used to detect such leaks.
Understanding these details helps developers write memory‑efficient JavaScript, especially when dealing with long‑living callbacks, event handlers, or large data structures.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
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.