JavaScript Variables Complete Master Guide
JavaScript Variables: Complete Overview
JavaScript has four ways to introduce a variable into your code: var, let, const, and the accidental global (declaring without any keyword). The practical rule for new code is short: use const by default, switch to let when you need to reassign, and avoid var entirely. But understanding why requires knowing what each one does, how they differ in scope and hoisting behavior, and what problems the older approaches caused that led to let and const being added in ES6.
The Four Variable Types in JavaScript
var- Function-scoped, hoisted and initialized to undefined, can be redeclared. The original declaration keyword - avoid in new code.let- Block-scoped, hoisted but not initialized (Temporal Dead Zone), cannot be redeclared. Use when value will change.const- Block-scoped, hoisted but not initialized, cannot be redeclared or reassigned. Use by default.Global (no keyword)- Assigning without a declaration keyword creates a global variable. Accessible everywhere, causes bugs that are hard to find - never do this intentionally.
var: The Original Variable Declaration
Var is function-scoped rather than block-scoped, which means a var declared inside an if statement or for loop is accessible anywhere in the containing function - not just inside the block where it was written. It's also hoisted and initialized to undefined, so you can access a var variable before the line where it's declared without getting an error. And it can be redeclared without complaint. All three of these properties were eventually recognized as making code harder to reason about, which is why let and const were added. The classic demonstration of var's surprising behavior is closures in loops - var's function scope means all iterations of a for loop share the same variable.
1// var is function-scoped, not block-scoped
2function varExample() {
3 if (true) {
4 var insideBlock = "accessible anywhere in this function";
5 }
6 console.log(insideBlock); // Works - var ignores block boundaries
7}
8
9// Hoisting: var declarations are moved to top of function, initialized to undefined
10console.log(hoistedVar); // undefined (not a ReferenceError)
11var hoistedVar = "I was hoisted";
12
13// Redeclaration is allowed - no warning
14var counter = 5;
15var counter = 10; // No error
16
17// The closure-in-loop problem
18for (var i = 0; i < 3; i++) {
19 setTimeout(() => console.log(i), 100); // Logs: 3, 3, 3
20 // All three callbacks share the same 'i' (function-scoped)
21}
22
23// The same loop with let - each iteration gets its own binding
24for (let i = 0; i < 3; i++) {
25 setTimeout(() => console.log(i), 100); // Logs: 0, 1, 2
26}var Key Characteristics
Scope- Function-scoped. A var declared inside an if, for, or while block is accessible throughout the entire containing function.Hoisting- Fully hoisted and initialized to undefined. Can be read before its declaration line without an error.Redeclaration- Allowed. You can declare the same var name multiple times in the same scope with no warning.Use case- Maintaining legacy code only. Never use var in new code.
let: Modern Block-Scoped Variables
Let was introduced in ES6 to fix var's scoping problems. It's block-scoped, meaning a let variable only exists within the curly braces it was declared in. It cannot be redeclared in the same scope - trying to declare the same let name twice is a syntax error. It is hoisted but not initialized, which means accessing it before its declaration line throws a ReferenceError rather than silently returning undefined the way var does. This behavior - the Temporal Dead Zone - makes it easier to catch the class of bug where a variable is used before it's ready.
1// Block scoping - let stays inside its block
2function letExample() {
3 if (true) {
4 let blockScoped = "only inside this if block";
5 console.log(blockScoped); // Works
6 }
7 // console.log(blockScoped); // ReferenceError
8}
9
10// No redeclaration
11let uniqueVar = "first";
12// let uniqueVar = "second"; // SyntaxError: Identifier already declared
13
14// Reassignment is fine
15let counter = 0;
16counter = 1;
17counter++;
18counter += 5;
19
20// Temporal Dead Zone - accessing before declaration throws
21{
22 // console.log(tdzLet); // ReferenceError: Cannot access before initialization
23 let tdzLet = "now initialized";
24 console.log(tdzLet); // Works
25}
26
27// The loop closure problem solved
28for (let i = 0; i < 3; i++) {
29 setTimeout(() => console.log(i), 100); // 0, 1, 2 - each iteration has own binding
30}let Key Characteristics
Scope- Block-scoped. Only accessible within the block it's declared in.Hoisting- Hoisted but not initialized. Accessing before declaration throws ReferenceError (Temporal Dead Zone).Redeclaration- Not allowed. Declaring the same name twice in the same scope is a SyntaxError.Use case- Any variable whose value needs to change after initial assignment.
const: Block-Scoped Constants
Const is block-scoped like let, cannot be redeclared, and additionally cannot be reassigned - trying to assign a new value to a const throws a TypeError. It must be initialized at the point of declaration. One thing that trips people up: const prevents reassignment of the variable binding, not mutation of the value. If the value is an object or array, you can still add, change, and remove properties or elements - you just can't replace the whole object. Object.freeze creates truly immutable objects, though only one level deep.
1// Basic const - must be initialized, cannot be reassigned
2const PI = 3.14159;
3const API_KEY = "abc123def456";
4// const UNINIT; // SyntaxError: Missing initializer in const declaration
5
6// Reassignment throws TypeError
7const TAX_RATE = 0.07;
8// TAX_RATE = 0.08; // TypeError: Assignment to constant variable
9
10// But const objects can be mutated
11const user = { name: "John", age: 30 };
12user.age = 31; // Fine - modifying a property
13user.city = "Boston"; // Fine - adding a property
14// user = { name: "Jane" }; // TypeError - reassigning the binding
15
16const colors = ["red", "green"];
17colors.push("blue"); // Fine
18colors[0] = "orange"; // Fine
19// colors = []; // TypeError
20
21// Object.freeze for actual immutability
22const CONFIG = Object.freeze({
23 API_URL: "https://api.example.com",
24 TIMEOUT: 5000
25});
26// CONFIG.API_URL = "other"; // Silently fails (throws in strict mode)
27// Note: freeze is shallow - nested objects can still be mutated
28
29// const in for...of: new binding per iteration
30const numbers = [1, 2, 3];
31for (const num of numbers) {
32 console.log(num); // 1, 2, 3
33 // Each iteration gets its own const binding
34}Global Variables: Implicit Declarations
A global variable created without a declaration keyword happens when you write a name on the left side of an assignment without var, let, or const. The variable silently becomes a property of the global object (window in browsers, global in Node.js). This is almost always accidental - a typo in a variable name, a missing declaration - and the resulting bugs are genuinely unpleasant to find because nothing warns you and the variable is readable from anywhere. Strict mode prevents this by throwing a ReferenceError when you assign to an undeclared name, which is one good reason to enable it.
1// Implicit global - happens silently without strict mode
2function createGlobal() {
3 accidentalGlobal = "I'm now a global"; // No declaration keyword
4}
5createGlobal();
6console.log(accidentalGlobal); // Works - pollutes global scope
7
8// Strict mode catches this
9function strictExample() {
10 "use strict";
11 // undeclaredVar = "error"; // ReferenceError: variable is not defined
12 const properVar = "correct"; // Correct
13}
14
15// Explicit globals - declared outside any function
16var globalVar = "accessible everywhere";
17let globalLet = "also accessible everywhere";
18const GLOBAL_CONST = "global constant";
19
20// Attaching to global object
21window.browserGlobal = "explicitly on window"; // Browser
22
23// Naming conflicts when globals are used carelessly
24var commonName = "original";
25
26function library1() {
27 commonName = "changed"; // Overwrites global - no warning
28}
29
30// Namespace pattern to reduce global pollution
31const MY_APP = MY_APP || {};
32MY_APP.config = { apiUrl: "https://api.example.com" };
33MY_APP.utils = {};
34
35// IIFE to avoid globals entirely in a block of code
36(function() {
37 const privateVar = "not global";
38 // Everything here stays local
39})();Variable Types Side-by-Side
The main differences in a single comparison: scope, hoisting behavior, redeclaration, and reassignment. The comparison also shows why each option exists and when - if ever - you'd reach for it.
1// SCOPE
2function scopeDemo() {
3 if (true) {
4 var funcScoped = "var: accessible in whole function";
5 let blockLet = "let: only this block";
6 const blockConst = "const: only this block";
7 }
8 console.log(funcScoped); // Works
9 // console.log(blockLet); // ReferenceError
10 // console.log(blockConst); // ReferenceError
11}
12
13// HOISTING
14console.log(varH); // undefined (hoisted, initialized to undefined)
15// console.log(letH); // ReferenceError (Temporal Dead Zone)
16// console.log(constH); // ReferenceError (Temporal Dead Zone)
17
18var varH = "var";
19let letH = "let";
20const constH = "const";
21
22// REDECLARATION
23var canRedeclare = "first";
24var canRedeclare = "second"; // Fine
25
26let noRedeclareLet = "first";
27// let noRedeclareLet = "second"; // SyntaxError
28
29const noRedeclareConst = "first";
30// const noRedeclareConst = "second"; // SyntaxError
31
32// REASSIGNMENT
33var varVal = 1; varVal = 2; // Fine
34let letVal = 1; letVal = 2; // Fine
35const constVal = 1;
36// constVal = 2; // TypeErrorVariable Types Comparison
Scope- var: function | let: block | const: block | global (implicit): everywhereHoisting- var: yes, initialized to undefined | let: yes, TDZ | const: yes, TDZ | global: noRedeclaration- var: allowed | let: not allowed | const: not allowed | global: allowedReassignment- var: allowed | let: allowed | const: not allowed | global: allowedInitialization required- var: no | let: no | const: yes | global: yes (implicit at first assignment)Use case- var: legacy code only | let: value will change | const: default choice | global: avoid entirely
How the Variable Types Relate
Let and const aren't just alternatives to var - they were added specifically to solve problems var caused. Understanding the evolution helps explain why the rules are what they are. Block scoping is the fundamental change: var's function scope meant that variables leaked outside the blocks they were written in, making it easy to accidentally reuse loop counter names or access variables that were only meant to exist within a single branch. The Temporal Dead Zone is a deliberate safety feature - by making it an error to access let and const before their declaration, JavaScript ensures you can't silently get undefined where you expected a real value.
1// The evolution: why let and const exist
2// var's scope leak problem
3function varProblem() {
4 for (var i = 0; i < 3; i++) {
5 // i is function-scoped, leaks out of the loop
6 }
7 console.log(i); // 3 - i is still alive here
8}
9
10// let solves it
11function letSolution() {
12 for (let i = 0; i < 3; i++) {
13 // i is block-scoped to the for loop
14 }
15 // console.log(i); // ReferenceError - i gone after loop
16}
17
18// Scope chain: all outer variables accessible from inner scopes
19const globalConst = "global";
20
21function outer() {
22 const outerConst = "outer";
23
24 function inner() {
25 const innerConst = "inner";
26
27 console.log(globalConst); // Accessible
28 console.log(outerConst); // Accessible (closure)
29 console.log(innerConst); // Accessible
30 }
31
32 inner();
33 // console.log(innerConst); // ReferenceError
34}
35
36// Migration: replacing var with appropriate const/let
37// Before
38var counter = 0;
39var user = { name: "John" };
40var isActive = true;
41
42// After - each gets the right keyword
43let count = 0; // will be reassigned
44const userObj = { name: "John" }; // reference won't change
45const active = true; // won't change
46
47// TDZ as a safety feature: catches use-before-initialization bugs
48{
49 // console.log(ready); // ReferenceError - caught early
50 const ready = true;
51 console.log(ready); // true
52}Migration Patterns and Best Practices
When updating legacy code from var to const/let, the process is mechanical: for each var, decide whether the value is ever reassigned. If not, it becomes const. If yes, it becomes let. Block scope changes from var to let/const can expose bugs that were previously hidden - a variable might have been accessible somewhere it shouldn't have been. The loop closure problem, where callbacks inside a for loop all capture the same var variable, is fixed by switching to let. For global variable problems in larger codebases, the namespace pattern or module pattern contain the damage until a proper module system is in place.
1// Migration process: var → const or let
2// Step 1: Find all vars
3// Step 2: Check if each is ever reassigned
4// Step 3: const if not reassigned, let if it is
5
6// Before
7var apiUrl = "https://api.example.com";
8var timeout = 5000;
9var currentUser = null; // gets reassigned
10var requestCount = 0; // gets incremented
11
12// After
13const API_URL = "https://api.example.com"; // never changes
14const TIMEOUT = 5000; // never changes
15let currentUser = null; // will be reassigned
16let requestCount = 0; // will be incremented
17
18// Loop closure: var → let
19// Before (broken)
20for (var i = 0; i < 3; i++) {
21 setTimeout(() => console.log(i), 100); // 3, 3, 3
22}
23
24// After (fixed)
25for (let i = 0; i < 3; i++) {
26 setTimeout(() => console.log(i), 100); // 0, 1, 2
27}
28
29// Global containment with module pattern
30// Before: scattered globals
31var globalData = {};
32var globalConfig = {};
33function init() { globalData.users = []; }
34
35// After: contained in a module
36const App = (function() {
37 const data = {}; // private
38 const config = {}; // private
39
40 return {
41 init() { data.users = []; },
42 getUsers() { return data.users; }
43 };
44})();
45
46// Best practices in one place
47const API_BASE = "https://api.example.com"; // const by default
48let isLoading = false; // let for reassignables
49// var oldStyle = "avoid"; // avoid
50// implicitGlobal = "never"; // never
51
52// Declare close to where it's used
53function processItems(items) {
54 const MAX_BATCH = 10;
55
56 for (let i = 0; i < items.length; i += MAX_BATCH) {
57 const batch = items.slice(i, i + MAX_BATCH);
58 console.log(`Processing batch of ${batch.length}`);
59 }
60}