Scope and Closures
What is Scope?
Compilation Principles
JavaScript is a compiled language, but its compilation process differs from traditional compiled languages. Instead of compiling the entire code before execution, JavaScript engines perform Just-In-Time (JIT) Compilation during execution.
The compilation process consists of three stages:
- Lexical Analysis: Breaks the code into individual lexical units (tokens), such as variable names, keywords, operators, etc.
- Syntax Analysis: Combines tokens into an Abstract Syntax Tree (AST), representing the code’s syntactic structure.
- Code Generation: Converts the AST into executable machine code.
Understanding Scope
Scope refers to the region in a program where variables are defined, determining their visibility and lifecycle. JavaScript has two types of scope: global scope and local scope.
Global Scope
Global scope is accessible from anywhere in the code. In a browser environment, the global scope is the window object; in Node.js, it is the global object.
var globalVar = 'I am global';
function foo() {
console.log(globalVar); // Can access global variable
}
foo(); // Output: I am globalLocal Scope
Local scope is defined within a function and is only accessible inside that function.
function bar() {
var localVar = 'I am local';
console.log(localVar); // Can access local variable
}
bar(); // Output: I am local
console.log(localVar); // Error: localVar is not definedLexical Scope
Lexical scope is determined during the code’s writing phase, based on the code structure, not the function’s call site.
function outer() {
var outerVar = 'I am outer';
function inner() {
console.log(outerVar); // Can access outer function's variable
}
inner();
}
outer(); // Output: I am outerIn this example, the inner function can access the outerVar variable from the outer function because its lexical scope includes the outer function’s scope.
Lexical Scope
Lexical Phase
Lexical scope is established during the lexical analysis phase, based on the code’s structure, not the function’s call site.
function foo() {
var a = 1;
function bar() {
var b = 2;
function baz() {
console.log(a, b); // Can access outer functions' variables
}
baz();
}
bar();
}
foo(); // Output: 1 2In this example, the baz function can access variables from foo and bar because its lexical scope includes their scopes.
Deceiving Lexical Scope
Although lexical scope is determined during lexical analysis, certain techniques can be used to “deceive” it, allowing functions to access variables they otherwise couldn’t.
eval
The eval function executes a string as code, altering the lexical scope.
function foo(str) {
eval(str); // Execute the input string
console.log(a); // Can access variable defined in eval
}
foo('var a = 1'); // Output: 1with
The with statement adds an object to the top of the scope chain, making variable lookups start from that object.
var obj = { a: 1 };
function foo() {
with (obj) {
console.log(a); // Can access obj's variable
}
}
foo(); // Output: 1Note: Both eval and with negatively impact performance and maintainability and should be avoided.
Performance
Lexical scope lookups are determined during compilation, making them generally more efficient than dynamic scope. When resolving variables, the JavaScript engine starts from the current scope and moves up the scope chain until the variable is found or the global scope is reached.
Function Scope and Block Scope
Function Scope
In JavaScript, functions are first-class citizens, meaning they can be passed as arguments, returned as values, and defined within other functions. Variables defined inside a function are only visible within that function, creating function scope.
function foo() {
var a = 1;
function bar() {
var b = 2;
console.log(a + b); // Can access outer function's variable
}
bar();
}
foo(); // Output: 3In this example, the bar function can access the a variable from foo because its lexical scope includes foo’s scope.
Hiding Internal Implementation
Function scope can be used to hide internal implementation details, preventing external code from accessing or modifying internal variables.
function createCounter() {
var count = 0;
return function() {
count++;
return count;
};
}
var counter = createCounter();
console.log(counter()); // Output: 1
console.log(counter()); // Output: 2In this example, createCounter returns a closure that encapsulates the count variable. External code cannot directly access or modify count, only interacting with it through the closure’s interface.
Function Scope
Function scope refers to the scope defined within a function, accessible only inside that function.
function foo() {
var a = 1;
console.log(a); // Can access local variable
}
foo(); // Output: 1
console.log(a); // Error: a is not definedIn this example, the a variable is only visible within the foo function, inaccessible to external code.
Block Scope
Block scope refers to the scope defined within code blocks (e.g., if, for, while), accessible only inside that block.
if (true) {
let b = 2;
console.log(b); // Can access block-scoped variable
}
console.log(b); // Error: b is not definedIn this example, the b variable is only visible within the if block, inaccessible to external code.
Garbage Collection
JavaScript’s garbage collector automatically reclaims memory no longer in use. When a variable is no longer referenced, its memory is freed.
function foo() {
var a = 1;
return function() {
console.log(a);
};
}
var bar = foo();
bar(); // Output: 1
// After foo executes, a is no longer referenced and can be garbage collectedIn this example, after foo executes, the a variable is no longer referenced (except by the closure), and the garbage collector can free its memory.
Hoisting
Chicken or Egg?
In JavaScript, variable and function declarations are hoisted to the top of their scope, but variable assignments are not.
console.log(a); // Output: undefined
var a = 1;In this example, var a = 1; is hoisted as var a;, so console.log(a); outputs undefined.
Compiler Strikes Again
The JavaScript engine compiles code before execution, hoisting variable and function declarations during this process.
foo(); // Output: I am foo
function foo() {
console.log('I am foo');
}In this example, the foo function declaration is hoisted to the top of the scope, allowing foo(); to execute correctly.
Functions First
Function declarations are hoisted before variable declarations.
var foo = 1;
function foo() {
console.log('I am foo');
}
console.log(foo); // Output: 1In this example, the function declaration foo is hoisted first, but the variable assignment foo = 1 overwrites it, so console.log(foo); outputs 1.
Scope Closures
The Essence
A closure is a function that retains access to variables from its enclosing scope, even after the outer function has finished executing.
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
let counter = createCounter();
console.log(counter()); // Output: 1
console.log(counter()); // Output: 2In this example, createCounter returns a closure that retains access to the count variable. Even after createCounter finishes, the closure can still access and modify count.
Now I Get It
Closures work through the scope chain. When a function is defined inside another, its scope chain includes the outer function’s scope. After the outer function executes, the inner function retains a reference to that scope chain, allowing access to its variables.
Closures have many practical uses, including:
- Data Hiding: Encapsulating internal state to prevent external access or modification.
- Callbacks: Creating callback functions that retain access to outer variables.
- Modularity: Building modules where variables and functions are private to the module.



