Introduction

So far in this course, we've learnt a great deal of information regarding the different aspects of JavaScript, ranging from control flow, to functions, to operators, and so on and so forth. Still however, one of the most fundamental ideas is to be able to effectively work with variables.

And in that regard, we need to know about an extremely important concept related to variables, i.e. that of the scope.

In this chapter, we shall understand what is meant by the scope of an entity, and see how it defines where the entity is accessible in a script. We'll see how JavaScript resolves and binds a name to a particular entity in a program, which is vital for us to know so that we are aware of when a given variable would get modified.

What are scopes?

There's no pitch-precise formal definition of the term 'scope' in programming. It often refers to different things. But whatever it refers to, it has something to do with the availability of a given entity in a program.

Let's try to define 'scope' in terms of an entity:

The scope of an entity in a program refers to the part of the program where it is accessible.

The term 'entity' here represents a variable, constant or a function, each of which can have a name tied to them.

Coming back to the definition, it says that the idea of scope is tied to an entity, i.e. it is not a standalone concept, and simply tells us where that entity is available in the program.

This technically means that we can't say something like "this is a scope"; instead we have to pair scope with an entity like as follows: "this is the scope of the variable x" or that "this is the scope of the constant PI", and so on.

Now even though it's easier and more sensible to think of the scope in regards to an entity and not as a standalone concept, the term is misused and meant to represent another idea. That is, sometimes a scope refers to a part of a program containing multiple entities (all of which obviously have the same scope).

For example, one might say that "the scope of the function x()" to refer to the body of the function x() and all the entities available in there.

We however feel that this misuse of terminology should be prevented whenever possible, and replace it with better and more intuitive terminology. A better term for this concept is a 'context', or even an 'environment'.

So now we'd rather say that "the environment of the function x()" to refer to the body of x() and all the entities available in there.

Better?

Resuming our discussion, the scope of an entity is what determines whether that entity would be accessible in a given function, outside a given block of code, and so on. Clearly, the idea of scope and environment are different but they're strongly related to one another. Likewise, you'll see both of them referred every now and then in this chapter.

Talking about JavaScript, entities could have either of the following three scopes:

  • Global scope
  • Local scope
  • Block scope (introduced with ES6)

Let's explore each of these three kinds of scopes in JavaScript.

Global scope

We'll start with the very first kind, i.e. global scope.

An entity with a global scope is accessible throughout the entire program.

The word 'global' means 'relating to the whole of something' and that's exactly the word represents in the term 'global scope'. That is, an entity with a global scope applies to the entire program.

As simple as it could get.

Talking about how something becomes globally-scoped, well that's pretty simple: any entity that's not defined inside a function in JavaScript, i.e. is defined at the top-level of the script, has a global scope.

Hence, it's available throughout the program. Such an entity is known as a globally-scoped entity or simply as a global entity.

Let's consider some examples.

In the following code, both the variables x and greeting are global variables, as they're not defined inside a function:

var x = 10;

if (true) {
   var greeting = 'Hello World!';
}

Similarly, the function showGreeting() and the constant PI in the code below are both globally-scoped as well:

function showGreeting() {
   alert('Hello World!');
}

const PI = 3.142

However, the variable x below is not global. That's simply because it's defined inside the function:

function f() {
   // This variable is not global.
   var x = 10;
}

In JavaScript, global entities can directly be accessed inside another function — there's no need for a special keyword to access them as is the case in some other programming languages, such as PHP.

Local scope

The next kind of scope is local scope, also referred to as function scope.

An entity with a local scope is accessible only inside the function that it's defined in.

It's only about entities defined inside a function that exhibit a local scope. If there's no function involved, then there's no local scope.

Apart from global entities, locally-scoped entities are really important in programming. They help us prevent unnecessarily polluting the global environment with entity names.

Local scope also illustrates another important idea related to functions in JavaScript, and many other programming languages. That is, the moment a function exits, all the entities defined inside it are deleted from memory.

Let's consider some examples of local entities.

In the code below, the variable x has a local scope. More specifically, it's locally-scoped to the function foo():

var a = 100;

function foo() {
   var x = 10;
}

This means that x won't be accessible outside the function foo() as is confirmed by the log below:

var a = 100;

function foo() {
   var x = 10;
}

// x is not available here.
console.log(typeof x);
undefined

The variable a, however, as we've seen just above, is globally-scoped and is therefore available throughout the entire program.

We could've directly accessed x in line 8 as well, but that would've led to an error since x doesn't exist in the region. Using typeof, we play it safe.

By virtue of local scopes, we can have identically-named entities in different functions. That's because each set of entities in each function is only scoped to that particular function and doesn't interfere with the set of entities of another function.

In the code below, we have two functions foo() and bar() with two different variables that turn out to have the exact same name, x:

function foo() {
   var x = 10; // A different x
   console.log(x);
}

function bar() {
   var x = 20; // A different x
   console.log(x);
}

Once again, it's important to remember that the local variable x of foo() is different from the variable x of bar() — they only have the same name, NOT that they refer to the same variable in memory.

Moving on, as stated before, a local entity is accessible inside the function where it's defined. This holds even if there is a function nested inside that function.

Shown below is an example:

function foo() {
   var x = 10;

   function bar() {
      console.log(x);
   }
   bar();
}

foo();
10

Here, we define a variable x inside foo() and so expect it to be available inside foo(), including inside the function bar() defined in there.

And as the console log confirms, the variable x is indeed available in bar().

Declarations govern the scope!

Let's try answering a very basic question.

What is the scope of x in the following code? Local or global?

var x;

function a() { x = 10; }
a();

Well, one might think that x is locally-scoped but that's NOT the case — instead, x is a global variable. But how?

Because the declaration of x is not inside any function; it's in the top-level of the script.

Always remember that the scope of a given entity is governed by its declaration, NOT by where the entity is itself used.

Block scope

Apart from global and local scopes, entities in JavaScript can have a block scope.

An entity with a block scope is accessible only inside the block it's defined in.

Prior to ECMAScript 6, JavaScript had only two scopes: global and local. But with the advent of ECMAScript 6, in particular the const and let keywords, things changed — we got a new scope, known as the block scope.

All let and const declarations inside a block of code, denoted via curly braces ({}), create variables and constants, respectively, that are only accessible within that block.

Consider the following code:

if (true) {
   let x = 10;
}

The variable x is defined inside if's body and is, henceforth, scoped only to the given block. Outside the block, the variable is not accessible.

Let's try confirming this with the help of some log statements:

if (true) {
   let x = 10;
   console.log(x);
}

console.log(typeof x);
10 undefined

The first log statement resolves with 10 since it's inside the block statement where we have our let x declaration. The second log statement, however, is outside the block and likewise doesn't have access to the variable.

Remember that it's only let and const declarations that are block-scoped; var declarations don't enjoy this kind of scoping.

This is evident in the following code:

if (true) {
   var x = 10;
   console.log(x);
}

console.log(x);
10 10

The variable x here is defined inside the block statement, but because it's declared using var, it's effectively a global variable, available throughout the entire program. This is apparent by the logs made.

Name resolution in JavaScript

All modern-day programming languages, including JavaScript, have sophisticated rules to determine what value to resolve a given name with.

This determination is absolutely crucial for the correctness of the program; ideally if a given name resolves with the correct entity, our program could at least be expected to run correctly (obviously given that the rest of the logic is right).

But if the names get resolved incorrectly, the program would cease to work correctly and we'd only be left with warning and/or error messages to deal with.

In this section, we aim to unravel the simple approach that JavaScript uses to resolve a name with a given entity. Let's begin.

First, let's state the general principle and then move on to consider some examples to help understand it better.

How JavaScript resolves names?

When resolving a name, JavaScript first searches for it in the local environment, if there's any. Then the search moves to the enclosing local environment, if there's any. Then it moves to the second enclosing environment, if there's any, and so on until we end up in the global environment.

If the name exists in the global environment, its value is used for the name's resolution. Otherwise, it's known for sure that the name doesn't exist in any environment whatsoever and so a ReferenceError exception must be thrown.

Python resolves names in the exact same way as JavaScript. In fact, it has fancy name for this approach: the LEGB rule which stands for 'Local, Enclosing, Global and Built-in.'

Resolving a name here refers to both kinds of context, i.e. a name in the 'get' context and a name in the 'assignment' context.

Let's now consider a handful of examples to comprehend this better.

Consider the following code:

var y = 10;

function f() {
   var x = 20;
   console.log(x, y);
}

We refer to a variable x inside the function f() and another variable y as well.

The statement in line 5 can't complete unless JavaScript is done resolving x with a value or maybe throwing an error to signal a reference to a non-existent entity. Here's how x is resolved.

First, search is made for x in the local environment of f(). Because x is indeed found here, the name x in line 5 is resolved with the value of this local x of f().

Simple.

A similar reasoning could be applied for y. That is, first search is made for y in the local environment of f(). Because no match is found here, searching shifts to the enclosing environment. This simply turns out to be the global environment, and here y indeed exists.

Likewise, the reference to y in line 5 gets resolved with the global y.

Once again, simple.

As mentioned before, name resolution isn't just limited to retrieving a particular entity's value — it also applies to cases where we assign a value to the entity.

For example, in the following code, we try to assign values to the same variables from the previous code:

var y = 10;

function f() {
   var x = 20;
   x = 1000;
   y = 2000;
}

Now before it could perform the assignment in either case, JavaScript has to decide which entity is referred to by either name.

  • x, as we know, refers to a local variable, and so the assignment applies to this local variable.
  • y, on the other hand, refers to a global variable, and so the assignment applies to the global variable.

If we slightly modify the code here, the meaning of y could change:

var y = 10;

function f() {
   var x = 20;
   x = 1000;

   var y;
   y = 2000; // Now this applies to a local variable.
}

Now, y is no longer a global variable; it's instead a local variable of f(), and likewise, the assignment in line 9 applies to this local variable. The global variable y would remain as it is.

Let's consider another example.

var b = 50;

function f() {
   var a = 10;

   function g() {
      console.log(a, b);
   }
   g();
}

What do you think would a and b resolve down here with? Well, let's see.

The name a is first searched for in the local environment of g(). Since, it's not found there, searching continues to the enclosing environment, which is the local environment of f(). Here, indeed a variable a is found, which is used to resolve the name a inside g().

As for the name b, the searches in the local environment of g() and the local environment of f() both end up without a match. It's only in the global environment that a match for b is found and that's used to resolve b in line 7 in g().

As you would've started to realize, name resolution isn't difficult business. Just start you way at the local environment of a function, if there's any, and work your way upwards until a match for the name is seen.

Let's consider yet another example, this time slightly complex.

var a = 100;

function f() {
   function g() {
      console.log(a);
   }

   a = 60;
   g();
}

What do you think would the name a resolve down here with? Well, don't get tricked by the assignments here and there.

Let's follow our old and simple resolution rule. First, searching happens in the local environment of g() for an entity named a. Nothing is found, so searching shifts to the enclosing environment, i.e. the local environment of f(). Even here, nothing is found.

But wait. What about the a = 60 statement in line 8? That is surely a part of g() so why don't we consider it?

Well, always remember that the scope of an entity is determined by its declaration only. In this case, a = 60 isn't a declaration; it's merely an assignment of a, which itself refers to a from the global environment.

Coming back to our resolution process, with nothing found in f(), searching moves to the global environment. Here, indeed a match is found for a, and so a in f() is resolved with this global entity.

Prior to the execution of line 8, i.e. a = 60, this global variable a would have a value of 100, but after its execution, it'll become 60.

As soon as the match for a name is found in a given environment, searching effectively ends beyond that point, even if there are more matches upstream. This means that entities in inner-most environments mask the values of entities in outer environments while we access their corresponding names in the inner environments.

This is referred to as entity masking (or variable masking, if we're specifically talking about variables).

In the following code, the local variable x of f() masks the global x inside f():

var x = 10;

function f() {
   var x = 500;
   console.log('From f():', x);
}
f()

console.log('From outside:', x);
From f(): 500 From outside: 10

Masking doesn't just apply to variables — even constants could be masked, as demonstrated below:

const x = 10;

function f() {
   const x = 500;
   console.log('From f():', x);
}
f()

console.log('From outside:', x);
From f(): 500 From outside: 10

This code doesn't throw an error. One might reason that because an identically-named constant is redeclared inside the function, the code should thrown an error.

But, thanks to masking, the constant x declared inside f() is in a separate environment and so doesn't constitute a redeclaration of the global constant x.