Get Your Fundamentals Right: JavaScript Edition
Advancing Your Knowledge from Beginner to Intermediate
Table of contents
- Introduction
- How does Java Script Work?
- How is JS code executed? (Most important topic)
- Hoisting in JavaScript
- How do functions work in JS?
- Undefined vs not defined
- Lexical Environment & Scope Chaining
- let vs const vs var and different types of errors
- Block Scope
- Shadowing in JavaScript
- Closures
- Data abstraction and encapsulation using closures
- Different Function Terminologies and Types
- Higher Order Functions In Js
- Array Methods
- setTimeout + Closures
- Callback Hell
- Promises In JavaScript
- Promise Chaining in JavaScript
Introduction
JavaScript, often labeled as the world's most hated programming language, but you know what? It is also the most loved programming language in the world! In this blog, we're on a mission to make you fall in love with JavaScript. Get ready to master the fundamentals and unlock the true potential of this powerful language.
How does Java Script Work?
Everything in Java Script happens inside an Execution Context. In simple words, the execution context in JavaScript can be thought of as the environment where your code is executed and variables, functions, and other statements are processed.
The execution context consists of two components
Memory component: It includes function arguments, local variables, and function declarations in the form of key:value pairs. It keeps track of all the identifiers and their associated values within the current scope. This memory component is also known as Variable Environment.
Code component: The code component is where the actual JavaScript code is executed line by line, one command at a time, in a specific order. This is also known as the Thread Of Execution.
As mentioned above, the js code runs line by line, executing one command at a time and in a specific order. This characteristic makes JavaScript a Single threaded Synchronous programming language.
What does this mean?
Single Threaded: JS can only execute one command at a time.
Synchronous: The execution of commands/statements occurs sequentially, from top to bottom. Each statement must be completed before the next one starts executing.
Points to remember:
Everything in Java Script happens inside an Execution Context.
Js is a single-threaded synchronous language.
How is JS code executed? (Most important topic)
Let us see what happens when you run the following code
var n = 2;
function square(num){
var ans = num*num;
return ans;
}
var sq2 = square(n);
var sq4 = square(4);
When you run the above code, a global execution context is created. It is created in two phases: the Memory Creation Phase and the Code Execution Phase.
Memory Creation Phase (Variable Environment):
During the Memory Creation Phase, the JavaScript engine sets up the Variable Environment for the global execution context. It scans through the code from top to bottom, line by line, and identifies all the variables and functions declared within the scope.
For variables, memory space is allocated, and they are initialized with the value
undefined
.For functions, the entire function code (the function's definition) is stored in memory as a value associated with its name. This allows functions to be used before they appear in the code because their declarations are also hoisted to the top.
So, in the case of the given code:
Memory space is allocated for the variable
n
, and it is initialized with the valueundefined
.Memory space is allocated for the function
square
and its entire code is stored as a value associated with the namesquare
.Similarly, memory space is allocated for variables
sq2 & sq4
and it is initialized with the valueundefined
.
- Code Execution Phase (Thread Of Execution)
The code is executed line by line, from top to bottom, in a sequential manner.
When function calls are encountered, new execution contexts are created for each function call and added to the call stack. The function's execution context has its own Variable Environment, separate from the global context, where function arguments and local variables are stored.
Continuing with the given code:
The JavaScript engine encounters the statement
var n = 2;
and allocates memory for the variablen
with the value2
.Next, it comes across the function declaration
function square(num) { ... }
. There is nothing to do here as the function is already stored in memory.When
square(n)
is called a new execution context is created for this function call(Memory creation and code execution phase is created for this context as well). The argumentnum
is assigned the value ofn
(which is2
), and the function body is executed.After the first function call is completed, the result
4
is returned and stored in the variablesq2
. The functions execution context is deleted.Similarly, when
square(4)
is called another execution context is created for this function call. The argumentnum
is assigned the value4
, and the function body is executed and the ans is returned. Now execution context is deleted.
Upon execution completion, the global execution context is deleted.
All the things discussed above ie context creation, deletion, and nesting is managed by the JavaScript engine using the call stack. The call stack is a data structure that keeps track of function calls during the execution of a program. Whenever a function is called, its execution context is added to the top of the call stack, and when the function returns, it is removed from the stack. The call stack is initially populated with the Global Execution Context.
Points to remember:
The global execution context in JavaScript is created when a script starts running(1. MemoryCreation phase, 2. Code Execution Phase)
The call stack is used to manage the creation and deletion of execution contexts.
Hoisting in JavaScript
Hoisting in JavaScript is a behavioral characteristic of the language where variables and functions can be used even before they are declared and initialized. However, it's important to note that only the declarations are hoisted, not the initializations or assignments.
Let us understand with an example
Code 2 may appear to trigger an error when run, but JavaScript manages to generate output without any issues. How does this happen? The answer is nothing but hoisting.
But the actual reason can be inferred from the previous section we talked about: How JS code is executed?
The reason lies in the memory creation phase of the global execution context. we learned that in the memory creation phase, the control flows from top to bottom line by line and memory is allocated to each variable and function encountered. So here in code 2
Memory space is allocated for the function
getName
and its entire code is stored as a value associated with the namegetName
.Memory space is allocated from variable
x
and is initialized with the valueundefined
.
So after memory allocation, when the code execution phase begins
getName() -> getName function is called which prints "Hello JavaScript".
console.log(x)-> prints the value of x ie undefined.
console.log(getName) -> prints the content of the function.
In codes 3 & 4, we have just changed the way we declare functions, and we are getting an error. Why?
This is because, in arrow functions and function expression, the function is stored as a variable and not a function. So in the memory creation phase, getName is treated as a variable and initialized with undefined.
Now y'all understand that variables and functions don't just magically rise to the top as others say; y'all know the real reason for how it really works.
How do functions work in JS?
Let us understand this with an example.
Global execution context is created and pushed onto the call stack (x:undefined, a: {code}, b: {code}
1: Value of x is updated to 1
2: function a is invoked -> Execution context of the function is created and pushed onto the call stack. Now again this execution context has two phases. In the memory creation phase, memory is allocated for 'x'. This variable 'x' is completely independent of the x which is created globally. Code inside the function is executed and 10 is logged on the console
3: Execution context of function a is deleted and popped out of the stack. Control moves back to the next line of the global execution context.
4: function b is invoked-> The same process as mentioned in point 2 is repeated. 100 is logged on the console.
5: Execution context of function b is deleted and popped out of the stack. Control moves back to the next line of the global execution context.
6: 1 is logged on to the console
7: Global Execution context is deleted and popped out of the call stack.
Note: The values of variable x are independent of each other since they are declared in different scopes (global scope and function scopes) or different execution contexts. Always a local variable has higher priority than a global variable.
When a variable is accessed within a function, the JavaScript engine first looks for it within the function's local scope/execution context. If the variable is not found locally, it then searches for it in the next outer scope until it reaches the global scope. This is known as scope chaining. We'll examine this closely in the upcoming sections.
Undefined vs not defined
Undefined :
In JavaScript, "undefined" is a special value that indicates a variable has been declared but has not been assigned a value (as seen above during the memory creation phase)
Not-defined:
"Not defined" is a term often used to describe a situation where a variable is used without being declared or initialized in the current scope.
If you try to access a variable that has not been declared or is out of scope, you'll encounter a "ReferenceError," and the console will indicate that the variable is "not defined."
Lexical Environment & Scope Chaining
Lexical Environment: Lexical environment in JavaScript can simply be defined as LOCAL MEMORY + LEXICAL ENVIRONMENT OF PARENT. Whenever an execution context is created, a lexical environment is also created. The lexical environment of a function is shown below in the figure.
Scope: In simple words, scope means where you can access a variable or a function.
Scope Chaining: Scope chaining in JavaScript refers to the process where the JavaScript engine searches for variables in nested or enclosing scopes, moving from the innermost to the outermost scope until the variable is found or the global scope is reached.
Let us understand scope chaining with an example code as shown above.
When the line
console.log(b);
is executed inside functionc
, JavaScript starts looking for the variableb
. It first checks the local scope ofc
, whereb
is not defined.Since
b
is not found in the local scope ofc
, JavaScript follows the scope chain to the outer environment. In this case, the outer environment ofc
is the scope of functiona
.n the scope of
a
JavaScript searches for the variableb
. It's not found here either, so JavaScript continues searching in the next outer environment, which is the global scope.In the global scope, JavaScript finds the variable
b
, which was declared and assigned a value of10
. This is the value that gets logged when theconsole.log(b)
statement is executed.
In summary, scope chaining involves JavaScript searching for a variable in the current scope, and if not found, moving up the scope chain to look for the variable in the enclosing scopes until the global scope is reached. This process ensures that variables can be accessed in the appropriate scope, following the rules of lexical scoping.
let vs const vs var and different types of errors
We will compare the behaviors of let
, const
, and var
in terms of hoisting, declaration, and initialization.
Declaration & Initialization
const
variables must be initialized at the point of declaration, whilelet
andvar
can be initialized later after declaration. Also, const variables cannot be reinitialised.var
: Can be declared multiple times in the same scope without causing errors. Re-declarations are ignored.var a=10; var a=200; //Valid
let
&const
: Allows re-declaration within the same scope, but it will result in an error if re-declaring a variable with the same name in the same block.let a=100; var a=10; //Invalid -> Syntax Error let a = 10; //Invalid -> Syntax Error const b=100; b=10; //Invalid -> TypeError
Hoisting:
let
&const
: Variables declared usinglet
andconst
cannot be accessed before they are initialized with a value. These variables reside in a special place in memory known as the temporal dead zone, during which attempting to access them leads to an error. Once the variables are initialized, they become accessible within the block scope (which will be discussed further in the next section).console.log(a); //ReferenceError:cannot access before initialization let a; a = 100;
var
: Variables declared usingvar
is hoisted to function scope or global scope when declared and can be accessed before initialized.
During our comparison of let
, var
, and const
, we encountered specific types of errors that can occur in JavaScript code. Let's take a closer look at each of these errors:
SyntaxError: A
SyntaxError
occurs when the JavaScript engine encounters code that violates the language's syntax rules. This error indicates that there's something wrong with the way the code is structured or written.ReferenceError: A
ReferenceError
occurs when the JavaScript engine encounters an attempt to reference a variable or function that is not declared or is not in scope.TypeError: A
TypeError
occurs when an operation is performed on a value of an incorrect type. This error indicates that the data type you're trying to use in a certain way is not compatible with that operation.
Block Scope
Before knowing what block scope is, let us first know what block means.
Block: Grouping multiple statements together in a place where JavaScript excepts one single statement. Sounds confusing? Let us see an example.
Eg: if statement: In JavaScript, the JS Engine expects a single statement after an if statement ie
if(condition) statement;
If we want to execute multiple statements when thecondition
is true, we group those statements using an angular bracket after the if statement ieif(condition) {statements...}
The angular brackets{}
represents a block.Block Scope: In simple words, block scope means what all variables and functions can be accessed within the block.
Shadowing in JavaScript
Shadowing in JavaScript occurs when a variable declared within a specific scope has the same name as a variable declared in an outer scope. This leads to the inner variable "hiding" or "shadowing" the outer variable within its scope.
Let us see how shadowing works with var
:
var a = 100;
{
var a = 10;
console.log(a);
}
console.log(a);
OUTPUT:
10
10
var a=10;
inside the block shadows the var a=100;
present outside the block. This is why console.log(a);
inside the block prints 10. But why is console.log(a);
outside the block also printing 10?
This is because var a
in the global scope and block scope, both are pointing to the same address location (because var is function scoped). So var a=10;
inside the is overwriting the variable in the global scope. This is called overriding
Now let us see how shadowing works with let
:
let a = 100;
{
let a = 10;
console.log(a);
}
console.log(a);
OUTPUT:
10
100
var a=10;
declared inside the block shadows the outer variable a
. This inner a
is local to the block scope and has no direct effect on the outer variable because let
is block scoped. console.log(a);
inside the block logs value of the inner a
, which is 10
When we move outside the block, console.log(a)
logs 100
Now let's understand how shadowing works when var
and let
are used together:
Why can't var
shadow let
?
This is because of scoping var
variable. WKT a var
has a functional scope or global scope. In this example, var is defined inside the block, so it has a global scope. This implies that both let
and var
is now present at the same scope ie global scope and in such case let
variable retains precedence.
Scope and shadowing work the same way for arrow functions and function expressions as well.
Closures
A closure is nothing but a function bundled together with its lexical environment. Closure = function + its lexical environment. In JavaScript, closures are created every time a function is created, at function creation time.
Let us understand with an example
function x(){
var a = 7;
function y(){
console.log(a);
}
return y;
}
var func = x();
func();
OUTPUT:
7
We have learned that when a function is called, a new execution context is created and when it completes execution, its execution context is deleted releasing all the memory it occupied.
So according to the above description, when we call the function
x
and it completes execution, the execution context ofx
should be deleted ie all the variables and functions inside it (var a & function y
) are also deleted.
Keeping the above 2 points in mind, let us look into the above code
Here we are returning a function y
from x
and it is stored in the variable func
. So func
stores func = function y(){console.log(a);
and when the function is called func()
, it is trying to print the value of a
. But where is a
, it is already destroyed right as it was a part of the function x
.
The answer to this question is NO, a
was not destroyed when x
completed its execution. Now this is where closures come into the picture. Remember the definition closure = function + its lexical environment. So when a function is returned from another function, it's not just returning the function itself, it's returning the function along with its lexical environment**.** So here when we returned y
from x
, y
is returned along with its lexical environment ie closure of y is returned, which is why it knows the value of a
.
Let's modify the code a little bit
function x(){
var a = 7;
function y(){
console.log(a);
}
a = 10;
return y;
}
x()();
OUTPUT:
10
Remember this point: When we return a function from another function wkt the lexical scope of that function is returned. But the lexical environment does not contain the value of the variables, it contains a reference to it. By that, we mean that address of the variables is returned and not the actual value.
In the above code, we did 2 modifications
We modified the value of
a
to 10 after defining the function y. When we returny
, fromx
, its lexical scope contains a reference toa
which is why 10 is logged to the console and not 7.We used
x()()
. This is another way of calling the returned function, here the first parenthesis is used to call the functionx
and the second parenthesis is used to call the returned function.
Applications of closure: Data encapsulation & abstraction, setTimeout, memoize etc.
Disadvantage of closure: Closed variables are not garbage collected till the program completes execution. This leads to increased memory usage
Data abstraction and encapsulation using closures
Let's directly look at an example
In code snippet 1, we are maintaining a count
which gets incremented when we call the function incriment
. The problem here is that we are using a global variable that can be modified by any other function.
As a solution to it, we use closure to encapsulate and abstract count
and incriment
function. So now, the count
variable can be modified only inside the counter function.
Different Function Terminologies and Types
Function statement OR Function declaration: A function statement is a code construct used to declare a named function.
function abc(){ ...... }
Function expression: A function expression is a way to define a function as a value.
var abc = function(){ ...... }
Anonymous function: An anonymous function is a function defined without a specified name. Anonymous functions do not have their own identity. They can only be used when returning a function from another function (closures) or assigning a function as a value to a variable (function expression) or when passing a function as an argument to another function (callback functions).
Named functions: A function expression in which we name the anonymous function.
var func = function xyz(){ console.log(xyz); } func(); //Valid xyz(); //Invalid
Here
xyz
acts as a local variable and can be accessed only inside the function.Arrow functions: An arrow is a concise way to define a function using the
=>
syntax.var func = (arg1, arg2, ....) => { ...... }
Callback functions: A function that is passed as an argument to another function is called a callback function.
First-class functions or First-class citizens: It is the ability to use functions as values ie function passed as an argument to another function or function assigned to a variable or returning a function from another function.
Higher Order Functions In Js
Functions that take function as an argument or return a function are called higher-order functions.
Let us understand how higher-order functions are used
Task: Write a JS program to find the area, diameter and circumference of circles whose radius is given in an array.
Array Methods
forEach method:
The
forEach
method is a built-in function that allows you to iterate over elements in an array and perform a specified action for each element.const arr = [1,2,3,4,5]; arr.forEach((data, idx) => { console.log(data, idx); });
filter method:
The
filter
method is a built-in function that allows you to create a new array containing all elements from an existing array that meet a certain conditionconst arr = [1,2,3,4,5,6,7,8,9,10]; const evenElements = arr.filter((element) => { return element % 2 === 0; } console.log(evenNumbers); // Output: [2, 4, 6, 8, 10]
The original array is unaffected. For each iteration, a copy of the array element is created. If the callback function returns true, it is included in the new array, else it is discarded.
map method:
The
map
method is a built-in function in JavaScript that allows you to create a new array by applying a specified function to each element of an existing array. It's part of the array prototype and is commonly used for transforming or modifying array elements.const products = [ {name: 'gold star', price:20}, {name: 'mushroom', price:40}, {name: 'green shells', price:30}, {name: 'banana skin', price:10}, {name: 'red shells', price:50} ]; const saleProducts = procuts.map(product => { if(product.price > 30) return {name: produce.name, produce.price/2}; else return product; });
For each iteration, a copy of the array element is NOT CREATED, a reference to the array element is passed. So modification's made to element in the callback function actually modifies the original array. Which is why we are creating a new object inside the callback function and returning instead of modifying the element itself ie
return {name: produce.name, produce.price/2};
instead ofproduct.price = product.price/2;
.reduce method:
The
reduce
method is a built-in function in JavaScript that allows you to iteratively process elements of an array and accumulate a single value based on those elements. It's part of the array prototype and is commonly used for performing calculations or aggregations on array elements.var result = array.reduce(function(accumulator, element, index, array) { // Update the accumulator with the processing of the current element return updatedAccumulator; }, initialAccumulatorValue);
accumulator
: The accumulated value resulting from previous iterations.element
: The current element being processed in the array.index
: The index of the current element in the array.array
: The array that thereduce
method is being called on.initialAccumulatorValue
: The initial value of the accumulator.
Example:
const scores = [10,5,15,25,7,1,3]
const count = scores.reduce((acc, element) => {
if(curr > 10) acc++;
return acc;
}
console.log(count); //Logs count of elements whose score > 10
setTimeout + Closures
setTimeout
is a method that is used to execute a piece of code or function after a specified delay in milliseconds.
Syntax -> setTimeout( callback function, timeout);
function x(){
var i = 10;
setTimeout(function (){
console.log(i);
}, 3000);
console.log("JavaScript");
}
x();
OUTPUT:
JavaScript
10
When function x
is called the variable i
is initialized to 10 and then setTimeout
method is called. The callback function passed forms a closure along with its lexical environment and is stored somewhere else in the browser and starts the timer. As the timer ends (3000ms), the callback function is pushed onto the call stack and executed. But we can see that "JavaScript" is already logged in the console.
Why? This is because setTimeout
is an asynchronous function and doesn't block the execution of the rest of your code. So code after setTimeout
is executed as if setTimeout
never existed.
In summary, the setTimeout
function schedules a task to be executed after a delay, but it doesn't block the rest of the code from continuing to execute. All asynchronous functions work the same way because JavaScript is a single-threaded synchronous programming language.
Callback Hell
Callback hell refers to a situation where multiple callbacks are used, leading to complex and hard-to-read code. This occurs when asynchronous operations are heavily nested within each other.
getUser(userId, function(user){
getPosts(user.id, function(posts){
getComments(posts[0].id, function(comments){
//Do something with comments
});
});
});
In this example, each function relies on the result of the previous function, and they are deeply nested. This can make the code hard to read, debug and maintain. Promises or async/await are better options in such scenarios.
Promises In JavaScript
Promises in JavaScript are a way to handle asynchronous operations.
A promise represents a value that may not be available yet but will be available at some point in the future.
A promise can be in one of three states: pending, resolved or rejected.
const promise = new Promise((resolve, reject) => {
//Perform async operations
//If successful, call resolve(value)
//If failed, call reject(error)
});
resolve & reject are functions that are built into the promise API. resolve is called if an operation is successful and reject is called on failure.
Promise Chaining in JavaScript
As we saw earlier, promises are a better way to deal with asynchronous operations when one operation is dependent on another. That's how we'd implement the same operation as in callback hell.
This sequence of .then()
calls represents promise chaining, where the result of each Promise is passed to the next .then()
in the chain, creating a streamlined flow of asynchronous operations.