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.

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

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

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

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

javascript
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;               // TypeError

Variable Types Comparison

  • Scope - var: function | let: block | const: block | global (implicit): everywhere
  • Hoisting - var: yes, initialized to undefined | let: yes, TDZ | const: yes, TDZ | global: no
  • Redeclaration - var: allowed | let: not allowed | const: not allowed | global: allowed
  • Reassignment - var: allowed | let: allowed | const: not allowed | global: allowed
  • Initialization 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.

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

javascript
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}

JavaScript Variables Comprehensive FAQ

When should I use var vs let vs const?

Use const by default for everything. Switch to let when you have a specific reason to reassign. Don't use var in new code - it's only worth knowing about when reading or maintaining old codebases. This rule is simple enough that if you find yourself reaching for var in new code, that's a signal to stop and reconsider.

What's the main difference between function scope and block scope?

Function scope means the variable exists throughout the entire function, regardless of what block it was declared in. Block scope means the variable only exists within the curly braces it was declared in. The practical consequence: a var declared inside an if statement is accessible after the if block closes, while a let or const in the same position is not.

Why are global variables considered bad practice?

Because they're accessible and modifiable from anywhere, including code you didn't write, third-party libraries, and any future code added to the project. Name collisions - two pieces of code using the same global name - cause bugs that are hard to trace because both pieces of code may be correct in isolation. Globals also make functions harder to test since the function's behavior can depend on state that's not visible in its parameters.

Can I make objects and arrays truly immutable with const?

No. Const prevents you from reassigning the variable to a new value, but the object or array it points to can still be modified. Person.name = 'new name' works fine even if person is const. Object.freeze creates shallow immutability - the top-level properties can't be changed, but nested objects can still be mutated. For truly deep immutability you need a recursive freeze or a library like Immer.

What is the Temporal Dead Zone?

The TDZ is the period between when a let or const variable's scope starts and when its declaration line is reached. The variable is technically hoisted - the engine knows it exists - but it's not initialized, so accessing it throws a ReferenceError. This is different from var, which is hoisted and initialized to undefined, allowing silent access before declaration. The TDZ exists to catch the category of bug where you use a variable before setting it up.

How do I migrate legacy var code to let/const?

Go through each var declaration and check whether the value is ever reassigned after initialization. If not, change it to const. If yes, change it to let. The trickier part is checking for scope issues - a var that was accessible outside the block it was written in will become inaccessible when changed to let or const, which is usually correct behavior but occasionally exposes a bug that was hiding in the old code.

What happens if I mix different variable types?

They coexist fine in the same codebase or file. The scoping rules apply per-declaration: var is function-scoped wherever it appears, let and const are block-scoped wherever they appear. The only constraint is within the same scope: you can't redeclare the same name with let or const, and you can't have a var and a let or const with the same name in the same scope.

Are there performance differences between var, let, and const?

Negligible in practice. Modern JavaScript engines optimize all three effectively. The choice between them should be based on code clarity and correctness, not performance. If you're profiling a performance problem, variable declaration type is not where to look.