Frontend Development 18 min read

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.

WeDoctor Frontend Technology
WeDoctor Frontend Technology
WeDoctor Frontend Technology
Master JavaScript Scopes, Hoisting, and Closures: A Deep Dive

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.

<code>window.innerHeight</code>

Variables declared with

var

become 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

var

are 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

let

and

const

. Variables declared with

let

inside a block are not accessible outside.

<code>{ var a = 1; let b = 2; }
console.log(a); // 1
console.log(b); // ReferenceError</code>
let

has 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

const

behaves like

let

regarding 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

const

can 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

eval

executes 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

eval

runs 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

eval

are not hoisted; they are created only when

eval

runs.

<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

for

loop 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"

frontendJavaScriptClosureEvalScopehoisting
WeDoctor Frontend Technology
Written by

WeDoctor Frontend Technology

Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.

0 followers
Reader feedback

How this landed with the community

login 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.