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 callAgent() { var agent = "Ethan Hunt"; console.log(agent); }
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.
šæ Guess the scope of below variables:
let firstAvenger = "Captain America"; { //standalone block var alimightyAvenger = "Thor"; } function greetAvenger() { const wizardAvenger = "Dr. Strange"; }
Ā
š„ 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:
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:
let anime = "Demon Slayer"; // identifier name: "anime" // creation phase - hoisting // memory || value // [01x001]anime || depends on var,let const
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
Ā
šæ 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. š