DEV Community

Cover image for Scopes
TIMI
TIMI

Posted on

Scopes

Scopes? why?

Scopes in JavaScript, like many other languages, seem easy to grasp on the surface,
You might say:

scopes are containers that make declared variables available only to specific parts of the program.

And you'd be absolutely right, but as my knowledge broadened I understood there are behaviors or quirks we experience due to the underlying process behind scopes and that is what we'll be discussing today.

The Compiling

To fully understand JavaScript scopes we have to understand how the engine handles declared variables and essentially how JavaScript runs.

Though widely thought to be an interpreted language, modern JavaScript engines use a combination of interpretation and compilation processes, churning chunks of code into machine code at run time - a stage called tokenizing or lexing. Tokenizing is the process of breaking down a program's statements and expressions into smaller bits that can be identified and are usable to the engine. Let's look at an example:

//Program before tokenizing

var player= "Mario";

//Program representation after tokenizing

var, player, =, "Mario"
Enter fullscreen mode Exit fullscreen mode

These tokens are parsed to make an AST(Abstract syntax tree) which is a grammatical representation of nested elements from which executed code is generated. All variable declarations are made at this stage, and because code isn't executed yet, our keyword declarations; function and var are initialized and thus accessible before being explicitly author defined. This is how hoisting works.

foo(); //Luigi

function foo(){
   console.log("Luigi")
}

//we seem to access foo() before it is declared because it is hoisted 
Enter fullscreen mode Exit fullscreen mode

You might wonder, what exactly does scope do in all these? Well, in the lexing and initialization stage, the compiler checks to see if a variable currently exists in scope (has been declared previously) by performing a left hand side lookup (left hand is where the variable is defined) of the variable in the closest local scope and works the way up, looking into outer scopes from there till it is at the global scope —this is often referred to as traversing the scope chain— where one of two outcomes is possible;

  1. in strict mode, the program throws a Reference Error.
  2. without strict mode the variable is attached as a global object property with the value defined by the author.

Hoisting

During compilation, the JavaScript engine identifies declarations and their respective scopes before execution..
A good example is

x = "Mario";

var x;

console.log(x) **//Mario; we get "Mario" not undefined**
Enter fullscreen mode Exit fullscreen mode

From this snippet, one might expect that the output should be undefined as code is ran top-down but here's how it really plays out in compilation

var x; // hoisted when compiled

x= "Mario" // left for execution

console.log(x)
Enter fullscreen mode Exit fullscreen mode

As we can see the, the var declaration is sorted to the top and other statements are left for execution.

This also affects function declarations and they take precedence over var declarations.

*If a function and var declaration with the same variable name are present in scope the function takes precedence.

box(); // "Muhammad Ali"

var box  ;

function box(){
  console.log("Muhammad Ali");
};

box = function(){
   console.log("six sides")
}

Enter fullscreen mode Exit fullscreen mode

The JavaScript engine ignores the var box and only function box is recognized and then executed. Any subsequent invocation of box logs "six sides" because the initial invocation takes advantage of hoisting and the order of priority favors functions over var .

box(); // "Muhammad Ali"

var box  ;

function box(){
  console.log("Muhammad Ali");
};

box = function(){
   console.log("six sides")
}

box() // "six sides"
Enter fullscreen mode Exit fullscreen mode

The three pillars of scope: Global, Functions & Blocks

Global - this scope exist as a central storage lookup, where all your environment's built in methods and properties can be referenced, also they serve as backup for "un-scoped" variables.
Common knowledge implies that variables declared outside a function end up in the global scope, which varies according to the environment the program runs in. In a nodeJs application the global scope this would be the global object, while in your browser this would be the window object.

Functions - variables declared within a function are scoped to that function and as such are not accessible from outer scopes.

function scopedFunction() {
  var a = 5
  console.log(a)
  };
}

scopedFunction() // 5

console.log(a) // undefined
Enter fullscreen mode Exit fullscreen mode

As the snippet above shows if we try to access the value of a outside scopedFunction we get undefined. This happens because variable a is private/only accessible to scopedFunction() and when accessed outside the function, the compiler attempts to find the variable a. If unsuccessful, as the variable was not explicitly defined by the author, it creates the variable in the global scope with a value of undefined. In a strict environment, this would throw a Reference Error

Function scopes making variables in the scope available only to that function and its nested scopes are important for avoiding collision avoidance

  • Collision avoidance: theses are techniques used to avoid the unintended clash of variables serving separate purposes, it often results in unintended overwrites.
var x = 10; // Outer scope

function myFunction() {
   x = 20; 
  console.log("Inside myFunction:", x); // Output: Inside myFunction: 20
}

myFunction(); //20
console.log("Outside myFunction:", x);  //20 also
Enter fullscreen mode Exit fullscreen mode

In this code snippet, we see how variables can clash unintentionally and can be an headache to debug, however explicitly declaring
... var x = 20 ... would fix it as this ensures the variable x is declared within myFunction. Renaming the inner variable is another way to fix this collision ... var z = 20 ....

Knowledge of scopes in collision avoidance helps in module management, ensuring only what is necessary is available outside the module to the shared application scope, and other variables are kept private.

  • Variable shadowing- this occurs when a variable in a local scope has the same name as one in the outer scope, resulting in the outer cope's variable being shadowed.

var x = 10;
function example() {
  var x = 20; // Shadows outer x
  console.log(x); // 20
}
console.log(x); //10

Enter fullscreen mode Exit fullscreen mode

This actually helps in eliminating variable collisions but can also become a quirk if not understood.

  • IIFE - this is JavaScript's way of ensuring functions exist as self contained scopes that do not interfere with the parent or global scope. Functions exist in a parent scope and thus can collide with other variables if they share a name. Additionally the function must be explicitly invoked unless they are Immediately Invoked Function Expression (IIFE).
(function IIFE(){
     var a = 5
     console.log(a)
})()
//5 
// this is logged immediately

Enter fullscreen mode Exit fullscreen mode

It looks quite verbose at first glance but lets break it down
** This is an expression not a function declaration, identified by the position of the function keyword. This ensures that the function is not hoisted during the compile process.
** The name of the function (when present) is contained in the ({...}) portion of the expression and so, doesn't pollute the parent scope
** With the name absent in our parent scope, the function invocation cannot be deferred and as such it is invoked immediately, note the () at the end of the expression.

They can also be anonymous functions, although named functions are generally recommended for better error traceability.

(function (){
     var a = "still works"
     console.log(a)
})()
//_still works_ 
// _this is logged immediately_

Enter fullscreen mode Exit fullscreen mode

Blocks - one of the first essentials of scopes is to have your variables as close to your operation as possible, but this can't always be possible when we do not need a function

for(var i; i<10; i++){
   console.log(i)
}
console.log(i) // 10 ...weird
Enter fullscreen mode Exit fullscreen mode

From this snippet, we see that we can access the variable i, though defined in the for loop's parentheses, the var key word is declared in the parent scope and is available in that scope, this a kind of faux-scope as it doesn't serve proximity and pollutes the parent scope.

*try/catch - this is the first example of true block scoping in JavaScript that is still around today,

try {
 undefined(); // illegal operation to force an exception!
}
catch (err) {

 console.log(err) //err}

console.log(err) //undefined
Enter fullscreen mode Exit fullscreen mode

err only exists in the catch block and returns undefined when you try to access it outside that scope.

  • let and const - these keywords, introduced in ES6 made true block scoping in JS possible, the variable declaration keywords attach the variable to the block scope (represented by {})
for (let i=0; i<5; i++) {
 console.log( i );
}
console.log(i) //undefined ..not weird

Enter fullscreen mode Exit fullscreen mode

The snippet above works just as expected, this is because the variable i binds to the loop, and the value is reassigned for each loop.

{
 let x = 2;
 console.log( x ); // 2
}

console.log(x) //undefined
Enter fullscreen mode Exit fullscreen mode

We see the full capability of block scoping here with the {...} here, tying the variable to that block pre ES6 we'd be able to achieve that with this :

try {
 throw 4
} catch(a){
 console.log( a ); // 4
}
console.log(a) //undefined

Enter fullscreen mode Exit fullscreen mode

Yes, that not-so-nice-looking (ugly) piece of code which leverages the catch block scope was the go-to. Thank the core community happy for better times I guess.

The main difference between const and let is that const variables are immutable, while variables declared with let can be reassigned.

*Temporal dead zone - as var declarations are hoisted, so are let and const declarations also hoisted to the top of the block scope however they are not initialized till execution reaches where they were author-defined

{
 console.log(a); // ReferenceError: Cannot access 'a' before 
 initialization
 let a = 10;
 console.log(a); // 10
}

Enter fullscreen mode Exit fullscreen mode

We have seen a couple of behaviors (or misbehaviors) of JavaScript that are related to scopes, including:

  • Hoisting
  • Faux-block scopes
  • Temporal Dead zones
  • Variable shadowing
  • Collision avoidance

All these make the language peculiar and can be harnessed as an advantage if scopes are thoroughly understood(as I hope you have now).

Top comments (0)