Skip to content

Scope And Hoisting

Let’s take a closer look at two fundamental concepts Scope and Hoisting.

Scope determines the accessibility of variables in different parts of the program. JavaScript supports two main types of scope:

  • Global Scope: Variables declared outside of any function body belong to the global scope. They can be accessed and modified anywhere in the code.
  • Function Scope: Variables declared inside a function are only accessible within that function and any nested functions.
  • Block Scope: Introduced with ES6, variables declared using let or const have block scope. This means they are accessible only within the surrounding curly braces {}.
{
let blockScoped = "I exist only in this block!";
console.log(blockScoped); // This works
}
console.log(blockScoped); // ReferenceError: blockScoped is not defined

In contrast, variables declared with var are not block-scoped, which can often lead to unexpected results.

Hoisting is JavaScript’s default behavior of moving variable and function declarations to the top of their respective scopes during the compilation phase.

Variables declared with var are hoisted, but their values are not initialized until the code execution reaches the declaration.

console.log(hoistedVar); // undefined
var hoistedVar = "I was hoisted!";
console.log(hoistedVar); // "I was hoisted!"

Variables declared with let and const are also hoisted, but they are placed in a “temporal dead zone” from the start of the block until the declaration.

console.log(blockScopedVar); // ReferenceError: Cannot access 'blockScopedVar' before initialization
let blockScopedVar = "This won't work with let or const!";

Functions declared using the function keyword are hoisted, and their definition is available for use before they are defined in the code.

sayHello();
function sayHello() {
console.log("Hello, Hoisting!");
}

However, functions assigned to variables AKA function expressions (using var, let, or const) are not fully hoisted. They behave like variables being assigned to function expressions.

this holds a reference to the execution context, which depends on how the function is called.

The value of this depends on where the function is called.

function regularFunction() {
console.log(this);
}
regularFunction(); // In non-strict mode: global object (e.g., `window`) // In strict mode: undefined

Arrow functions do not have their own this, instead, they inherit it from their surrounding scope.

const obj = {
arrowFunction: () => {
console.log(this);
},
};
obj.arrowFunction(); // Inherits `this` from the surrounding scope ie, In non-strict mode: global object (e.g., `window`) // In strict mode: undefined

Regular function vs Arrow function behaviour example

const obj1 = {
regularFunc: function () {
console.log(this); // `this` refers to obj1
},
arrowFunc: () => {
console.log(this); // `this` refers to outer scope
},
};
// Above example in action
const obj2 = {
name: "example",
regularMethod: function () {
console.log(this.name); // "example"
},
arrowMethod: () => {
console.log(this?.name); // undefined
},
};

Example to see how it works in nested functions

const obj3 = {
name: "test",
outer: function () {
// Regular function creates its own `this`
console.log("Regular function this:", this.name); // "test"
// Arrow function inherits `this` from outer function
const inner = () => {
console.log("Arrow function this:", this.name); // "test"
};
inner();
},
};

If you need the object’s context try using the following.

// Use regular function
const obj4 = {
name: "correct",
method: function () {
console.log(this.name); // "correct"
},
};
// Use method shorthand
const obj5 = {
name: "correct",
method() {
console.log(this.name); // "correct"
},
};
// Bind the function
const obj6 = {
name: "correct",
method: (() => {
console.log(this.name);
}).bind(obj6),
};