Lexical Scope & Scope Chain: Dibs on Variables & Functions

Lexical Scope & Scope Chain: Dibs on Variables & Functions

We all know about variables & functions & their different types, but we all must have been a part of long debugging sessions trying to solve Uncaught ReferenceErrors at some point of our life. This is because, different parts of our program call dibs on certain variables & functions. Let's see how, when & where. Let's start with Lexical Environment.

Lexical Environment

Let's see what does lexical environment mean by taking an example:

var a = "Hello";

b();

function b() {
  var c;
  console.log(a); // "Hello"
}

It is a very simple example & does not require a genius to figure out the output. It prints "Hello" even if the variable is not a part of the function. This is called Lexical Environment.

Note: If you are wondering how can b() execute before it's definition, have a look at Hoisting in JavaScript - More Than "Moving Variables To The Top".

A Lexical Environment is a specification type used to define the association of Identifiers to specific variables & functions based upon the lexical nesting structure of ECMAScript code

Let me elaborate it a little bit. I've explained in my previous article on Execution Context & Call Stack: How JavaScript Code is Executed that JavaScript program has different Execution Contexts (ECs), which handle the memory allocation & execution for different parts of the program. Now here if we see, we will be having 2 ECs:

  1. Global Execution Context (GEC): It contains all the pre-defined variables & functions such as window, document, etc.

  2. Function execution Context (FEC): It contains all the variables & functions defined in the function b().

Now, FEC of b() will have access to the GEC. This means that every child EC has access to it's parent EC. Based on this we can say that - Local environment of a function along with it's parent's environment is known as the lexical environment of that particular function.

So, when the function b() starts executing:

  • It searches for a in it's local lexical environment.

  • When it is unable to find a in it's local environment, it searches for a in it's parent lexical environment.

Below is a visual representation for what I've explained above.

image.png

Now let's modify our program a little bit & add a variable a in function b() as well.

var a = "Hello";

b();

function b() {
  var c;
  var a = "World";
  console.log(a); // "World"
}

console.log(a); // "Hello"

Let's see what happens now:

  • Now, the program will print "World" in the console for the first console statement as it is able to find the variable in it's local lexical scope.

  • But after the function execution, the FEC for b() gets deleted & the control moves back to GEC & it print "Hello" as a in GEC is "Hello".

image.png

Let's complicate things a little bit & debug the code step-by-step.

var hello = "Hello";

console.log(hello); // "Hello"

function a() {
  var world = "World";

  console.log(hello); // "Hello"

  function b() {
    console.log(hello); // "Hello"
    console.log(world); // "World"
  }

  b();
}

a();

console.log(world); // ReferenceError

I've added breakpoints at different lines & we'll have a look at each of them & we'll also look at scope & call stack for each breakpoint.

  1. Breakpoint 1, Line 2: In this line the program starts executing and prints "Hello". The variable is a part of global scope for now.

    • Scope: Here the scope is Global as we're in the GEC for now.

    • Call Stack: Here the call stack is anonymous as we're in GEC.

      image.png

  2. Breakpoint 2, Line 6: In this line, a value is being assigned to the variable.

    • Scope: You can see a new scope is created, known as local, which represents the local scope & the FEC for function a(). In the scope you can see the variables & functions which are created inside the function a().

    • Call Stack: Also, you can see that a is pushed to the call stack as a new FEC is created.

      image.png

  3. Breakpoint 3, Line 8: In this line, "Hello" will be printed in the console, even if hello is not a part of function a(). This is because lexical scope of the function a() has access to the lexical scope of it's parent, which is the Global scope. The status of scope & the call stack will remain the same.

    • Scope: Same as above.

    • Call Stack: Same as above.

      image.png

  4. Breakpoint 4 & 5, Line 10 & 11: In these lines, "Hello" & "World" will be printed even if they are not in function b(). This is because they're part of the parent scopes which are - function a() & Global scope.

    • Scope: Here a new scope is created as Closure (a) because this is a nested function. Closure is a separate topic, more on this later. For now just consider it as a new scope with it's lexical scope containing the parent scopes.

    • Call Stack: Also, you can see b is pushed to the call stack same way a was pushed.

      image.png

  5. Breakpoint 6, Line 20: Here we'll see a Uncaught ReferenceError as the control has shifted back to the Global scope & the variable world is not a part of this scope. This means we cannot access the child scope in the parent scope.

    • Scope: The control has shifted back to the Global scope.

    • Call Stack: As all of the other FECs are removed from the stack, only anonymous is left as we're in GEC now.

      image.png

Scope Chain

In the above example we can see how lexical scopes are created & removed during the execution. The whole process of creating scopes inside scopes is called as Scope Chain. Or in other words you can say that it is a way of managing the scope of the given function during the execution. Pretty simple!

Summary

  1. Lexical Environment or Scope keeps track of variables & functions inside FEC & it's parents ECs.

  2. Every function has access to the variables & functions in it's parent scope.

  3. JavaScript compiler tries to find a variable in the local scope first & if it is unable to find it in the local scope it tries to find the variable in it's parent scope and so on.

  4. Scope chain is a way to manage the scope of a given function & it also keeps track of all the parent scopes as well.