Chapter 4.1โ˜• 18 min read

var: Hoisting, Function Scope & Variable Object

V8 code ko do baar padhta hai โ€” pehle scan, phir run. Yahi hoisting ka sach hai.

0101 โ€” The Two-Pass Model: Creation vs Execution Phase

V8 does NOT execute your code line by line in one pass. It processes every scope in two phases:

Creation Phase (before any code runs in a scope):

1. Scan the entire function body for var declarations
2. Register each var name in the Variable Object โ€” set to undefined
3. Scan for function declarations โ€” fully initialize (not just undefined!)
4. Set up scope chain and determine this binding

Execution Phase: run code top to bottom โ€” assignments happen NOW.

"Hoisting" = variable names are registered in Creation Phase before execution. Only the DECLARATION is hoisted โ€” the ASSIGNMENT stays where you wrote it.

// What you write:
console.log(x);         // undefined โ€” not ReferenceError!
console.log(greet);     // [Function: greet] โ€” fully available!
var x = 10;
console.log(x);         // 10 โ€” assignment executed here
function greet() { return 'Hi!'; }

// What V8 sees after Creation Phase:
// Variable Object: { x: undefined, greet: [Function] }

// Execution Phase runs this:
// console.log(x);         // reads VO โ†’ undefined
// console.log(greet);     // reads VO โ†’ [Function]
// x = 10;                 // ASSIGNMENT โ€” updates VO
// console.log(x);         // reads VO โ†’ 10
// (function greet already in VO โ€” declaration line is a no-op now)
In V8's C++ implementation, the Variable Object is called an "activation object" or "environment record". During parsing (which is the Creation Phase equivalent), V8 walks the AST and creates bindings for every var declaration and function declaration. These bindings exist in the scope before a single bytecode instruction executes.
0202 โ€” Function Declarations vs Expressions: Hoisting Difference

Function declaration (function foo() {}): FULLY hoisted โ€” name AND body available immediately.

Function expression (var foo = function() {}): only the VAR is hoisted โ€” to undefined. The function body is NOT.

Arrow function expression (var foo = () => {}): same as function expression โ€” var hoisted to undefined.

Named function expression: name only accessible INSIDE the function, not outside.

Why: function declarations are recognized by the parser and fully processed in Creation Phase. Expressions are assignments โ€” they happen in Execution Phase.

// Function DECLARATION โ€” fully hoisted
console.log(declared()); // 'I work!' โ€” called BEFORE the declaration line!
function declared() { return 'I work!'; }

// Function EXPRESSION โ€” only var hoisted (to undefined)
console.log(expression); // undefined โ€” var hoisted but no function yet
// console.log(expression()); // TypeError: expression is not a function!
var expression = function() { return 'I work too!'; };
console.log(expression()); // 'I work too!' โ€” now it's assigned

// Arrow function expression โ€” same as function expression
// console.log(arrow()); // TypeError: arrow is not a function!
var arrow = () => 'arrow works';
console.log(arrow()); // 'arrow works' โ€” assigned now

// Named function expression โ€” name only inside
var factorial = function fact(n) {
  return n <= 1 ? 1 : n * fact(n - 1); // fact accessible here
};
// console.log(fact); // ReferenceError โ€” not accessible outside
console.log(factorial(5)); // 120 โœ…
๐Ÿ“‹ Interview Answer:
The difference between function declaration and function expression: declarations are fully hoisted (name + body), expressions are not (var name hoisted to undefined, body assigned later). This changes whether you can call the function before its line in code.
0303 โ€” var's Function Scope: Leaking out of Blocks

var is scoped to the nearest FUNCTION โ€” not to the nearest block.

if, for, while, switch, try/catch blocks do NOT create a new scope for var.

var declared inside a block leaks out to the enclosing function (or global).

for loop with var: one var shared across all iterations (classic closure bug).

// var leaks out of blocks
if (true) {
  var leaked = 'I escaped the if block!';
}
console.log(leaked); // 'I escaped!' โ€” var is in function/global scope

// for loop var โ€” one variable, shared
for (var i = 0; i < 3; i++) {
  // i is in the FUNCTION scope, not loop scope
}
console.log(i); // 3 โ€” leaked out! loop is done but i survives

// try/catch with var
try {
  var result = 'success';
  throw new Error('oops');
} catch (err) {
  var status = 'error';  // leaked to function scope!
}
console.log(result); // 'success' โ€” even though it threw
console.log(status); // 'error' โ€” leaked from catch block

// switch โ€” vars share scope across all cases!
switch (true) {
  case true:
    var answer = 42;
    break;
  case false:
    var answer = 'not 42'; // re-declaration! NO error with var
    break;
}
console.log(answer); // 42 โ€” last assignment wins

// Contrast: var IS contained by function boundary
function containedVar() {
  var local = 'inside function';
}
// console.log(local); // ReferenceError โ€” function boundary IS a scope wall
V8's Variable Object for a function scope is a single flat object. Every var declaration in the function body โ€” regardless of which block it's inside โ€” goes into this one object during Creation Phase. Blocks are not scope boundaries for var. Only function boundaries create new Variable Objects.
0404 โ€” Variable Object in Depth: What V8 Actually Creates

Variable Object (VO): internal V8 structure for each function execution context.

Global VO: same as the global object (window/globalThis) โ€” var adds properties to it!

Function VO: created fresh per function call โ€” contains: arguments, params, var declarations, function declarations.

VO lookup: when reading a variable, V8 walks the scope chain through VOs until found.

Variable shadowing: inner function's VO has same name โ€” inner shadows outer.

// Global var โ†’ adds to global object!
var globalVar = 'I am global';
console.log(globalVar === window?.globalVar); // true in browser!
// (window.globalVar is now 'I am global')
// let/const do NOT do this

// Function VO contains all var and params
function example(a, b) {
  // VO after Creation Phase:
  // { a: 1, b: 2, local: undefined, inner: [Function] }
  console.log(local);  // undefined โ€” in VO but not assigned yet
  console.log(inner);  // [Function] โ€” function decl, fully in VO
  var local = 'assigned';
  console.log(local);  // 'assigned'
  function inner() { return 'I am inner'; }
}
example(1, 2);

// var re-declaration โ€” silent
var city = 'Hyderabad';
var city = 'Bangalore'; // No error! Second var is ignored in creation
city = 'Chennai';       // But assignment works โ€” overwrites
console.log(city);      // 'Chennai'

// Arguments object โ€” old way to access all args
function oldStyle() {
  console.log(arguments[0]); // first arg
  console.log(arguments.length);
  // arguments is in the VO โ€” part of every non-arrow function
}
oldStyle(1, 2, 3); // 1, 3
V8 implements scope chain as a linked list of environment records. When your code reads a variable, V8 starts at the current function's VO. If not found, it follows the outer reference to the parent's VO, and so on until the global VO. If still not found โ†’ ReferenceError. This chain is set up during Creation Phase, before any code executes.
0505 โ€” Practical Impact: When var Causes Real Bugs

Bug 1: Callbacks in loops โ€” all share the same var variable (classic closure bug).

Bug 2: Accidental global โ€” missing var/let/const creates global property (sloppy mode).

Bug 3: var in conditional blocks โ€” code that "shouldn't run" still hoists its var.

Bug 4: Re-declaration confusion โ€” no error but overwrites previous value.

Why var still matters: browser support, legacy code, interview questions, understanding older codebases. When to use var in 2024: basically never โ€” let and const exist.

// Bug 1: Closure in loop (classic var trap)
const fns = [];
for (var i = 0; i < 3; i++) {
  fns.push(function() { return i; }); // all capture same 'i' in VO
}
console.log(fns[0]()); // 3 โ€” not 0!
console.log(fns[1]()); // 3 โ€” not 1!
console.log(fns[2]()); // 3 โ€” the loop finished, i=3, all share it

// Fix: use let (new VO per iteration)
const fns2 = [];
for (let j = 0; j < 3; j++) {
  fns2.push(function() { return j; }); // each iteration has own j
}
console.log(fns2[0]()); // 0 โœ…
console.log(fns2[1]()); // 1 โœ…

// Bug 2: Accidental global (sloppy mode only)
function setupHandler() {
  handler = function() { console.log('clicked!'); }; // no var/let!
  // In sloppy mode: window.handler = function...
}
setupHandler();
// handler is now on window โ€” leaks globally!

// Bug 3: var in dead code still hoists!
if (false) {
  var neverRuns = 'this code never executes';
}
console.log(neverRuns); // undefined โ€” var hoisted even though if(false)!
// The DECLARATION was registered in Creation Phase
// The ASSIGNMENT never ran (code never executed)
๐Ÿ“‹ In 2024:
There is no reason to write new var declarations. Use let for values that change, const for values that don't. The only time you encounter var now is: reading old code, debugging legacy projects, or answering interview questions. But understanding var's behavior is essential because it explains WHY let and const were designed the way they were.

Lo kar liya โ€” Key Points:

  • โœ… V8 processes a function in TWO phases: Creation (scan for var/function declarations) then Execution (run code line by line)
  • โœ… var declarations are hoisted and initialized to undefined. Function declarations are hoisted AND fully initialized
  • โœ… Only the DECLARATION is hoisted โ€” the ASSIGNMENT stays on the line where you wrote it
  • โœ… var is function-scoped, NOT block-scoped โ€” it leaks out of if/for/while/switch blocks
  • โœ… var in a loop creates ONE variable shared by all iterations โ€” the classic closure-in-loop bug
  • โœ… Global var declarations add properties to the global object (window) โ€” let/const do NOT
Course Search
Search across all chapters & stages
๐Ÿ“–

Search the course

Type any topic โ€” branching, stash, rebase, hooks โ€” and jump straight to that chapter.

merge branchesgit stashundo commitrebase