Scope Chain & Lexical Environment in Javascript

Scope Chain & Lexical Environment in Javascript

Scopes and Lexical Environment are a fundamental concept of JavaScript that every JavaScript developer should know and understand. Yet, it’s a concept that confuses many new JavaScript developers.

So in this article, I will try to explain all these concepts and how they really work in JavaScript.

Scope

Scope in JavaScript refers to the current context of code, which determines the accessibility of variables to JavaScript.

There are two types of scope in JS:

  • Global Scope

  • Local Scope

Points to remember:

  • Variables defined inside a function are in the local scope
  • Variables defined outside of a function are in the global scope.
  • Each function when invoked creates a new scope.

Global Scope

When you start writing JavaScript in a document, you are already in the Global scope. There is only one Global scope throughout a JavaScript document. A variable is in the Global scope if it's defined outside of a function.

var language = 'JS';

Variables inside the Global scope can be accessed and altered in any other scope.

var language = 'JS';

function handleChange(){
    console.log("Inside Function before modification-> ", language);
    language = 'PHP';
    console.log("Inside Function after modification-> ", language);
}

handleChange();

console.log("Outside function-> ", language);

image.png

Local Scope

Variables defined inside a function are in the local scope. And they have a different scope for every call of that function. This means that variables having the same name can be used in different functions. This is because those variables are bound to their respective functions, each having different scopes, and are not accessible in other functions.

var language = 'JS';

function a() {
    var language = 'PHP'
    console.log("Inside Function a()-> ", language); //Output: PHP
    function b() {
        var language = 'C++'
        console.log("Inside Function b()-> ", language); // Output: C++
    }
    b();
}

a();

console.log("Outside function-> ", language); //Output: JS

image.png

This also tells us that variables having the same name in different execution contexts gain precedence from top to bottom of the execution stack. A variable, having a name similar to another variable, in the innermost function (topmost context of the execution stack) will have higher precedence.

Block Statements

Block statements like if and switch conditions or for and while loops, unlike functions, don't create a new scope. Variables defined inside of a block statement will remain in the scope they were already in.

if(true){
    var language = 'JS';
}

console.log(language); // Output: JS

Contrary to the var keyword, the let and const keywords support the declaration of local scope inside block statements.

if (true) {
    var language = 'JS';
    const language2 = 'PHP';
    let language3 = 'C++';

    console.log("===Inside Block Statement===")
    console.log(language); // Output: JS
    console.log(language2); // Output: PHP
    console.log(language3); // Output: C++
}

console.log("===Outside Block Statement===")

console.log(language); // Output: JS
console.log(language2); // Output: ReferenceError
console.log(language3); // Output: ReferenceError

image.png

Global scope lives as long as your application lives. Local Scope lives as long as your functions are called and executed.

Scope Chain

The Scope Chain is the hierarchy of scopes that will be searched in order to find a function or variable.

image.png

In the creation phase of the execution context, the scope chain is created after the variable object. The scope chain itself contains the variable object.

The Scope Chain is used to resolve variables. When asked to resolve a variable, JavaScript always starts at the innermost level of the code nest and keeps jumping back to the parent scope until it finds the variable or any other resource it is looking for. The scope chain can simply be defined as an object containing the variable object of its own execution context and all the other execution contexts of its parents, an object having a bunch of other objects.

Lexical Scope

Lexical Scope means that in a nested group of functions, the inner functions have access to the variables and other resources of their parent scope. This means that the child's functions are lexically bound to the execution context of their parents. Lexical scope is sometimes also referred to as Static Scope.

function a() {
    var language = 'PHP'
    console.log("Inside Function a()-> ", language); //Output: PHP

    function b() {

        console.log("Inside Function b() before modification-> ", language); // Output: PHP
        language = 'C++'
        console.log("Inside Function b() after modification-> ", language); // Output: C++

        var count = 3;
        console.log("Count inside b()-> ", count); //Output: 3
    }

    b();
    console.log("Inside Function a() after modification-> ", language); // Output: C++
    console.log("Count inside a()-> ", count); //Output: ReferenceError
}

a();

image.png

Lexical Scope works in a forward manner.

language can be accessed by its children's execution contexts. But it doesn't work backward to its parents, meaning that the variable count cannot be accessed by its parents.

Lexical Environment

Every time the JavaScript engine creates an execution context to execute the function or global code, it also creates a new lexical environment to store the variable defined in that function during the execution of that function.

A lexical Environment is a data structure that holds an identifier-variable mapping. (here identifier refers to the name of variables/functions, and the variable is the reference to actual object [including function type object] or primitive value).

A Lexical Environment has two components:

  • Environment record: is the actual place where the variable and function declarations are stored.
  • Reference to the outer environment: means it has access to its outer (parent) lexical environment.

A lexical environment conceptually looks like this:

lexicalEnvironment = {
  environmentRecord: {
    <identifier> : <value>,
    <identifier> : <value>
  }
  outer: < Reference to the parent lexical environment>
}

let's try to understand this using a simple example:

let language = 'JS';
function a() {
  let b = 25;  
  console.log('Inside function a()');
}
a();
console.log('Inside global execution context');

When the JavaScript engine creates a global execution context to execute global code, it also creates a new lexical environment to store the variables and functions defined in the global scope. So the lexical environment for the global scope will look like this:

globalLexicalEnvironment = {
  environmentRecord: {
      language    : 'JS',
      a : < reference to function object >
  }
  outer: null
}

Here the outer lexical environment is set to null because there is no outer lexical environment for the global scope.

When the engine creates an execution context for a() function, it also creates a lexical environment to store variables defined in that function during the execution of the function. So the lexical environment of the function will look like this:

functionLexicalEnvironment = {
  environmentRecord: {
      b    : 25,
  }
  outer: <globalLexicalEnvironment>
}

The outer lexical environment of the function is set to the global lexical environment because the function is surrounded by the global scope in the source code.

When a function completes, its execution context is removed from the stack, but its lexical environment may or may not be removed from the memory depending on if that lexical environment is referenced by any other lexical environments in their outer lexical environment property.

Wrap Up!!

Thanks for reading!! I know this is a lot to consume in a single article. I hope it was helpful to some extent. Please share it with your network. Don’t forget to leave your comments below.

Buy-me-a-coffee