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 window object and can be accessed anywhere. window.innerHeight Variables declared with var become properties of window. Implicit assignments without a keyword also create global variables.
var a = 1;
console.log(window.a); // 1
function func1() { console.log('hello'); }
func1();
window.func1(); // hello b = 2; // global variable
function func1() { c = 2; }
func1();
console.log(window.b); // 2
console.log(window.c); // 2Variable Hoisting
Declarations with var are moved to the top of their scope, so the variable exists before its line of code but holds undefined.
console.log(window.a); // undefined
var a = 1;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.
console.log(a); // undefined
var a = 1;
// equivalent to
var a;
console.log(a); // undefined
a = 1;Function Scope
Variables and inner functions defined inside a function are confined to that function's scope and cannot be accessed from outside.
function func1() { var a = 1; return a; }
func1(); // 1
console.log(a); // ReferenceErrorFunction Declaration
Function declarations are hoisted, allowing them to be called before their definition.
console.log(add(10, 10)); // 20
function add(a, b) { return a + b; }Function Expression
Function expressions are not hoisted; the variable is hoisted but its assignment is not.
console.log(add(10, 10)); // TypeError
var add = function(a, b) { return a + b; };Combined Declaration and Expression
test(); // calls function declaration
var test = function() { console.log('expression'); };
function test() { console.log('declaration'); }
test(); // calls expressionBlock Scope
ES6 introduced block scope with let and const. Variables declared with let inside a block are not accessible outside.
{ var a = 1; let b = 2; }
console.log(a); // 1
console.log(b); // ReferenceError lethas a "temporal dead zone" where the variable cannot be accessed before its declaration.
{ console.log(a); // ReferenceError
let a = 1; }Temporal dead zone: a variable is unaccessible from the start of its block until its declaration line.
Const
constbehaves like let regarding scope and the temporal dead zone, but it must be initialized and cannot be reassigned.
const a = 1;
a = 2; // TypeErrorObjects assigned to const can have their properties mutated unless frozen.
const obj = { a: 1, b: 2 };
obj.b = 3; // allowed
Object.freeze(obj); // now properties cannot changevar / 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.
eval('2 + 2'); // 4
eval("console.log('hi')"); // hi
eval(new String('2 + 2')); // String{'2 + 2'}Eval and Scope
Direct eval runs in the current scope; indirect calls (e.g., window.eval) run in the global scope.
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)Eval and Variable Hoisting
Variables and functions created inside eval are not hoisted; they are created only when eval runs.
// 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); // ReferenceErrorScope 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.
var c = 1;
function func() {
var b = 2;
function add(a) { return a + b + c; }
return add;
}
const addTest = func();
addTest(3); // 6Closure
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.
var c = 1;
function func() {
var b = 2;
function add(a) { return a + b + c; }
return add;
}
const addTest = func();
addTest(3); // 6When using closures, the original function remains in memory, which can increase memory usage and potentially cause leaks.
Closure Usage
Callbacks
In a for loop with let, each iteration creates a new block-scoped variable, so asynchronous callbacks capture the correct value.
for (let i = 0; i < 5; i++) {
setTimeout(function(){ console.log(i); }, 0);
}
// outputs: 0 1 2 3 4Modularization
Modules can be built by returning an object that exposes internal functions, which form closures over the module's private state.
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]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"
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.
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.
