CommonLounge Archive

Understanding Scope in JavaScript

March 19, 2018

Introduction

It is common for programmers to approach learning scope by trial and error. Scope is a complex topic that can be very powerful when it is well understood, but it can also be a double-edged sword when used sloppily.

In this first part of our Advanced JavaScript Tutorial we are going to delve deeply into execution contexts and scope. In this tutorial, we will learn about scope and the important difference between global and local environments. In the next tutorial, we will analyze what is called the execution context, which has to do with the process by which JavaScript creates scopes and contexts. Finally, in the last tutorial, we are going to learn some tricks to “hack” Javascript’s scopes, by using keywords like let and const to create block scopes or taking advantage of the eval() function.


So, what is scope?

Scope establishes which variables, functions and objects are available or accessible in some particular section of your code during runtime. What this means is that not everything that is visible to you is “visible” to the rest of your JavaScript’s code. This is called The Principle of Least Privilege — it states that it’s better not to give full access to everyone that is involved in a system. Users should only have access to the things they need to fulfill their purpose at a given time. If everyone has full access, you won’t know who caused the error when something fails. In programming languages, this principle is applied by restricting which part of your code has access to which resources.

There are two different types of scopes in JavaScript:

  1. Global Scope
  2. Local Scope

Let’s look at a very simple example:

var global_variable = "I am a global variable";
function aFunction() {  
    var local_variable = "I am a local variable";
}

As you can see, we have two variables. The first one is outside of any function, and thus exists in the global scope. The second one is inside the function aFunction() and exists in the local scope of said function.

Any variable declared outside of a function is a global variable, and is created within the global scope. All scripts and functions in your JavaScript code will have access to the global scope. Of course, there is only ONE global scope per JavaScript file, and it is created as soon as runtime begins (we will learn more about this process in the next tutorial).

By contrast, any variable declared inside of a function is a local variable, and they are created within the local scope of the function. These variables are only available within their local scope, and thus any functions OUTSIDE of that scope will not be able to access them.

Let’s take our former example and expand it a little bit:

var global_variable = "I am a global variable";
function aFunction() {
    var local_variable = "I am a local variable";
    console.log(global_variable); //"I am a global variable"
    console.log(local_variable); //"I am a local variable"
}
aFunction();
console.log(global_variable); //"I am a global variable"
console.log(local_variable); //ReferenceError: local_variable is not defined

This script will print the following in the console:

"I am a global variable"
"I am a local variable"
"I am a global variable"
ReferenceError: local_variable is not defined

Why is that? Well, global_variable is in the global scope, so it can be accessed by any other part of the script (including, of course, aFunction()); local_variable is instead defined inside of aFunction(), so it only lives WITHIN the function.

When we call aFunction(), the function has access to the global scope and to its own local scope, and so it is able to print both variables. But, when we try to access local_variable from outside of the function, it throws a Reference Error, because that variable is not accessible from that part of the script (in fact, when console.log() tries to find it, that variable doesn’t even exist).


Function arguments

Function arguments (parameters) basically work as local variables inside of a function, so they can only be called within the function where they are created:

function aFunction(first_argument, second_argument) {
    console.log(first_argument + second_argument);
}
aFunction(1, 2); //3
console.log(first_argument + second_argument); //ReferenceError: first_argument is not defined.

Block Statements

By contrast, block statements like if and switch conditions or for and while loops do not create their own scopes. This means that variables that are defined within a block statement will live inside of the scope that the block statement was created in:

if (true) {
    var if_statement = "If";    
}
switch (1 + 2) {
    case 3:
        var switch_statement = "Switch";
}
for (var i = 0; i < 1; i++) {
    var for_loop = "For";
}
var j = true;
while (j) {
    var while_loop = "While";
    j = false;
}
console.log(if_statement); //”If”
console.log(switch_statement); //”Switch”
console.log(for_loop); //”For”
console.log(while_loop); //”While”

Here, the four variables are accessible from the global scope, because all of them were created within block statements that ran inside of the global scope.

There is a way around this, the const and let keywords, but we will talk about them in a later tutorial.


Namespaces

One cool thing about local scopes is that they allow us to create multiple namespaces. Think of a namespace as a “container of unique names”. Two objects cannot have the same name inside of a namespace, but objects with the same name can exist throughout different namespaces. This means that variables with the same name can be used in different functions:

var var1 = 1;
var var2 = 2;
function aFunction() {
    var var1 = 3;
    var var2 = 4;
    console.log(var1 + var2);
}
function otherFunction() {
    var var1 = 5;
    var var2 = 6;
    console.log(var1 + var2);
}
aFunction(); //7
otherFunction(); //11
aFunction(); //7
console.log(var1 + var2); //3

In this example, the console is going to print 7, 11, 7, and 3. Notice that calling otherFunction() did not override the variables that we declared in aFunction(). Also, even after calling both functions, the global variables remained intact. This is because the variables exist only within their own local scopes. The lifetime of JavaScript’s variables starts when they are declared, and local variables are deleted when the function where they exist in is completed.

Having said that, variables with the same name in the SAME scope will result in name collision. The last variable to be declared will override all previous declared variables that have the same name:

var var1 = 1;
var var1 = 2;
var var1 = 3;
var var1 = 4;
console.log(var1); //4

Be careful when naming variables in the global scope! If you create them using the same name as a built-in function/object, you could redefine the original function/object and cause some trouble. For example:

var console = 1;
alert(console); //1
console.log(console); //TypeError: console.log is not a function

Here, I am redefining console. Notice that alert(console) will work (it will alert 1) because it is simply printing the variable named console. But console.log() will not work because we have redefined console and now it is a variable containing an integer instead of a built-in complex object.


Lexical Scope

When creating nested functions, the inner functions will always have access to the variables, functions and objects defined in their parent functions. This is known as Lexical Scope or Static Scope. We say that the child functions are lexically bound to their parent functions’ execution contexts (think “scopes” for now).

Nonetheless, parent functions don’t have access to their child functions’ variables, because lexical scope only works forward, not backward. This has to do with the way in which JavaScript constructs execution contexts. We will explain execution context in detail in the next tutorial. For the time being, we can understand this concept with an example:

function firstFunction() {
    var var1 = 1;
    //Only var1 is accesible from this scope.
    function secondFunction() {
        var var2 = 2;
        //var1 and and var2 are accessible from this scope.
        console.log(var1 + var2); //3
        function thirdFunction() {
            var var3 = 3;
            //var1, var2, and var3 are accessible from this scope.
            console.log(var1 + var2 + var3); //6
        }
        thirdFunction();
    }
    secondFunction();
    //console.log will throw a ReferencError because var2 and var3 and not accessible from here.
    console.log(var1 + var2 + var3); //ReferenceError
}
firstFunction();

As you can see, firstFunction() only has access to var1. secondFunction() has access to var1 and var2; and thirdFunction() has access to all three variables.

When we access var1 and var2 from within secondFunction(), the console prints 3, as var1 was created in secondFunction’s parent scope (firstFunction()).

Accessing var1 and var2 is also possible from within thirdFunction() because firstFunction() and secondFunction() are parents of thirdFunction().

Nevertheless, var2 and var3 are not accessible from firstFunction() because Lexical Scope only works forward: when the script tries to access var1 and var2 from firstFunction(), the variables simply don’t exist and JavaScript throws a Reference Error.


The var keyword

Have you noticed that in all of our examples all variable declarations have started with the var keyword? What happens when we don’t use it?

Well, when JavaScript finds a non-declared variable (a variable without the var keyword that can’t be found within the current scope), it starts to look for it in the parent functions’ scopes. If it finds it, it simply redefines the variable; if it DOES NOT find it, then it creates it for you in the global scope. This is dangerous, because if you are not careful you could be creating variables in unexpected scopes that will cause your script to have unexpected behaviors. Look at this script:

var var1 = 1;
var var2 = 2;
function aFunction() {
    var1 = 3;
    var var2 = 4;
    var3 = 5;
    console.log(var1 + var2 + var3);
}
aFunction(); //12
console.log(var1 + var2); //5
console.log(var3); //5

var1 and var2 are first created in the global scope. The problems start when we define aFunction().

  • var1 inside _aFunction()_ doesn’t have the var keyword and thus JavaScript starts searching for it in the parents’ scopes. It is found in the global scope and its value is reassigned (from 1 to 3).
  • var2 inside aFunction() is declared as usual, so a new local variable is created (which will stop existing when aFunction() is completed).
  • var3 also doesn’t have the var keyword, so JavaScript looks for it. var3 is not found, and so it is created in the global scope.

And so, when we call console.log(var1 + var2), the console prints 5 (not 3, as we would expect), because var1 has been reassigned.

Surprisingly (not really) console.log(var3) does not throw a Reference Error, because var3 was created in the global scope, and now it is accessible from everywhere in the script.


Security

Some programming languages give you the option of declaring public, private and protected scopes to “hide” certain aspects of your code from users. JavaScript does not allow the definition of public and private scopes, but we can simulate them using closures, which will be a topic for another tutorial.


Scope and Context

It is important not to mistake the meanings of scope, context, and execution context. Scope is what we have been discussing in this tutorial. Context is used to refer to the value of this in a particular section of your script (this will be discussed in the tutorials regarding Object-Oriented Programming). Execution Context is the process by which scopes are created and this is assigned (which will be discussed in the next tutorial).


Conclusion

We hope that it is now clear to you the importance that scopes have when writing JavaScript code. Scopes are a powerful tool to organize your code, but when used carelessly, they can become a terrible headache. Throughout the next tutorials, we will explore scopes deeper, so that you are able to always use them in your favor. Read on to continue your journey to become a JavaScript Grand Master!


© 2016-2022. All rights reserved.