DEV Community

Rahul Vijayvergiya
Rahul Vijayvergiya

Posted on • Edited on

Hoisting, Lexical Scope, and Temporal Dead Zone in JavaScript

JavaScript is often a quirky language. It's crucial to understand concepts like hoisting, lexical scope, and the temporal dead zone (TDZ). Rather than just going through theory, let's dive into practical examples that illustrate these concepts, some of which might initially seem confusing.

Hoisting

Hoisting is JavaScript's behavior of moving declarations to the top of their containing scope during compilation. This can lead to some unexpected results.

Example 1: Variable Hoisting

console.log(myVar); // undefined
var myVar = 5;
console.log(myVar); // 5
Enter fullscreen mode Exit fullscreen mode

In this example, you might expect a ReferenceError for the first console.log, but JavaScript "hoists" the declaration of In this example, you might expect a ReferenceError for the first console.log, but JavaScript "hoists" the declaration of myVar to the top, so it exists (but is undefined) before its initialisation. to the top, so it exists (but is undefined) before its initialisation.

Example 2: Function Hoisting

console.log(myFunction()); // "Hello, world!"

function myFunction() {
  return "Hello, world!";
}
Enter fullscreen mode Exit fullscreen mode

Function declarations are also hoisted, allowing them to be called before their definition in the code.

Example 3: Hoisting with Function Expressions

When a function is assigned to a variable using var, only the variable declaration is hoisted, not the function assignment. This can lead to different behavior compared to function declarations.

console.log(myFunction); // undefined
myFunction(); // TypeError: myFunction is not a function

var myFunction = function() {
  console.log("Hello, world!");
};

myFunction(); // "Hello, world!"
Enter fullscreen mode Exit fullscreen mode

The variable myFunction is hoisted and initialised to undefined, causing a TypeError when called before the function assignment.

Example 4: Hoisting Confusion with Var

function testHoisting() {
  console.log(foo); // undefined
  var foo = 'bar';
  console.log(foo); // "bar"
}

testHoisting();
Enter fullscreen mode Exit fullscreen mode

In testHoisting, foo is hoisted and initialised to undefined, resulting in the first console.log(foo) logging undefined.

Example 5: Hoisting with Let and Const

function testLetConst() {
  console.log(bar); // ReferenceError
  let bar = 'baz';
  console.log(bar); // "baz"
}

testLetConst();
Enter fullscreen mode Exit fullscreen mode

Variables declared with let and const are not hoisted to the top, so the first console.log(bar) results in a ReferenceError.


Lexical Scope

Lexical scope means that the scope of a variable is determined by its position in the source code.

Example 1: Nested Functions (Lexical Scope)

function outerFunction() {
  var outerVar = 'I am outside!';

  function innerFunction() {
    console.log(outerVar); // "I am outside!"
  }

  innerFunction();
}

outerFunction();
Enter fullscreen mode Exit fullscreen mode

Here, innerFunction can access outerVar because it is defined within the same lexical scope.

Example 2: Scope Confusion (Lexical Scope)

var x = 10;

function foo() {
  var x = 20;
  bar();
}

function bar() {
  console.log(x); // 10
}

foo();
Enter fullscreen mode Exit fullscreen mode

One might expect bar() to print 20, but it prints 10 because bar is defined in the global scope, where x is 10.

Example 3: Lexical Scope and Closures (Lexical Scope)

function createFunction() {
  var localVar = 'local';

  return function() {
    console.log(localVar); // "local"
  };
}

var myFunction = createFunction();
myFunction();
Enter fullscreen mode Exit fullscreen mode

myFunction retains access to localVar even after createFunction has finished executing, demonstrating a closure

Example 4: Lexical Scope in Loops (Lexical Scope)

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 3, 3, 3
  }, 1000);
}

for (let j = 0; j < 3; j++) {
  setTimeout(function() {
    console.log(j); // 0, 1, 2
  }, 1000);
}
Enter fullscreen mode Exit fullscreen mode

The var declaration in the first loop results in i being hoisted to the global scope, so all three setTimeout callbacks log 3. The let declaration in the second loop creates a new j for each iteration, so the callbacks log 0, 1, and 2.

Example 5: Comparing Lexical Scope for Function Declarations and Arrow Functions

I think this topic is big enough to complete in one example, so i created a article about it, here is the link


Temporal Dead Zone (TDZ)

The TDZ refers to the period between the entering of a scope and the actual declaration of variables, where they cannot be accessed.

Example 1: Let and Const - Temporal Dead Zone (TDZ)

console.log(a); // ReferenceError
let a = 5;
Enter fullscreen mode Exit fullscreen mode

Unlike var, variables declared with let and const are not hoisted to the top of their block scope, resulting in a ReferenceError if accessed before their declaration.

Example 2 - Temporal Dead Zone (TDZ)

{
  console.log(b); // ReferenceError
  let b = 10;
}

{
  let c = 20;
  console.log(c); // 20
}
Enter fullscreen mode Exit fullscreen mode

In the first block, accessing b before its declaration results in a ReferenceError due to the TDZ. In the second block, c is accessed after its declaration, so it logs 20.

Example 3: Temporal Dead Zone with Functions

{
  console.log(func); // ReferenceError
  const func = function() {
    return 'Hello!';
  };
}

{
  const func = function() {
    return 'Hello!';
  };
  console.log(func()); // "Hello!"
}
Enter fullscreen mode Exit fullscreen mode

In the first block, accessing func before its declaration results in a ReferenceError due to the TDZ. In the second block, func is accessed after its declaration, so it works as expected.

Combining Concepts

Example 1: Hoisting and TDZ

{
  console.log(d); // ReferenceError
  let d = 15;
  console.log(d); // 15
}
Enter fullscreen mode Exit fullscreen mode

This example shows how accessing a let variable before its declaration results in a ReferenceError due to the TDZ, even though the declaration is hoisted to the top of the block.

Example 2: Lexical Scope and Hoisting

var e = 30;

function test() {
  console.log(e); // undefined
  var e = 40;
  console.log(e); // 40
}

test();
console.log(e); // 30
Enter fullscreen mode Exit fullscreen mode

In test(), e is hoisted and initialised to undefined within the function scope, so the first console.log(e) logs undefined.

Example 3: Hoisting, Lexical Scope, and TDZ Combined

let f = 50;

function outer() {
  console.log(f); // 50
  let f = 60;
  inner();

  function inner() {
    console.log(f); // ReferenceError
  }
}

outer();
Enter fullscreen mode Exit fullscreen mode

Here, inner is defined within the scope of outer, but f is in the TDZ within outer before its declaration, leading to a ReferenceError when inner tries to access it.

Conclusion

Understanding hoisting, lexical scope, and the temporal dead zone is vital for writing robust JavaScript code. These examples illustrate how JavaScript handles variable and function declarations, scopes, and the strange behaviour of accessing variables before their declaration. By experimenting with these concepts, you'll develop a more intuitive grasp of JavaScript's behaviour.

Related Article By Author:

References
MDN Web Docs: Hoisting
MDN Web Docs: Closures
MDN Web Docs: Lexical Scoping
MDN Web Docs: Temporal Dead Zone
JavaScript Info: Variable Scope, Closure
Eloquent JavaScript: Functions

Top comments (0)