Hoisting & Scope Traps

Medium100% Free~25 mins#hoisting#scope#output-prediction#lexical-environment#default-parameters#var#closures
Key Learning Objectives
✓

Understand the distinction between creation phase hoisting and execution phase assignment.

✓

Master declaration collision rules: why function declarations take precedence over var declarations during hoisting.

✓

Understand ES6 default parameter scope isolation and how it forms an intermediate scope between outer variables and function bodies.

✓

Analyze var leakage and absence of block scope in classic for loops.

✓

Confidently predict console output in tricky multi-scope interview code questions.

The Interview Problem

What is logged to the console when the following functions execute, and how do parameter scoping, function declaration precedence, and loop variable hoisting resolve?

1var x = 10;
2
3function parameterScope(a = x) {
4 var x = 20;
5 return a;
6}
7
8function declarationPrecedence() {
9 var before = typeof entity;
10 function entity() {}
11 var entity = 'reassigned';
12 return before + ' ' + typeof entity;
13}
14
15var b = 1;
16for (var b = 0; b < 3; b++) {}
17
18console.log(parameterScope(), declarationPrecedence(), b);
Predict Console Output
Interactive Challenge

Select the option that matches what standard ECMAScript prints to the console:

10 function string 3

20 undefined string 1

10 undefined string 3

undefined function string 2

V8 Engine Execution Trace
Step 1 of 7 (Line 1)

Global scope initialized: x = 10, b = 1. Functions parameterScope and declarationPrecedence are hoisted.

Call Stack (Top = Active)
Global Execution Context
Lexical Scope / Bindings
x:10
b:1
Console Stream
> [empty]

Deep Technical Breakdown

1. The ES6 Parameter Scope Trap

When a function declaration contains default parameter values (e.g., function fn(a = x)), the ECMAScript specification (§ 9.2.12) creates a separate parameter environment record:

  1. The parameter scope sits between the outer enclosing environment and the function body environment.
  2. When a = x evaluates, x is looked up in the parameter scope and falls back to the outer scope (x = 10).
  3. Inside the function body, var x = 20 creates an independent variable in the body environment.
  4. Reassigning x in the body never alters the a parameter in the parameter environment!

2. Declaration Hoisting Precedence: Function vs Var

During the Creation Phase of an execution context:

  • function foo() {} is hoisted first, attaching the actual function object to the environment record.
  • var foo is hoisted second. If an identifier with the same name already exists from a function declaration, the var declaration is essentially ignored during creation.
  • However, in the Execution Phase, assignment statements (foo = 'reassigned') overwrite whatever is currently bound to that identifier.
javascript
function test() {
  console.log(typeof fn); // 'function' (function declaration hoisted)
  function fn() {}
  var fn = 123;
  console.log(typeof fn); // 'number' (assignment took place)
}

3. Loop Scoping: var vs let

var statements are scoped to the nearest enclosing function or script/global environment, never to blocks ({ ... }). Therefore, for (var b = 0; b < 3; b++) mutates the global variable b. When the loop finishes, b has reached 3.

In contrast, let creates a fresh lexical binding for each loop iteration, preserving block isolation.

Common Traps & Mistakes

Assuming `var` in a function body hoists into default parameter expressions. Parameter evaluation happens before body execution in a parent scope.

Thinking `var entity` overwrites `function entity() {}` during hoisting. Function declarations always take priority during the creation phase.

Believing that `{ ... }` blocks create scope boundaries for `var`. Only functions and ES modules create scope for `var`.

Expecting `b` to be `2` at the end of `for (var b = 0; b < 3; b++)`. The loop continues until the condition `b < 3` evaluates to false, which occurs when `b` is `3`.

FAANG Follow-Up Probes
Probe #1

What happens if a default parameter references another parameter declared later in the parameter list (e.g. fn(a = b, b = 1))?

Probe #2

Why does typeof nonExistent return 'undefined', but typeof uninitializedLet throw a ReferenceError?

Probe #3

How does hoisting behave for function declarations placed inside an if block in strict mode versus sloppy mode?