🤖 Looking in-depth at JavaScript Scopes

Photo by Dawit on Unsplash

🤖 Looking in-depth at JavaScript Scopes

I know it's so nice to be able to write good code. However, is even better when you can explain it, understand why things work like that and have a good reasoning behind each line of code you write.

From my point of view, once you have aquired a theorical knowledge and you're able to perform it even from a practical side, that easily and quickly becomes a habit you're used to include in all of your projects without even having to think about it.

You just know it works like that, you're used to how a particular thing behaves once implemented and you just repeat that code more and more without worrying about whether your syntax is making sense or not.

It clearly does, because you made studies before writing that code that way.

So, one of the topics you've definitely come across during your career as a JavaScript developer are Scopes.

One of my personal and simple descriptions about what Scopes are could be the following:

A Scope in JavaScript is the environment within which you're able to see, use and have access to variables and functions.

MDN describes Scopes as follows:

The scope is the current context of execution in which values and expressions are "visible" or can be referenced. If a variable or expression is not in the current scope, it will not be available for use.

The main different kind of Scopes

Global Scope

Even if you've never heard about Scopes, you've definitely worked with them already. Each time you setup a new JavaScript file and open it ready to write your code, you're already inside a Scope. That's called the Global Scope.

The Global Scope is the main Scope in which all the variables, functions, objects and entities in general we declare are available throughout our whole JavaScript file.

This means that, if you write a new function, that function can have access to all the variables your declared outside of it. And all the functions in your code will too.

Here's an example of a function accessing and editing the value of a variable declared (and initialized) in the Global Scope:

let peopleInRoom = 12;

function kickPeople(peopleToKick) {
    peopleInRoom -= 5;
}

kickPeople();

console.log(peopleInRoom); // 7

Now, ignoring the low use and and the badness of this code wanting to randomly kick people out of the room, by running this script you can definitely notice that it works, and the function kickPeople() has actually access to the peopleInRoom variable declared outside of it.

This small fragment of code above teaches us a fundamental rule: when going in-depth one or more levels of nesting, we always have access to all the variables, functions and methods we instanciated in the parent levels.

This means that we can access peopleInRoom within the kickPeople() function, but not viceversa: everything declared inside our kickPeople() function won't, instead, be accessible in our Global Scope (so, outside our kickPeople() function).

Here's an example of a WRONG syntax, which will cause a reference error during runtime (when we execute the script in our page).

function sum(a, b) {
    const RESULT = a + b;
    return RESULT;
}

console.log(RESULT); // ReferenceError: RESULT is not defined

Well, we tried to access the RESULT variable, but that variable doesn't exist in the Global Scope, rather only inside the sum() function.

That's exactly how the Function Scope behaves.

Function Scope

What we just saw above is a pretty good representation of how a Function Scope works. We declared a RESULT variable, which we discovered ended up being only accessible inside the function and not outside.

Why does that happen? The reason is that all the variables which get created after invoking and running a function, are immediately deleted as soon as the function ends its work. This causes the variables not to be accessible anymore outside and, indeed, not to exist anymore.

That's why we get a reference error. That variable was deleted just after the function finished all its operations, and that variable cannot be accessed anymore from outside of the function.

What about, instead, a function nested inside another function? How are variables handled, and what entities can I use or invoke inside each? Let's see an example:

function generator() {

    let word = 'cat';

    function generateSentence() {
        console.log('I really love my ' + word);
    }

    generateSentence();

}

generator(); // "I really love my cat"

Well, generateSection() function (also called inner function, since it's the deeper one) can have access to the variable word, which was instead declared in its parent function generator() (also called outer function).

Each of these two functions generated their own Scope, through which they're able to see and edit all the variables contained inside of each.

The difference is that the inner function can have access to the outer function, and not viceversa. After making few modifications to the code above, here's an example of an alternative code which, instead, generates an error:

function generator() {

    function generateSentence() {
        const PHRASE = 'I really love my ';
    }

    console.log(PHRASE + 'cat'); // ReferenceError: PHRASE is not defined

}

generator();

By running this code, we get another reference error, which tells us that the variables PHRASE is not defined.

It's clear: that variable is placed within an inner function, and we don't have access to the Scope of that inner function. That variable is something which is declared in a deeper level, and we cannot expect our code to work by using it.

Summarizing, you can access variables starting from the most in-depth function you have till the most outer function.

This happens because, if you try to use a variable, the JavaScript engine will look for your variable first in the most in-depth functions and nidifications in a hierarchieral way, and will climb the nidifications one at at time until it finally reaches the Global Scope.

And, if the variable you're trying to use doesn't exist in the Global Scope neither, it will throw a reference error during runtime.

Block Scope

Last but not least, is the Block Scope, a kind of scope introduced with the ES6 version of JavaScript, which makes sure the new kind of variable declarations let and const work the right way.

Simply, all the variables declared with the let and const keywords within curly brackets of any type won't be accessible from outside, since using curly brackets creates its own Scope.

That said, you won't be able to perform this piece of code, since it will throw a reference error:

if (1 === 1) {
    let myNumber = 5;
}

console.log(myNumber); // ReferenceError: myNumber is not defined

But you will be able to access the myNumer variable if you declare it using the var keyword, instead (not recommended anymore).

if (1 === 1) {
    var myNumber = 5;
}

console.log(myNumber); // 5

Tips for understanding Scopes

You'll feel comfortable with Scopes as soon as you try working with them. Do your tests, fail, look at your code and try writing it again in order for it to properly work.

Nest functions, methods inside objects, use variables from outer functions within your inner functions and see what happens when you try to access them. What do you notice? Are you having success?

It's all about trying. Keep up the good work, you got it.

And if you don't like the verb to fail, just try to look at it from a different perspective.

I have not failed, I've just found 10.000 ways that won't work.