Master JavaScript Scopes, Hoisting, and Closures: A Deep Dive
This article explains JavaScript's variable scope models—including lexical and dynamic scope—covers the three scope types (global, function, and block), details hoisting, the eval function, scope chains, and closures, and shows practical code examples for each concept.
Scope
Programming languages store values in variables and allow later access or modification; a variable's scope defines where it can be accessed. JavaScript uses lexical (static) scope, while languages like Bash and Perl use dynamic scope.
Three JavaScript Scopes
Global scope: the outermost code level.
Function scope: each function creates its own scope.
Block scope (ES6): introduced by
let,
const, and
{}blocks.
Global Scope
Variables defined at the top level are attached to the global
windowobject and can be accessed anywhere.
<code>window.innerHeight</code>Variables declared with
varbecome properties of
window. Implicit assignments without a keyword also create global variables.
<code>var a = 1;
console.log(window.a); // 1
function func1() { console.log('hello'); }
func1();
window.func1(); // hello</code> <code>b = 2; // global variable
function func1() { c = 2; }
func1();
console.log(window.b); // 2
console.log(window.c); // 2</code>Variable Hoisting
Declarations with
varare moved to the top of their scope, so the variable exists before its line of code but holds
undefined.
<code>console.log(window.a); // undefined
var a = 1;</code>Variable hoisting: var declarations are treated as if they appear at the top of the current scope.
The JavaScript engine first parses all declarations, then executes the code.
Parse and collect declared variables.
Execute the code.
<code>console.log(a); // undefined
var a = 1;
// equivalent to
var a;
console.log(a); // undefined
a = 1;</code>Function Scope
Variables and inner functions defined inside a function are confined to that function's scope and cannot be accessed from outside.
<code>function func1() { var a = 1; return a; }
func1(); // 1
console.log(a); // ReferenceError</code>Function Declaration
Function declarations are hoisted, allowing them to be called before their definition.
<code>console.log(add(10, 10)); // 20
function add(a, b) { return a + b; }</code>Function Expression
Function expressions are not hoisted; the variable is hoisted but its assignment is not.
<code>console.log(add(10, 10)); // TypeError
var add = function(a, b) { return a + b; };</code>Combined Declaration and Expression
<code>test(); // calls function declaration
var test = function() { console.log('expression'); };
function test() { console.log('declaration'); }
test(); // calls expression</code>Block Scope
ES6 introduced block scope with
letand
const. Variables declared with
letinside a block are not accessible outside.
<code>{ var a = 1; let b = 2; }
console.log(a); // 1
console.log(b); // ReferenceError</code> lethas a "temporal dead zone" where the variable cannot be accessed before its declaration.
<code>{ console.log(a); // ReferenceError
let a = 1; }</code>Temporal dead zone: a variable is unaccessible from the start of its block until its declaration line.
Const
constbehaves like
letregarding scope and the temporal dead zone, but it must be initialized and cannot be reassigned.
<code>const a = 1;
a = 2; // TypeError</code>Objects assigned to
constcan have their properties mutated unless frozen.
<code>const obj = { a: 1, b: 2 };
obj.b = 3; // allowed
Object.freeze(obj); // now properties cannot change</code>var / let / const Differences
Aspect
var
let
const
Scope
Function scope
Block scope
Block scope
Declaration
Can be redeclared
Cannot be redeclared
Cannot be redeclared, must initialize
Features
Hoisting (global if no var)
Temporal dead zone
Temporal dead zone
Eval
evalexecutes a string of JavaScript code. It is discouraged due to performance, security, and maintainability concerns, but understanding its behavior is useful.
If the argument is an expression string, it evaluates the expression.
If the argument is a statement string, it executes the statements.
If the argument is not a string, it returns the argument unchanged.
<code>eval('2 + 2'); // 4
eval("console.log('hi')"); // hi
eval(new String('2 + 2')); // String{'2 + 2'}</code>Eval and Scope
Direct
evalruns in the current scope; indirect calls (e.g.,
window.eval) run in the global scope.
<code>function testEval() {
eval('var a = 111');
console.log(a); // 111
}
testEval();
console.log(a); // ReferenceError (direct)
window.eval('var a = 111');
console.log(a); // 111 (indirect)</code>Eval and Variable Hoisting
Variables and functions created inside
evalare not hoisted; they are created only when
evalruns.
<code>// function
sayHi(); // ReferenceError
eval('function sayHi(){ console.log("hi"); }');
sayHi(); // hi
// var
console.log(msg); // ReferenceError
eval('var msg = "hello"');
console.log(msg); // hello
// let
eval('let msg = "hello"; console.log(msg)'); // hello
console.log(msg); // ReferenceError</code>Scope Chain
When a block or function is nested inside another, the engine looks for variables first in the current scope, then outward through the chain until it reaches the global scope.
<code>var c = 1;
function func() {
var b = 2;
function add(a) { return a + b + c; }
return add;
}
const addTest = func();
addTest(3); // 6</code>Closure
A closure is an inner function that retains access to the variables of its outer (lexical) scope even after the outer function has finished executing.
<code>var c = 1;
function func() {
var b = 2;
function add(a) { return a + b + c; }
return add;
}
const addTest = func();
addTest(3); // 6</code>When using closures, the original function remains in memory, which can increase memory usage and potentially cause leaks.
Closure Usage
Callbacks
In a
forloop with
let, each iteration creates a new block-scoped variable, so asynchronous callbacks capture the correct value.
<code>for (let i = 0; i < 5; i++) {
setTimeout(function(){ console.log(i); }, 0);
}
// outputs: 0 1 2 3 4</code>Modularization
Modules can be built by returning an object that exposes internal functions, which form closures over the module's private state.
<code>function arrOperate() {
let errorMsg = 'Please provide an array';
function getPositiveArr(arr) {
if (Array.isArray(arr)) {
return arr.sort((a,b) => a - b);
} else { throw errorMsg; }
}
function getBackArr(arr) {
if (Array.isArray(arr)) {
return arr.sort((a,b) => b - a);
} else { throw errorMsg; }
}
return { getPositiveArr, getBackArr };
}
const arrObj = arrOperate();
arrObj.getPositiveArr([1,10,5,89,46]); // [1,5,10,46,89]
arrObj.getBackArr([1,10,5,89,46]); // [89,46,10,5,1]
</code>Summary
Understanding JavaScript scope determines where variables are accessible, helps avoid undefined errors, and improves code quality.
References
"You Don't Know JS"
"JavaScript: The Good Parts"
WeDoctor Frontend Technology
Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.
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.