< WebDevRef />

Embracing JavaScript: Scope and Hoisting

Sep 18, 2021

#javascript

8 min read

last updated: Sep 21, 2021

Scope refers to the accessibility of concerning variables, functions or objects in a particular part of code.

As JS interpreter weaves through our code (in a single thread) and executes it line by line, it checks whether the variables and invoked functions are properly defined.

šŸ”„ Scopes can be broadly categorized as:

šŸŒæ Global scope - Any declaration outside a function or block (curly braces). This is the outermost scope.

let agent = "James Bond"; //global scope

šŸŒæ Local scope - This is turn is of two types:

  • Function scope: Inside a function
function callAgent() { var agent = "Ethan Hunt"; console.log(agent); }
  • Block scope: Inside a pair of curly braces (like if statements and for loops, or standalone braces)
if(true){ console.log("Bitter but truth"); }

It might seem confusing at first since function too have curly braces like blocks do. But there is a separation of concern, which we'll see below. So you could summarize scope types as Global, Function/Local and Block.

Now when it comes to variables in JavaScript, we can declare one using: var, let or const.

  • Declared with var: function/local or global scope
  • Declared with let or const: block scope

šŸŒæ Guess the scope of below variables:

let firstAvenger = "Captain America"; { //standalone block var alimightyAvenger = "Thor"; } function greetAvenger() { const wizardAvenger = "Dr. Strange"; }
  1. "firstAvenger" ā‡’ global scope, since it's out in the open.
  2. "alimightyAvenger" ā‡’ global scope. Yes, it's in a block but "var" is not bound to a block, it is global in this case.
  3. "wizardAvenger" ā‡’ local scope. Yes, it is in a function, but remember from the previous post, a function has its own new execution context. Hence, within the curly braces of the function, the variable is in the local scope.

Ā 

šŸ”„ Talking about functions and execution contexts, they can be nested, like below. Here we observe something called lexical scope.

function outer() { inner(); function inner() { } }

Ā 

šŸš§ Recollection Alert šŸš§

In the last post, we discussed "Lexical Environment", which is basically the environment that a function exists in, including both the inner and outer space. And when we talk about scope in that context, we refer to the scope as lexical scope.

Lexical Environment has a component that holds the reference to its outer scope. Meaning, during the execution phase, if the JS engine is unable to locate a variable used inside a function, it can go look for it outside.

Lexical scope is what makes this possible:

let wizard = "Dumbledore"; function hogwardsNews() { console.log(`We lost professor ${wizard}.`); } hogwardsNews(); // "We lost professor Dumbledore." //-------------------------------Nested functions---------------- function hogwardsNews() { let wizard = "Severus Snape"; function finale() { console.log(`We lost professor ${wizard} as well.`); } finale(); // "We lost professor Severus Snape as well." }

Ā 

šŸ“Œ The main takeaway of lexical scope is that, it is one way. Meaning, we can access outer variables (in the global scope or outer function scope) from a function but not vice versa.

console.log(`${wizard} is gone.`); // ReferenceError: wizard is not defined function hogwardsNews() { let wizard = "Voldemort"; } //------------------------In case of nested functions---------------------- function hogwardsNews() { console.log(`We lost ${wizard}.`); // ReferenceError: wizard is not defined tournamentNews(); function tournamentNews() { let wizard = "Cedric Diggory" } }

Ā 

šŸ”„ See what we did there with "tournamentNews" function ā‡’ called it before defining.

Well, it means, it's time to bridge the gap between Scope and Hoisting.

Where the variables and functions are declared dictates where they will be accessible in our code.

When our script runs, the JS engine creates a Global Execution Context(GEC), and when functions are invoked we get Function Execution Context (FEC).

EC creation happens over two phases:

  • Creation Phase
  • Execution Phase

It is during the creation phase that we observe Hoisting.

In the creation phase, the engine scans every variable declaration and function declaration and hoists them to the top of their context. Hoisting literally means to lift.

This dictates, how our code will behave in the execution phase, like whether it runs properly or throws an exception.

Ā 

šŸŒæ There is a catch, which we will explore through the below examples:

Only declarations are hoisted, not initialization.

That means, during the creation phase:

  • For variables: JS allocates them a location in memory labelled with their identifier names but doesn't bother with their actual values.
let anime = "Demon Slayer"; // identifier name: "anime" // creation phase - hoisting // memory || value // [01x001]anime || depends on var,let const
  • For functions: JS allocates them a location in memory labelled with their identifier names, but the content of that location is a reference to the actual function definition
function favAnime() { console.log("demon slayer") } //creation phase - hoisting // stack memory || value // [01x002]favAnime || [08xx01] // heap memory || value // [08xx01] || function favAnime..... (whole definition)

Ā 

Keeping that in mind, let's consider the following example:

let place= "Hobbit Village"; lordOfTheRings(); function lordOfTheRings() { // some code }

Both, "place" variable and "lordOfTheRings" function gets hoisted to the top of GEC.

We can imagine it as:

// creation phase // Hoisting let place; lordOfTheRings // holds a reference to the "lordOfTheRings" function definition place = "Blade Runner 2049"; lordOfTheRings() // JS will fetch the function definition from heap memory and execute the function

Ā 

šŸš§ Word of caution šŸš§

No, JS doesn't physically move those variables or functions to the top of the code. This is just so you can have a photo memory of how to explain the concept.

Since function definition gets hoisted, we can invoke it beforehand. But, what about the variables?

Ā 

šŸ”„ Given that we have multiple ways to define variables, so there is a catch in how they are hoisted.

šŸ“Œ var keyword

console.log(fruit); // undefined var fruit = "banana";

šŸ“Œ let and const keyword

we get the following reference error

console.log(fruit); // "ReferenceError: Cannot access 'counter' before initialization let fruit = "orange";

So in the creation phase, during hoisting

  • "var" variables are allocated space in stack memory and initialized with a special value "undefined". Not a string. It is a primitive data type.
  • "let" and "const" variables are allocated a space in stack memory, but not initialized, hence the "initialization error".

Ā 

šŸŒæ Another catch is when we use function expressions and arrow functions

// this is function declaration function favFruit() { //some code } // this is function expression let favFruit = function() { //some code } const favAnime = () => { //some code }

In a function expression, we use the "function" keyword, or simple braces for arrow function expressions, to define an anonymous function and assign it as a value to a variable. They are treated as any other value of a variable.

Here's the case,

favFruit(); // TypeError: favFruit is not a function var favFruit = function () { //some code } //During hoisting, this the state var favFruit = undefined; favfruit() // means we are trying to do undefined(), hence the error

Ā 

šŸ”„ All these errors and chaos šŸ˜µ is due to Temporal Dead Zone (TDZ).

Sounds fancy, but it simply refers to the zone/space that exists in the time between, variable declaration and initialization.

In the context of hoisting think of it as the time between variable hoisting and initialization of the variable.

// TDZ starts // let favAnime; // uninitialized when hoisited console.log(favAnime); // variable has been declared, thus memory allocated, but not initialized yet let favAnime = "Demon Slayer"; // variable is initialized, marks the end of TDZ

Ā 

As we could clearly see that hoisting can create some valid confusion. It is important to understand but advised not to exploit it as a convenience. Try calling the functions and using the variables after defining them.

Ā 

šŸ”„ To hit it home with try this example:

var food = "grapes"; thoughts(); let thoughts = function () { console.log(`Original food: ${food}`); var food = "oranges"; console.log(`New food: ${food}`); };

This example is taken from the free trial videos of Andrei Neagoie's Advanced JavaScript Course. Validate your answer with the video.

Ā 

šŸ“ššŸ’” Resources referred for the blog:

Ā 

šŸ• Teaser

Did you observe these terms we used above: Stack and Heap memory. Let's discuss that in our next post.

Ā 

Leave your queries, suggestions or corrections in the comments below.

Have a good one. Take care. Bye. šŸ‘‹

Go to all posts...