Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
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.
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.
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:
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.
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();
For a better understanding, here’s an image illustration of JS scoping
JavaScript scoping
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:
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();
Each execution context has three essential components:
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.
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:
console.log(myVar); // Output: undefined var myVar = "Hello World!"; console.log(myVar); // Output: "Hello World!"
sayHello(); // Output: "Hello!" function sayHello() { console.log("Hello!"); }
function outerFunction() { innerFunction(); function innerFunction() { console.log("Hello from innerFunction!"); } } outerFunction(); // Output: "Hello from innerFunction!"
Not Hoisted:
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.
let myVar = "Hello World!"; if (true) { myVar = "Goodbye World!"; // Re-assigning a value to the variable } console.log(myVar); // Output: "Goodbye World!"
sayHello(); let sayHello = function() { console.log("Hello!"); };
console.log(sayHello); // Output: Uncaught ReferenceError: Cannot access 'sayHello' before initialization const sayHello = () => { console.log("Hello!"); };
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!"); } }
Closures in JavaScript can be better understood by breaking the concept down into simpler parts
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 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:
To avoid these common mistakes, it is important to follow best practices when working with closures and scoping in JavaScript:
You can avoid common mistakes and write efficient, maintainable JavaScript code by following these best practices.
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.