|$ curl https://forge-ai.dev/api/markdown?path=docs/js/variables
$cat docs/javascript-—-variables-&-scoping.md
updated This week·30 min read·published

JavaScript — Variables & Scoping

JavaScriptVariablesScopeBeginnerBeginner to Advanced
Introduction

Variables are the fundamental building blocks of any JavaScript program. They store data values, reference objects, and hold state throughout the lifecycle of an application. Understanding how variables work — their declaration, assignment rules, scoping behavior, and hoisting mechanics — is essential for writing predictable, bug-free code.

JavaScript provides three keywords for declaring variables: var, let, and const. Each has distinct rules about scope, reassignment, redeclaration, and hoisting. Modern JavaScript strongly favors const by default and let only when reassignment is necessary. The legacy var should be avoided in new code.

In addition to declaration mechanics, JavaScript's scoping rules determine where variables are accessible — from the global scope down to function and block scopes. The scope chain, lexical scoping, and closures build on these foundational rules. This page covers all aspects of variables and scoping in depth.

declaration-overview.js
JavaScript
1// Three ways to declare a variable
2var legacy = "function-scoped, hoisted, avoid me";
3let mutable = "block-scoped, reassignable";
4const immutable = "block-scoped, cannot reassign";
5
6// The modern rule:
7// Use const by default, let when you need reassignment
8// Never use var in new code
var vs let vs const

The choice between var, let, and const determines how a variable behaves across scope, hoisting, and reassignment. Understanding these differences is critical for writing correct JavaScript.

Featurevarletconst
ScopeFunctionBlockBlock
Reassignable
Redeclarable
Hoisted✓ (undefined)✓ (TDZ)✓ (TDZ)
Global property✓ (window.varName)
var-let-const.js
JavaScript
1// const — cannot be reassigned, but object properties can mutate
2const PI = 3.14159;
3PI = 3; // TypeError: Assignment to constant variable
4
5const user = { name: "Alice" };
6user.name = "Bob"; // OK — mutating property, not reassigning
7user = {}; // TypeError: reassigning const reference
8
9const array = [1, 2, 3];
10array.push(4); // OK — mutation allowed
11array = []; // TypeError
12
13// let — block-scoped and reassignable
14let count = 0;
15count = 1; // OK
16count = 2; // OK
17let count = 3; // SyntaxError: redeclaration
18
19// var — function-scoped, hoisted, redeclarable
20var x = 10;
21var x = 20; // OK — redeclaration works (no error)
22console.log(x); // 20
23
24// var ignores block scope
25if (true) {
26 var leaked = "I escape!";
27}
28console.log(leaked); // "I escape!" — var is function-scoped, not block-scoped
29
30// let/const respect block scope
31if (true) {
32 let blocked = "I stay in the block";
33 const alsoBlocked = "Me too";
34}
35console.log(blocked); // ReferenceError

best practice

Default to const. Only use let when you know the variable needs to be reassigned. Never use var in modern JavaScript — it has confusing scoping rules, allows redeclaration, and creates global properties. const also signals intent: readers know this binding will not change.
Global, Function & Block Scope

Scope determines where a variable is accessible in your code. JavaScript has three levels of scope: global (accessible everywhere), function (accessible within a function), and block (accessible within a { } block). The interplay between these scopes forms the foundation of JavaScript's lexical scoping model.

Global Scope

Variables declared outside any function or block are in the global scope. They are accessible from anywhere in the program. In browsers, global var declarations become properties of the window object, while global let and const do not.

global-scope.js
JavaScript
1// Global scope — accessible everywhere
2const globalConst = "global const";
3let globalLet = "global let";
4var globalVar = "global var"; // becomes window.globalVar in browsers
5
6function testScope() {
7 console.log(globalConst); // "global const"
8 console.log(globalLet); // "global let"
9 console.log(globalVar); // "global var"
10}
11
12// var creates a property on the global object (window in browsers)
13console.log(window.globalVar); // "global var" (var only)
14console.log(window.globalLet); // undefined (let does not)
15console.log(window.globalConst); // undefined (const does not)
16
17// Avoid creating globals implicitly
18function bad() {
19 implicitGlobal = "I'm global!"; // no declaration keyword!
20}
21bad();
22console.log(implicitGlobal); // "I'm global!" — accidental global

Function Scope

Variables declared with var inside a function are scoped to that function. They are accessible anywhere within the function, regardless of block nesting. let and const also respect function scope but are additionally constrained by block scope within the function.

function-scope.js
JavaScript
1function scopeDemo() {
2 var functionScoped = "accessible everywhere in this function";
3 let blockScoped = "only in this block or child blocks";
4 const alsoBlockScoped = "same as let";
5
6 if (true) {
7 var stillAccessible = "var ignores blocks";
8 let blockOnly = "trapped in this if block";
9 const alsoBlockOnly = "trapped too";
10
11 console.log(functionScoped); // OK — function scope
12 console.log(blockScoped); // OK — parent scope
13 console.log(stillAccessible); // OK — same function scope
14 }
15
16 console.log(stillAccessible); // "I escape!" — var ignores blocks
17 console.log(blockOnly); // ReferenceError — block-scoped
18 console.log(alsoBlockOnly); // ReferenceError — block-scoped
19}
20
21scopeDemo();
22console.log(functionScoped); // ReferenceError — function scope

Block Scope

Block scope was introduced in ES6 with let and const. A block is defined by a pair of curly braces { } — this includes if, for, while, switch, and bare blocks. Variables declared with let or const are confined to the block in which they are defined.

block-scope.js
JavaScript
1// Bare blocks create scope for let/const
2{
3 let a = 10;
4 const b = 20;
5 var c = 30; // var escapes!
6}
7console.log(c); // 30
8console.log(a); // ReferenceError
9console.log(b); // ReferenceError
10
11// Block scope in loops
12for (let i = 0; i < 3; i++) {
13 // Each iteration gets its own 'i'
14 setTimeout(() => console.log(i), 100); // 0, 1, 2
15}
16
17for (var j = 0; j < 3; j++) {
18 // Only one 'j' — shared across all iterations
19 setTimeout(() => console.log(j), 100); // 3, 3, 3
20}
21
22// Block scope in switch
23const value = "b";
24switch (value) {
25 case "a": {
26 let result = "Case A";
27 console.log(result);
28 break;
29 }
30 case "b": {
31 let result = "Case B"; // Different scope — no conflict
32 console.log(result);
33 break;
34 }
35}

info

The per-iteration binding of let in for loops is one of the most practical benefits of block scope. With var, you need an IIFE to capture each iteration's value. With let, it works naturally. This is why closures in loops work correctly with let.
Hoisting Mechanics

Hoisting is JavaScript's behavior of moving declarations to the top of their containing scope during compilation. The key distinction is how each declaration type is hoisted: var is hoisted and initialized to undefined, while let and const are hoisted but not initialized — they enter the Temporal Dead Zone.

DeclarationHoisted?Initial ValueAccessible Before Declaration?
varundefined✓ (returns undefined)
letuninitialized (TDZ)✗ (ReferenceError)
constuninitialized (TDZ)✗ (ReferenceError)
functionthe function itself✓ (full hoisting)
classuninitialized (TDZ)✗ (ReferenceError)
hoisting.js
JavaScript
1// var hoisting — declaration moves up, value stays
2console.log(a); // undefined (not ReferenceError!)
3var a = 5;
4// The above is interpreted as:
5// var a;
6// console.log(a);
7// a = 5;
8
9// let/const hoisting with Temporal Dead Zone
10console.log(b); // ReferenceError: Cannot access 'b' before initialization
11let b = 10;
12
13console.log(c); // ReferenceError
14const c = 15;
15
16// Function declarations are fully hoisted
17sayHello(); // "Hello!" — function is fully hoisted
18function sayHello() {
19 console.log("Hello!");
20}
21
22// Function expressions are NOT hoisted (only the var/let/const)
23sayGoodbye(); // TypeError: sayGoodbye is not a function
24var sayGoodbye = function() {
25 console.log("Goodbye!");
26};
27
28// Arrow function expressions — also not hoisted
29greet(); // ReferenceError
30const greet = () => console.log("Hi");

warning

Function expressions (both named and anonymous) assigned to var, let, or const follow the hoisting rules of the declaration keyword, not the function hoisting rules. Only function declarations (using the function keyword at the statement level) are fully hoisted with their body.
Temporal Dead Zone (TDZ)

The Temporal Dead Zone is the period between entering a scope and the actual declaration of a let or const variable. During this time, the variable exists in the scope but cannot be accessed — any attempt throws a ReferenceError. The TDZ is a deliberate design improvement over var's silent undefined behavior, catching potential bugs at runtime.

tdz.js
JavaScript
1// TDZ demonstration
2{
3 // Enter block — TDZ starts for 'x'
4 // Any reference to 'x' here throws ReferenceError
5
6 function logX() {
7 console.log(x); // ReferenceError if called before declaration
8 }
9
10 // TDZ ends here:
11 let x = 42;
12 // Safe to use 'x' after this point
13 console.log(x); // 42
14}
15
16// TDZ with typeof — unexpected behavior
17console.log(typeof undeclaredVar); // "undefined" (not an error)
18console.log(typeof tdzVar); // ReferenceError!
19let tdzVar = "hello";
20
21// TDZ in conditional — the variable IS in scope but uninitialized
22if (true) {
23 // 'val' is in scope here (TDZ)
24 function test() {
25 console.log(val); // ReferenceError if called
26 }
27 // test(); // Would throw
28 let val = "defined";
29 test(); // Would work here (after TDZ ends)
30}
31
32// TDZ with default parameters
33function foo(a = b, b = 5) {
34 return a;
35}
36foo(); // ReferenceError: Cannot access 'b' before initialization
37// TDZ applies to the parameter scope — 'b' is not yet initialized
🔥

pro tip

The TDZ prevents a common class of bugs where you accidentally reference a variable before it is defined. While var silently returns undefined, let and const throw a clear error. This makes it easier to spot logic errors where the order of declarations matters.
Scope Chain & Lexical Scoping

JavaScript uses lexical scoping (also called static scoping), meaning the scope of a variable is determined by its location in the source code at author-time, not at runtime. When a variable is referenced, JavaScript traverses the scope chain — starting from the innermost scope and moving outward — until it finds the variable or reaches the global scope and throws a ReferenceError.

scope-chain.js
JavaScript
1// Lexical scope is determined by where code is written
2const global = "global";
3
4function outer() {
5 const outerVar = "outer";
6
7 function inner() {
8 const innerVar = "inner";
9
10 // Scope chain: inner → outer → global
11 console.log(innerVar); // "inner" — found in current scope
12 console.log(outerVar); // "outer" — found in outer's scope
13 console.log(global); // "global" — found in global scope
14 }
15
16 inner();
17}
18
19outer();
20
21// The scope chain is fixed at author-time, not call-time
22const x = "global x";
23
24function test() {
25 console.log(x); // "global x" — accesses the global 'x'
26}
27
28function run() {
29 const x = "local x";
30 test(); // Still prints "global x" — lexical scope!
31}
32
33run();
34
35// Variable shadowing — inner scope "shadows" outer
36const value = "outer";
37function shadow() {
38 const value = "inner"; // shadows the outer 'value'
39 console.log(value); // "inner"
40}
41shadow();
42console.log(value); // "outer" — outer is unchanged
Scope LevelWhere CreatedAccessible From
GlobalOutside any function or blockEverywhere in the program
ModuleTop-level of ES moduleWithin the module (not leaked to global)
FunctionInside a function bodyInside the function and nested scopes
BlockInside { } (if, for, while, etc.)Within that block only (let/const)

best practice

Avoid variable shadowing — it creates confusion and subtle bugs. A good linter (ESLint) with the no-shadow rule catches accidental shadowing. When you intentionally shadow, consider renaming the inner variable to be more specific.
IIFE for Scope Isolation

An Immediately Invoked Function Expression (IIFE, pronounced "iffy") is a function that is defined and executed immediately. IIFEs create a new function scope, which was the primary mechanism for creating private variables and avoiding global scope pollution before let and const introduced block scope. They remain useful in certain patterns.

iife.js
JavaScript
1// Basic IIFE syntax
2(function() {
3 const privateVar = "This is private";
4 console.log("IIFE executed!");
5})();
6// privateVar is not accessible here
7
8// IIFE with parameters
9(function(name, age) {
10 console.log(`${name} is ${age} years old`);
11})("Alice", 30);
12
13// IIFE with return value
14const result = (function(a, b) {
15 return a + b;
16})(5, 3);
17console.log(result); // 8
18
19// IIFE for creating private state (module pattern)
20const counter = (function() {
21 let count = 0; // private variable
22
23 return {
24 increment() { return ++count; },
25 decrement() { return --count; },
26 getCount() { return count; },
27 };
28})();
29
30console.log(counter.increment()); // 1
31console.log(counter.increment()); // 2
32console.log(counter.getCount()); // 2
33console.log(counter.count); // undefined — private!
34
35// IIFE with arrow function
36(() => {
37 const msg = "Arrow IIFE";
38 console.log(msg);
39})();
40
41// IIFE for loop capture (pre-ES6 pattern — now use 'let')
42for (var i = 0; i < 3; i++) {
43 (function(index) {
44 setTimeout(() => console.log(index), 100);
45 })(i);
46}
47// Without IIFE: would log 3, 3, 3
48// With IIFE: logs 0, 1, 2
🔥

pro tip

IIFEs are less necessary in modern JavaScript because let and const provide block scoping, and ES modules provide module-level scope. However, IIFEs are still useful for the module pattern, creating private state in non-module scripts, and in older codebases where ES6+ features are not available.
Variable Naming Conventions

Consistent naming conventions make code more readable and maintainable. JavaScript does not enforce naming rules (beyond valid identifier requirements), but the community follows well-established conventions.

ConventionPatternExamplesUsed For
camelCaselowercaseFirstWorduserName, fetchData, isActiveVariables, functions, methods, parameters
PascalCaseEachWordCapitalizedUserProfile, ApiClient, AppConfigClasses, constructors, React components, types
UPPER_SNAKE_CASEALL_CAPS_UNDERSCORESMAX_RETRIES, API_BASE_URL, DEFAULT_TIMEOUTTrue constants (compile-time), environment variables
_underscorePrefix_leadingUnderscore_privateField, _internalFn, _cachePrivate/internal (convention only, no enforcement)
$dollarPrefix$leadingDollar$el, $container, $btnDOM element references (jQuery, or convention)
naming.js
JavaScript
1// Valid identifiers — letters, digits, $, _ (cannot start with digit)
2const validName = "ok";
3const $dollar = "also ok";
4const _underscore = "fine";
5const camelCase = "standard";
6const PascalCase = "classes and components";
7const UPPER_SNAKE = "constants";
8
9// Invalid identifiers
10// const 123num = "no"; // SyntaxError: starts with digit
11// const my-var = "no"; // SyntaxError: hyphen not allowed
12// const const = "no"; // SyntaxError: reserved keyword
13
14// Reserved keywords cannot be used as identifiers
15// await, break, case, catch, class, const, continue, debugger
16// default, delete, do, else, enum, export, extends, false
17// finally, for, function, if, import, in, instanceof, let
18// new, null, of, return, super, switch, this, throw, true
19// try, typeof, var, void, while, with, yield
20
21// Meaningful naming examples
22// Bad:
23const d = new Date();
24const u = "Alice";
25const f = (a, b) => a + b;
26
27// Good:
28const currentDate = new Date();
29const userName = "Alice";
30const sum = (a, b) => a + b;
31
32// Boolean naming — use is/has/should prefixes
33const isActive = true;
34const hasPermission = false;
35const shouldUpdate = true;
36const isLoading = false;
37
38// Array naming — plural or List/Array suffix
39const users = ["Alice", "Bob"];
40const fruitList = ["apple", "banana"];
41const itemArray = [1, 2, 3];

best practice

Names should reveal intent. A variable name like d or tmp forces readers to infer meaning from context, while currentDate or temporaryBuffer is immediately clear. Spend time on naming — it is one of the highest-leverage activities for code quality.
Variable Best Practices

Essential Guidelines

Default to const — only use let when reassignment is necessary, never use var in new code
Declare variables at the top of their scope — improves readability and avoids TDZ surprises
Use one variable per declaration — const a = 1, b = 2 is harder to read and refactor
Initialize variables at declaration — let count = 0 is clearer than let count; ... count = 0
Avoid global variables — they create naming conflicts and make code harder to reason about
Use descriptive, intention-revealing names — names should answer 'what does this hold?'
Keep variable scope as narrow as possible — minimize the window where a variable is accessible
Use UPPER_SNAKE_CASE for true constants (values known at compile time, like PI or MAX_SIZE)
Never rely on implicit globals — always declare with const, let, or the module export system
Use consistent naming conventions across your project — enforce with ESLint camelcase rule

Common Pitfalls

✗ Accidentally Creating Globals

function set() { total = 0; } // total becomes global!

Without strict mode, assignment to an undeclared variable creates a global. Always use "use strict" or ES modules.

✗ Confusing var Hoisting

console.log(x); var x = 5; // undefined, not error

Use let and const so hoisting violations throw errors instead of silently returning undefined.

✗ Block Scope Confusion with var

if (true) { var x = 5; } console.log(x); // 5 — var ignores blocks

Use let or const when you want block-scoped variables.

✗ Const ≠ Immutable

const arr = [1]; arr.push(2); // Works — const prevents reassignment, not mutation

const prevents reassignment of the binding, not mutation of the value. Use Object.freeze() for shallow immutability.

danger

Implicit globals are one of the most common and insidious JavaScript bugs. A single missing declaration keyword creates a global variable that can be modified from anywhere. Always enable strict mode ("use strict" or use ES modules) — it turns implicit global creation into a ReferenceError.
$Blueprint — Engineering Documentation·Section ID: JS-VARS·Revision: 1.0