09Apr
JavaScript Scoping
JavaScript Scoping

JavaScript, an adaptable and broadly utilised programming language, enables developers to create interactive web applications. As a developer, it is essential to delve deeper into the core concepts of JavaScript, such as closures and scoping, to create efficient and high-quality code. This article will discuss closures and scoping in detail and elucidate the significance of mastering execution context and variable hoisting.

What is JavaScript Scoping?

Scoping in JavaScript is a set of rules and mechanisms that govern the visibility and accessibility of variables, functions, and objects in the code. Scoping creates a hierarchical structure for variable and function access, which is important for controlling how and where identifiers can be accessed or modified. JavaScript supports two types of scoping: global and local.

There are two primary types of scoping in JavaScript:

  • Global Scope: Global scoping is the accessibility of variables, functions, and objects defined independently of any function or block of code. Global variables, which include functions and blocks, are accessible from anywhere in the code. Global scoping is useful for sharing values between parts of the code; nevertheless, it can be troublesome since variations of global variables can impact the actions of other the code’s components that rely on them. To reduce the possibility of naming conflicts and unintended consequences, it is generally advised to avoid using global variables as much as possible.
var name = "John";

function printName() {
  console.log(name);
}

printName(); // Outputs "John"

In this example, the variable name is defined outside of the printName() function and therefore has global scope. The printName() function can access the variable and print its value to the console.

  • Local Scope: The accessibility of variables, functions, and objects defined within a function or code block is referred to as local scoping. Locally declared variables are only obtainable within the function or block where they are outlined, so access from outside the function or block is not possible. Local scoping can improve code readability and ease of maintenance while reducing labelling conflicts by confining data and logic within specific functions or code blocks. Local scoping in JavaScript is divided into two types: lexical scoping and block scoping.

a. Lexical Scoping: Lexical scoping, also known as static scoping, describes the availability of variables defined in a function’s outer scope. When a variable is cited within a function, JavaScript looks in the function’s local scope first. If the variable is not found in that scope, JavaScript checks the outer scope and repeats the process until the variable is found or the global scope is reached. This is also referred to as variable scoping. Lexical scoping allows for the development of recursive functions that can access variables from their parent functions without having to pass those variables as parameters, resulting in improved code organization and encapsulation.

function outer() {
  var age = 10;
  
  function inner() {
    console.log(age);
  }
  
  inner();
}

outer(); // Output: 10

The function inner() is defined inside the function outer() in this example, resulting in a nested function. Because of lexical scoping, the variable age is defined in the local scope of outer(), and the function inner() can access it. When outer() is called, the variable age is defined and the function inner is called. When inner() is called, the value of age, which is 10, is logged. Because inner() is defined within outer(), it has access to all of outer’s variables,  including age.

b. Block Scope: The availability of variables declared within one block of code, such as those in a for loop or if statement, is referred to as block scoping. JavaScript had only global and function scope prior to ES6, which implied that variables defined inside a code block were indeed accessible outside of that block. The addition of the let and const keywords in ES6 allowed for the creation of block-scoped variables. Variables declared in a block can only be accessed within that block and cannot be accessed outside of that block. This reduces naming conflicts and unforeseen effects caused by customizing variables outside of their intended scope.

function count() {
  let i;
  for (i = 0; i < 3; i++) {
    let j = i;
    console.log(j);
  }
  console.log(i); // i is still accessible here
}

count();

The count() function defines a variable I using the let keyword, which has block scope. Inside the for loop, a new variable j is defined using let, which also has a block scope. Because j is defined within the block of the for loop, it can only be accessed within the loop. This means that each iteration of the loop has its own variable j, which is set to the value of i. When the count() function is called, it logs the j values (0, 1, and 2) for each loop iteration. When the loop is finished, the final value of I is logged, which is 3.

function count() {
  let i;
  for (i = 0; i < 3; i++) {
    let j = i;
    console.log(j);
  }
  console.log(j);
  console.log(i);
}

count();
Count function results
Count function results

For a better understanding, here’s an image illustration of JS scoping

JavaScript scoping

JavaScript scoping

What is the JavaScript Execution Context?

The JavaScript Execution Context is a conceptual framework which describes the eco system in which JavaScript code is assessed and executed. It can be viewed as a container that contains all of the information required for the code to run, such as variable declarations, function definitions, and the existing scope. As a developer, I believe that understanding how the execution context works is critical because it has a direct impact on how your code behaves. Understanding the execution context can assist with creating more efficient and maintainable code, which is particularly crucial when interacting with asynchronous JavaScript code or troubleshooting problems.

There are two main types of execution contexts in JavaScript:

  1. Global Execution Context: The Global Execution Context constitutes the initial execution context created when a JavaScript program starts up (GEC). The GEC is the base context for all code that is not contained within any functions. In other words, it deals with code at the global level. The GEC is associated with the window object in a web browser. This means that every variable or function stated at the global scope becomes a window object property. As an example,
var globalVar = "Hello, World!";
console.log(window.globalVar); // Output: "Hello, World!"

In a Node.js environment, the GEC is associated with the global object instead of the window object:

var globalVar = "Hello, World!";
console.log(global.globalVar); // Output: "Hello, World!"

2. Function Execution Context: A new Function Execution Context (FEC) is created whenever a function is invoked (or called). Because each function call has its own execution context, variables and functional areas declared within a function are inaccessible outside of that function. When functions are called in other functions, these execution contexts can be nested. The FEC is in charge of running the code within the function as well as managing its local variables and arguments. This provides encapsulation and enables each function to have its own private scope. Here’s an example:

function outer() {
  var a = 10;

  function inner() {
    var b = 20;
    console.log(a); // Output: 10 (accessing variable 'a' from the outer function)
  }

  inner();
  console.log(b);
 // Error: 'b' is not defined (variable 'b' is not accessible outside the   inner function)
}

outer();
Outer function results
Outer function results

Components of the Execution Context

Each execution context has three essential components:

  1. Variable Environment: Within the context, this component stores data about variables, functions, and arguments. It contains the Variable Object (VO), which contains the variables and function declarations.
  2. Scope Chain: The availability of variables and functions within the context is determined by this component. It is a top – down chain of all the parent execution contexts accessible from the current execution context. Whenever the JavaScript engine attempts to access a variable or function, it first looks in the Variable Object of the current execution context. If it doesn’t find the identification there, it moves up the Scope Chain, checking the Variable Object of each parent context until it either finds the identifier or reaches the Global Execution Context. A ReferenceError is thrown if the identifier is not found in any context.
  3. The this Binding: The component holds a reference to the object related to the current execution context. Generally, this keyword points to the global object when in a global execution context, such as the window in a browser or global in Node.js. The value of this within a function depends on the method used to call the function.
  • Regular function call: This points to the global object (window or global).
  • Method call (a function within an object): This points to the object that contains the method.
  • Constructor call (using the new keyword): This points to the newly created instance of the object.
  • Event handler: This points to the element that triggered the event.
  • Arrow functions: This is lexically bound, meaning it takes the value of this from its surrounding (enclosing) scope.

Phases of Execution

When JavaScript code is executed, the JavaScript engine goes through two phases:

a. Creation Phase: In this phase, the execution context is set up. The engine scans the code for variable and function declarations and initializes the memory space for them. The variables are initialized with a default value of undefined while functions are stored in their entirety.

b. Execution Phase: In this phase, the engine starts executing the code line by line. It assigns values to variables, invokes functions, and takes care of any expressions or statements found in the code.

What is JavaScript Hoisting?

Variable and function assertions in JavaScript are “hoisted” to the top of their comprising scope before the code is executed during the compilation phase. This means that regardless of whether a variable or function is proclaimed later in the code, it’s able to be utilized before it is physically announced, as long as the variable or function is declared somewhere within the same scope as the variable or function.

Here are some examples of things that can and cannot be hoisted:

Hoisted:

  • Variable declarations using the var keyword
console.log(myVar); // Output: undefined

var myVar = "Hello World!";

console.log(myVar); // Output: "Hello World!"
  • Function declarations (both named and anonymous) using the function keyword.
sayHello(); // Output: "Hello!"

function sayHello() {
  console.log("Hello!");
}
  • Function declarations inside of other functions
function outerFunction() {
  innerFunction();

  function innerFunction() {
    console.log("Hello from innerFunction!");
  }
}

outerFunction(); // Output: "Hello from innerFunction!"

Not Hoisted:

  • Variable declarations using let or const keywords (in block-scoped).
console.log(myVar); // Output: Uncaught ReferenceError: myVar is not defined

let myVar = "Hello World!";

// When using let or const to declare a variable,
// the variable is not hoisted to the top of the scope,
// so trying to use it before its declaration results in an error.
  • Assignments to variables declared with let or const keywords.
let myVar = "Hello World!";

if (true) {
  myVar = "Goodbye World!"; // Re-assigning a value to the variable
}

console.log(myVar); // Output: "Goodbye World!"
  • Function expressions (where a function is assigned to a variable).
sayHello();

let sayHello = function() {
  console.log("Hello!");
};
Cannot access "sayHello" before initialization
Cannot access “sayHello” before initialization
  • Arrow functions (in case of let or const).
console.log(sayHello); // Output: Uncaught ReferenceError: Cannot access 'sayHello' before initialization
const sayHello = () => {
  console.log("Hello!");
};
  •  Class declarations
console.log(myClass); // Output: Uncaught ReferenceError: Cannot access 'myClass' before initialization

const myObj = new myClass(); // Trying to create an instance of the class

class myClass {
  constructor() {
    console.log("Hello from myClass!");
  }
}

What are Closures in JavaScript?

Closures in JavaScript can be better understood by breaking the concept down into simpler parts

  1. Closures are a potent JavaScript attribute that enables functions to keep access to variables in their vicinity scope even after the parent function has finished execution. A closure is a function that “remembers” or keeps access to the variables in its nearby scope even after the function that created it (the parent function) has finished execution. This means that even if the parent function is no longer running, the closure function can still access and use those variables.
  2. This is possible due to JavaScript’s lexical scoping behaviour, which enables functions to ‘remember’ the environment in which they were created.
Using an example to illustrate 
function createGreeting(greeting) {
  return function(name) {
    console.log(greeting + ", " + name);
  }
}

const sayHello = createGreeting("Hello");
const sayHola = createGreeting("Hola");

sayHello("John"); // Output: "Hello, John"
sayHola("Maria"); // Output: "Hola, Maria"

In this example, createGreeting is a function that takes a greeting argument and returns another function that takes a name argument. The returned function logs a message combining the greeting and the name.

When I call createGreeting(“Hello”), it returns a new function that remembers the greeting argument “Hello”. I assign this function to the variable sayHello. When I call sayHello(“John”), it logs “Hello, John”.

Similarly, when I call createGreeting(“Hola”), it returns a new function that remembers the greeting argument “Hola”. I assign this function to the variable sayHola. When I call sayHola(“Maria”), it logs “Hola, Maria”. In both cases, the returned functions (closures) remember their respective greeting values even after the createGreeting function has completed execution. This is the core concept behind closures in JavaScript.

For a better understanding, here’s an image illustration of JS closure

JavaScript closure explanation
JavaScript closure explanation

Common mistakes in JavaScript Scoping and Closures

JavaScript closures and scoping can be difficult concepts to grasp, and developers frequently make mistakes when using them. The following are some examples of common closure and scoping errors:

  1. Creating global variables: When variables are declared globally, they can be accessed from anywhere in the code, which can lead to naming conflicts and unintended side effects. It is best to avoid declaring variables globally unless absolutely necessary.
  2. Not understanding the scope chain: The scope chain determines the order in which JavaScript looks for variables. If a variable is not found in the current scope, JavaScript will look in the next scope in the chain until the variable is found. It is essential to understand the scope chain to avoid unexpected behaviour.
  3. Not properly using closures: Closures are a powerful tool, but they can also be a source of confusion and errors. It is important to understand how closures work and use them properly to avoid memory leaks and unexpected behaviour.
  4. Using the wrong type of scoping: Global scoping should be used sparingly, as it can lead to naming conflicts and unintended side effects. Local scoping should be used wherever possible to limit the scope of variables and functions.

Best Practices

To avoid these common mistakes, it is important to follow best practices when working with closures and scoping in JavaScript:

  1. When possible, use local scoping: I highly recommend using local scoping as much as possible in your code. I find it to be a crucial way to reduce the risk of naming conflicts and unintended side effects. By limiting the scope of variables and functions, you can create a more stable and secure codebase.
  2. Another important aspect of local scoping is avoiding the use of global variables. As a developer, I always try to declare variables with the smallest possible scope to limit their accessibility and avoid any potential naming conflicts. This not only makes my code easier to read and understand but also ensures that it is less prone to errors.
  3. It’s essential to take the time to learn about the scope chain and how to use it when working with closures. As a developer, I have found that understanding the scope chain can be a valuable tool when working with closures. Closures can be powerful, but they can also cause memory leaks and unexpected behavior if used incorrectly.
  4. Finally, I believe that closures should be used sparingly. As a developer, I understand that closures can be an effective tool, but they can also cause problems if used excessively or without proper knowledge. Therefore, it’s crucial to use closures sparingly and ensure that you fully understand how they work.

You can avoid common mistakes and write efficient, maintainable JavaScript code by following these best practices.

Sources

Conclusion

Finally, knowing closures and scoping, as well as execution context and variable hoisting, is critical for writing blunder-free JavaScript code. Developers can create high-quality, standards-compliant applications with powerful functionality by mastering the above concepts. To achieve clean and scalable code, it is critical to follow best practices and avoid common mistakes. With these abilities, developers can advance their JavaScript knowledge and create impressive applications.

Leave a Reply