Deep Dive into JavaScript

Kishan Patel
23 min readMay 27, 2023

Javascript is a synchronous single-threaded language. It means JS can execute single command at a time in a specific order.

Highlights

How does JS code run?

Let’s take a simple example

var n = 2;
function square (num) {
var ans = num * num;
return ans;
}
var square2 = square(n);
var square4 = square(4);

When we run JS code it creates a Global Execution Context(GEC) inside the call stack. The GEC was created in two-phase those are

  1. Memory creation phase

In this phase, all the variables and functions are stored in memory. But the variable is stored with undefined value and functions are stored with all code copied to memory.

|----------------------|-------------------------------|
| Memory | Code |
|----------------------|-------------------------------|
| n:undefined | |
| square: {...all code | |
| copied to memory } | |
| | |
| square2: undefined | |
| square4: undefined | |
|----------------------|-------------------------------|

Then, a new execution context is created for var square2 = square(n);, inside the code block, and all the new variables and functions are stored in memory inside the code creation phase.

|----------------------|-------------------------------|
| Memory | Code |
|----------------------|-------------------------------|
| n:undefined | | Memory | Code | |
| square: {...all code | |----------------|---------| |
| copied to memory } | | ans: undefined | | |
| | | | | |
| square2: undefined | |----------------|---------| |
| square4: undefined | |
|----------------------|-------------------------------|

The Execution context will be deleted after the execution of the code. So the variable square2 will be stored in memory with the value 4.

And same for var square4 = square(4); a brand new execution context is created for this line again and will be deleted after the execution of the code.

And finally, the GEC will be deleted after the execution of the code.

Hoisting

Now we know how Javascript executes the code, so now with that let’s check what is Hoisting.

var x = 10;

function sayHello() {
console.log('Hello there!');
}

console.log(x); // 10
sayHello(); // Hello there!

This is a simple example where we have defined a variable x and a function sayHello() and later we are executing it.

Now let’s call the function before its declaration and log the variable.

console.log(x); // undefined

sayHello(); // Hello there!

var x = 10;

function sayHello() {
console.log('Hello there!');
}

As we know when a JS code is executed it creates a GEC with memory and code. And the first phase stores undefined for variables and stores the whole code block for functions inside the memory.

It means, even before the code starts executing memory is allocated to all variables and functions. that’s why even before defining the variable and function, we can get the value from memory.

In our case, we get the value of x as undefined and we got Hello there! from function.

|----------------------|---------------|
| Memory | Code |
|----------------------|---------------|
| x:undefined | |
| sayName: {... stores | |
| all code of func | |
| inside the memory } | |
| | |
|----------------------|---------------|

but, it behaves differently for let and const .

let and const declarations are also hoisted to the top of their scope, but unlike var, they are not initialized inside the global object. Instead, they enter a temporal dead zone until the point of the declaration in the code. This means that if you try to access a let variable before its declaration, you will get a ReferenceError (Uncaught ReferenceError: Cannot access ‘xxx’ before initialization)

So we can saytemporal dead zone is the time from when the let/const variable was hoisted until it is initialized with some value.

Another example

var x = 1;
a(); // 10
b(); // 100
console.log(x); // 1

function a() {
var x = 10;
console.log(x);
}

function b() {
var x = 100;
console.log(x);
}

Lexical scope and chain scope

Lexical env is created whenever an execution context is created, and lexical env is a memory along with the reference to the lexical env of the parent.

Here function b is lexically inside function a and function b has reference to the function a’s lexical environment.

function a() {
var x = 10;
function b() {
console.log(x);
}
b();
}
a(); // 10

Chain scope

The chain scope is the scope of a function along with the scope of it’s parent and grandparent and so on.

Example:

function a() {
var x = 10;
function b() {
function c() {
console.log(x);
}
c();
}
b();
}
a(); // 10

In this example inside function c, we have console.log(x), first, it will try to get the variable inside function c if it does not exist there then it will go to function b and then function a and then global scope. This is called the scope chain.

Let’s see it in the browser

Another example

let a = 10;
var b = 100;

If we will run the above code, the var b is attached to the window object, but in case b it is not.

Let, var & const

let’s understand through an example

console.log(a); // undefined -> no error for var
var a = 10;
var a = 100; // no error at this line

console.log(b); // Uncaught ReferenceError: Cannot access 'a' before initialization
let b = 10;
let b = 100; // Error: Uncaught SyntaxError: Identifier 'a' has already been declared

// Same for const
const c = 10;
const c = 100; // Error: Uncaught SyntaxError: Identifier 'a' has already been declared


// ****Difference between let and const****

// 1 > We can re assign a value to a let variable but not to a const variable;

b = 100; // no error
c = 100; // Error: Uncaught TypeError: Assignment to constant variable.

// 2 > In case of let we can define a variable and later we can assign a value
// but we can't do for const
let b2;
b2 = 20;

const c2; // Uncaught SyntaxError: Missing initializer in const declaration

// 3> We can change the value of let but can't change value of const.

let b3 = 30;
b3 = 40;

const c3 = 25;
c3 = 35; // Uncaught TypeError: Assignment to constant variable.

Block, scope, and shadowing

A block is surrounded by { and } .

{
....
}

Let’s see how var, let and const are hoisted.

We can’t access block-scoped variables outside of the block,

{
var a = 10;
let b = 20;
const c = 30;
}

console.log(a); // 10
console.log(b); // Uncaught ReferenceError: b is not defined
console.log(c); // Uncaught ReferenceError: c is not defined

shadowing

When a variable declared in a certain scope (e.g. a local variable) has the same name as a variable in an outer scope (e.g. a global variable) then it’s called shadowing.

Let’s understand this through example,

var a = 100;
let b = 200;
const c = 300;
console.log("a: ", a); // 100

{
var a = 10; // a is shadowed in the block
let b = 20;
const c = 30;
console.log("a: ", a); // 10
console.log("b: ", b); // 20
}

console.log("a: ", a); // 10 -> var a is shadowed inside the block and
//the value is updated because, they both are pointing to same memory location

console.log("b: ", b); // 200 -> let b is shadowed in the block but,
// the value does not changed, because they both are pointing to
// different memory location

console.log("c: ", c); // 300

Let’s see how it looks in the browser

Other use cases

// Use case 1
let a = 10;
{
var a = 20; //Uncaught SyntaxError: Identifier 'a' has already been declared
}

// Use case 2
var b = 10;
{
let b = 20;
console.log(b); // 20
}
console.log(b); // 10

// Use case 3
let a = 10;
{
const a = 20;
console.log(a); // 20
}
console.log(a); // 10

// Below code is a valid shodowing
const c = 10;
{
const c = 20;
{
const c = 30;
console.log(c); // 30
}
console.log(c); // 20
}
console.log(c); // 10

Closures

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.

Example 1:

Example 2:

function x() {
var a = 7;
function y() {
console.log(a); // 7, "a" is a closure, will learn closure later
}
return y;
}
var z = x();

z();

In this program, we are returning a function y from x. After returning the function y, the execution context (EC) of x will be popped off the call stack. But we can still access the variable a from the function y. This is called closure. Function y remembers where it came from through closure.

Let’s see it on the browser

Example 3:

In e.g 3, function y is inside x() and the function x is inside z(). And we are trying to access var b from z() and var a from x() , and we can access it through closures.

Even if you return the function y from line 5 and execute outside, it will still retain the value of a and b.

Usages of closures:

  • Module design pattern
  • Currying
  • Functions like one
  • Memoize
  • Maintaining state in async world
  • setTimeouts
  • Iterators
  • and many more…

Disadvantage of closures

  1. Memory Consumption: Closures can lead to increased memory consumption. When a closure is created, it retains references to its outer variables, preventing them from being garbage collected. If closures are created within a loop or in a long-running process, they can accumulate and consume significant memory.
  2. Performance Impact: Closures can have a performance impact, particularly when they are used in nested or deeply nested functions. Accessing variables from outer scopes requires traversing the scope chain, which can be slower compared to accessing variables within the same function scope.
  3. Potential Memory Leaks: Closures can inadvertently lead to memory leaks if not used carefully. If a closure retains references to objects or resources that are no longer needed, those objects will not be garbage collected, resulting in memory leaks.
  4. Readability and Maintenance: Overuse or misuse of closures can make code more difficult to read, understand, and maintain.

Closures with setTimeout

Let’s try to print incremented counter after every 1 second,

for (var i = 1; i <= 5; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
// output after every one sec: 6, 6, 6, 6, 6, 6

To fix the above problem we can use let instead of var in the above program, but we can also do it with var using closure .

for(var i=1; i < 6; i++) {
function counter(x) {
setTimeout(() => {
console.log(x);
}, x * 1000)
}
counter(i);
}
// 1, 2, 3, 4, 5

Example of how closure is different than lexical env

Example of lexical env

function z() {
var b = 900;
function x() {
var a = 7;
function y() {
console.log(a, b);
}
y();
}
x();
}

z();

Example of closure

function z() {
var b = 900;
function x() {
var a = 7;
function y() {
console.log(a, b);
}
return y;
}
x();
}

var inner = z();
inner();

After returning the function y the function z and x will be removed from the call stack but we can still access through closure. (can refer to 2nd example for more details).

Closure as data hiding

Example 1:

function counter() {
var count = 0;
function increment() {
return count++;
}
increment();
}

console.log(count); // can't access count here
counter();
console.log(count); // can't access count here either

We can’t access the var count outside of the function because before calling the function the EC has not been created so the var x has not hoisted and initialized. And we can’t call it after the function call because once the function execution is finished the EC of the function will be deleted and no longer available.

Example 2:

function counter() {
var count = 0;
return function increment() {
return count +=1;
}
}

var counter1 = counter();
console.log(counter1()); // 1
console.log(counter1()); // 2

var counter2 = counter(); // new instance
console.log(counter2()); // 1

console.log(counter1()); // 3
console.log(counter1()); // 4

console.log(counter2()); // 2

Closure with constructor functions

function Counter() { // since it's constructure func name start with caps
var count = 0;
this.incrementCounter = function() {
return count +=1;
}
this.decrementCounter = function() {
return count -=1;
}
}

var counter1 = new Counter(); //

console.log(counter1.incrementCounter()); // 1
console.log(counter1.incrementCounter()); // 2
console.log(counter1.decrementCounter()); // 1

We can do the same thing in class

  • The first way
class Counter {
constructor() {
var count = 0;
this.incrementCounter = function () {
return count += 1;
};
this.decrementCounter = function () {
return count -= 1;
};
}
}

var counter1 = new Counter();

console.log(counter1.incrementCounter()); // 1
console.log(counter1.incrementCounter()); // 2
console.log(counter1.decrementCounter()); // 1
  • Second way
class Counter {
constructor() {
this.count = 0;
}
incrementCounter() {
return this.count += 1;
};
decrementCounter() {
return this.count -= 1;
};
}

var counter1 = new Counter();

console.log(counter1.incrementCounter()); // 1
console.log(counter1.incrementCounter()); // 2
console.log(counter1.decrementCounter()); // 1

Currying

Currying in JavaScript is a process in functional programming in which you can transform a function with multiple arguments into a sequence of nesting functions.

Currying can be achieved in two ways

  • Currying through bind()
function multiply(x, y) {
console.log(x * y);
}

// multiply(2, 3);

const multiplyByTwo = multiply.bind(this, 2);
multiplyByTwo(3);

const multiplyByThree = multiply.bind(this, 3);
multiplyByThree(10);
  • Currying through closure
function mul(x) {
return function(y) {
console.log(x * y);
}
}

const mulByTwo = mul(2);
mulByTwo(3);

const mulByThree = mul(3);
mulByThree(10);

Event Loops

The event loop continuously checks for pending tasks in the callback queue, if there is a callback in the callback queue it moves to thecall stack if the call stack is empty.

And in case there are no pending events or callbacks, the event loop will wait for events to occur.

Let’s understand this through an example,

console.log("start");

setTimeout(() => {
console.log("Callback called!");
}, 5000);

console.log("End");
  1. The code execution starts, and “start” is logged to the console.
  2. The setTimeout function is called. It registers the callback function inside web API env and sets a timer for 5000 milliseconds.
  3. The event loop continues to the next line without waiting for the timer to finish.
  4. “End” is logged to the console.
  5. The event loop keeps running and checks if there are any pending tasks.
  6. After 5000 milliseconds, the timer for setTimeout completes, and the callback function is added to the task queue.
  7. The event loop checks the task queue and finds the callback function.
  8. The callback function is then executed, and “Callback called!” is logged to the console.

The Event loop has two types of task queue

  1. Microtask Queue
    * process.nextTick()
    * Promise callback
    * async callback
  2. Callback Queue (Macrotask Queue)
    * setTimeout()
    * setInterval()
    * setImmediate()

Microtask Queue has higher priority than Callback Queue. The Event loop executes the tasks in the following order:

  • Executes all the tasks in the Microtask Queue.
  • Executes the oldest task in the Callback Queue.

Starvation in the Event loop

If the Callback Queue has a large number of tasks, then the tasks in the Microtask Queue will be executed only after the execution of all the tasks in the Callback Queue. This will cause starvation in the Event loop.

Web APIs

Web APIs are provided by the web browser environment. These APIs are built into the browser and enable JavaScript to interact with various aspects of the web platform, such as manipulating the DOM (Document Object Model), making HTTP requests, handling timers and events, accessing browser storage, and more.

Web APIs are not part of the core JavaScript language, but they are exposed to JavaScript through the browser environment. They provide additional functionalities and allow JavaScript to interact with the browser and its surrounding environment.

Commonly used web APIs

  • setTimeout (timer API)
  • console.log (console API)
  • DOM API
  • Fetch API
  • Geolocation API
  • localstorage, sessionStorage (Web Storage API)
  • Web Audio API
  • Canvas API

Check out here to see all web APIs.

Callbacks

A callback function is a function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action.

function greeting(name) {
alert(`Hello, ${name}`);
}

function processUserInput(callback) {
const name = prompt("Please enter your name.");
callback(name);
}

processUserInput(greeting);

Examples of predefined methods which has the callback

setTimeout(callBackFunc, 100)
btn.addEventListener('click', callBackFunc);
array1.map(function callbackFunc(el) {} )
array1.filter(function callbackFunc(el) {} )
array1.forEach(function callbackFunc(el) {} )

Advantage of Callback

  • As we already know, Javascript is a synchronous & single-threaded language, but through the callbacks, we can perform asynchronous operations.

Limitation of callback

  • Callback hell

Callback hell, also known as the pyramid of doom, is a situation that arises in asynchronous programming when there are multiple nested callbacks. It occurs when callbacks are chained together, resulting in deeply nested and hard-to-read code structures. This can make the code difficult to understand, maintain, and debug.

  • Inversion of control

Whenever we have the callback function and we pass it to some other function we are giving control of our function to another function and we don’t know what’s happening behind the scenes now. It may not call out the callback function or may call multiple times.

Promise

A Promise is an object representing the eventual completion or failure of an asynchronous operation.

Promises are used to handle asynchronous operations in JavaScript. They are easy to manage when dealing with multiple asynchronous operations where callbacks can create callback hell leading to unmanageable code.

Components of Promise

  • State
  • Result (the result of promise is immutable)

A promise has three states:

  • pending: the promise is still in the works
  • fulfilled: the promise resolves successfully and returns a value
  • rejected: the promise fails with an error

How promise look like,

createOrder(cart)
.then((orderId) => {
return fetchPaymentDetails(orderId);
})
.then((paymentDetails) => {
return processPayment(paymentDetails);
})
.then((result) => {
return showSuccessMessage(result);
})

Example 1:

// producer
const examResult = new Promise((resolve, reject) => {
const res = Math.floor((Math.random() * 100) + 1);
if(res >= 50 ) {
resolve(`Pass: ${res}`)
} else {
const err = new Error(`Failed: ${res}`);
reject(err); // rejection should be an error
}
})

// consumer
examResult
.then(res => console.log(res))
.catch(err => console.error(err.message))
.finally(() => console.log('Done!'))

Example of promise

const cart = ['Shoes', 'Shirt', 'Pant']

createOrder(cart)
.then((orderId) => {
return proceedToPayment(orderId); // keep returning promise to chain
})
.then((paymentId) => {
console.log('Payment Successfull with paymentId:', paymentId);
})
.catch((err) => { // catch error from any of the above promise
console.log(err.message);
})
.finally(() => { // finally block will always execute
console.log('End of order!');
})


function createOrder(cart) {
return new Promise((resolve, reject) => {
// Validate cart
if(cart.length >= 3) { // modify this to test error
const orderId = '12345';
resolve(orderId);
} else {
const err = new Error('Invalid cart, Order not created!');
reject(err);
}
})
}

function proceedToPayment(orderId) {
return new Promise((resolve, reject) => {
// Validate orderId
if(orderId === '12345') { // modify this to test error
const paymentId = '67890';
resolve(paymentId);
} else {
const err = new Error('Invalid orderId, Payment not processed!');
reject(err);
}
})
}

You can learn more about Promise APIs in my separate article here.

Debouncing & Throttling

Debouncing and throttling are two ways to limit the rate of function calls. They are used to improve performance and user experience.

Let’s understand them one by one.

Debouncing

Debouncing is a programming practice used to ensure that time-consuming tasks do not fire so often, that it stalls the performance of the web page. In other words, it limits the rate at which a function gets invoked.

Debouncing checks if the same function is being called again within a certain time frame, it cancels the previous call and starts over again.

Example:

const getData = () => {
// calls an API and gets Data
searchInput = document.getElementById('search');
console.log("Fetching Data ..", searchInput.value);
}

const debounce = function (getDataCallback, delay) {
let timer;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
getDataCallback();
}, delay);
}
}

const debounceInput = debounce(getData, 300);

/* <input type="text" id="search" onkeyup="debounceInput()" /> */

In the above example, we are calling debounceInput() on every keyup event but debounceInput() will not call getData() until 300ms after the last keyup event.

That means if you are continuously typing in the input box, getData() will not be called until you stop typing for 300ms.

Throttling

Throttling is a technique in which, no matter how many times the user fires the event, the attached function will be executed only once in a given time interval.

const getData = () => {
// calls an API and gets Data
searchInput = document.getElementById('search');
console.log("Fetching Data ..", searchInput.value);
}

const throttle = function (getDataCallback, delay) {
let flag = true;
return function () {
if (!flag) return;
flag = false;
setTimeout(() => {
flag = true;
getDataCallback();
}, delay);
}
}

const throttleInput = throttle(getData, 300);

/* <input type="text" id="search" onkeyup="throttleInput()" /> */

async and defer

In HTML, the <script> tag is used to include JavaScript code within an HTML document. Both the async and defer attributes can be used with the <script> tag to control how the script is loaded and executed.

  1. Async attribute: When you add the async attribute to the <script> tag, it indicates that the script should be executed asynchronously. This means that the script will not block the rendering of the HTML document while it is being loaded and executed. The browser will continue parsing the HTML and rendering the page, and when the script is fully loaded, it will be executed at the first available opportunity.

<script src="script.js" async></script>

  1. Note that the order of execution may not be guaranteed if you have multiple scripts with the async attribute. They may be executed as soon as they are loaded, without waiting for the previous scripts to finish loading or executing.
  2. Defer attribute: When you add the defer attribute to the <script> tag, it indicates that the script should be deferred. This means that the script will be loaded in the background while the HTML document is being parsed, but its execution will be deferred until the HTML parsing is complete. The scripts with the defer attribute will be executed in the order they appear in the HTML document.

<script src=”script.js” defer></script>

  1. Unlike the async attribute, the defer attribute ensures that the scripts are executed in the order they are specified in the HTML document, without blocking the rendering of the page.

Both async and defer are used to improve the loading and execution of JavaScript code, especially when you have scripts that are not critical for the initial rendering of the page. They can help improve page loading speed and overall performance. The choice between async and defer depends on the specific requirements of your script and how it interacts with the rest of the page.

Prototype

All JavaScript objects inherit properties and methods from a prototype.

Array.prototype.printData = function (data) {
console.log(data);
}

const arr = [1, 2, 3, 4, 5];
const arr2 = [10, 20, 30];


arr.printData('Hello');

arr.__proto__.showLength = function () {
console.log(`Length of array is ${this.length}.`);
}

arr.showLength(); // arr is able to access showLength method

arr2.showLength(); // arr2 is also able to access showLength method

Add a prototype of one object to another object

const obj = {};
const parent = { foo: 'bar' };

console.log(obj.foo);
// Expected output: undefined

Object.setPrototypeOf(obj, parent);

console.log(obj.foo);
// Expected output: "bar"

Prototype Chain

The prototype chain is used to build a chain of objects to find a property or method.

const arr = [1, 2, 3, 4, 5];
arr.__proto__.__proto__.__proto__; // this is prototype chain

More details

// Array proptotype
const arr = [1, 2, 3, 4, 5];

console.log(arr.__proto__ === Array.prototype); // true
console.log(arr.__proto__.__proto__ === Object.prototype); // true
console.log(arr.__proto__.__proto__.__proto__); // null


// Object prototype
const obj = {
name: 'John',
age: 32,
}

console.log(obj.__proto__ === Object.prototype); // true
console.log(obj.__proto__.__proto__); // null


// Function Prototype
function fun() {};

// Both are same
console.log(fun.__proto__ === Function.prototype);

// Both are same
console.log(fun.__proto__.__proto__);
console.log(Object.prototype);

console.log(fun.__proto__.__proto__.__proto__); // null

Functions in Javascript

Higher order function

A function that takes another function as an argument or a function that returns a function is called Higher order function.

// Example 1
function a() {
console.log('a');
}

function b(a) { // Here b is an higher order function
a();
}
b();

// Example 2

function x() { // Here x is an higher order function
return function y() {
console.log('y');
}
}

Real use case of higher order function

const radius = [3, 1, 2, 4];


const area = function (radius) {
return Math.PI * radius * radius;
}

const circumference = function (radius) {
return 2 * Math.PI * radius;
}

const diameter = function (radius) {
return 2 * radius;
}

// Higher order function && Polyfills for map
const calculate = function (radius, logic) { // Higher order function
const output = [];
for (let i = 0; i < radius.length; i++) {
output.push(logic(radius[i]));
}
return output;
}

console.log(calculate(radius, area));
console.log(calculate(radius, circumference));
console.log(calculate(radius, diameter));

Here we just created a logic for the map, above calculate method will do the same job as the map. So we can say calculate is the polyfills for map

console.log(radius.map(area)); //

Function Statement or function declaration

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

Function Expression

var x = function a() {
console.log("Hello!");
}

Function Statement Vs Function Expression

We can call the function statement before declaring because of hoisting (the whole function body is stored in memory).

It is not possible in function expression since variable x is hoisted as undefined.

a();
x();


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

const x = function b() {
console.log("Hello!");
}

Anonymous functions

A function without a name is called an Anonymous function.

The anonymous function must be assigned to a variable to be called later otherwise it will raise an error.

var x = function () {
// ...
}

// or in arrow func

var y = () => {
// ...
}

First-class function or First-class citizens

The ability of a function to be used as a value and return a function is called a first-class function.

function a() {
console.log('a called.');
}

const b = function (funcA) {
funcA();
return () => {}
}

b(a);

Pure Function

A pure function is a function that returns the same result if the same arguments(input) are passed to the function.

  • Return value should only be dependent on the input
  • It should not modify any non-local state.
  • The function should not have any side effects, such as reassigning non-local variables, mutating the state of any part of code that is not inside the function, or calling any non-pure functions inside it.

Example:

// Below function will give same output for same input each time
function operationAdd(a, b){
return a+b;
}


// It is a pure function because it is not modifying the original array.
const ary2 = [10, 20, 30];
function addElement(ary, element) {
return [...ary, element];
}

Let’s also see few example of non pure function

// It is not a pure function because it is modifying the original array.
const ary1 = [1, 2, 3, 4, 5];
function addElement(ary, element) {
ary.push(element);
}

/* It is not a pure function because it is returning
different output for same input. */
function add(a, b) {
return a + b + Math.random();
}

IIFE — Immediately Invoked Function Expression

IIFE is a function that runs the moment it is invoked or called in the JavaScript event loop. Having a function that behaves that way can be useful in certain situations. IIFEs prevent pollution of the global JS scope.

// Example 1
(function () {
console.log('Hello World!');
})();

// Example 2
(favNumber = function(num = 3) {
console.log(`My favourite number is ${num}`);
})();


favNumber(6);

Constructor Function

var user1 = {name: 'Kishan', age: 30, getName: function() { return this.name }}
var user2 = {name: 'Ashish', age: 29, getName: function() { return this.name }}
// to create n number of use we need to create n number of independent object
// to avoid this we can use function constructor
var Person = function(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.getName = function() {
return `${this.firstName} ${this.lastName}`;
},
this.getAge = function() {
return this.age;
}
}
var p1 = new Person('Kishan', 'Patel', 30);
var p2 = new Person('Ram', 'Patel', 30);
console.log(p1);
console.log(p1);

console.log(p1.getName);
console.log(p2.getName);

console.log(p1.getAge);
console.log(p2.getAge);

In the above example you might have noticed that console log of p1 and p2 both are havinggetName and getAge which are repeating. To avoid this we can use prototype.


var Person = function(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}

Person.prototype.getName = function() {
return `${this.firstName} ${this.lastName}`;
};

Person.prototype.getAge = function() {
return this.age;
}

var p1 = new Person('Kishan', 'Patel', 30);
var p2 = new Person('Ram', 'Patel', 30);

// if you want to add a method for a perticular object
p1.__proto__.myAge = function() {
return this.age;
}

console.log(p1);
console.log(p1.getName()); // Kishan Patel
console.log(p1.getAge()); // 30
console.log(p1.myAge()); // 30

How to acive inheritance in constructor function

function Animal(name, age) {
this.name = name;
this.age = age;
this.logDetails = function() {
console.log(`${this.name} is of ${this.age} years`);
}
}

Animal.prototype.eats = function() {
console.log(`${this.name} is eating now.`);
}

function Dog(name, age, breed) {
this.breed = breed;
// inherit all attributes and methods
Animal.call(this, name, age);
this.logBreed = function() {
console.log(this.name + ' is a ' + this.breed);
}
}

// inherit the prototype as well.
Dog.prototype = Object.create(Animal.prototype);

const d1 = new Dog('Simba', 4, 'bulldog');
d1.logBreed();
d1.logDetails();
d1.eats();

Class

class Person {
constructor(firstName, lastName, dob) {
this.firstName = firstName;
this.lastName = lastName;
this.dob = dob;
}

// Public Method
getFullName() {
return `${this.firstName} ${this.lastName}`;
}

// Public Method
getAge() {
return this.#calculateAge();
}

// Private Method
#calculateAge() {
var dob = new Date(this.dob);
var month_diff = Date.now() - dob.getTime();

//convert the calculated difference in date format
var age_dt = new Date(month_diff);

//extract year from date
var year = age_dt.getUTCFullYear();

//now calculate the age of the user
var age = Math.abs(year - 1970);
return age;
}
}

const p1 = new Person('Kishan', 'Patel', '1993/04/26');
console.log(`${p1.getFullName()}'s age is: ${p1.getAge()} years.`);

Class Inheritance

class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
logDetails = function() {
console.log(`${this.name} is of ${this.age} years`);
}
}

Animal.prototype.eats = function() {
console.log(`${this.name} is eating now.`);
}

class Dog extends Animal {
constructor(name, age, breed) {
super(name, age);
this.breed = breed;
}
logBreed = function() {
console.log(this.name + ' is a ' + this.breed);
}
}

Dog.prototype = Object.create(Animal.prototype);

const d1 = new Dog('Simba', 4, 'bulldog');
d1.logBreed();
d1.logDetails();
d1.eats();

Polyfills

In JavaScript, a polyfill is a piece of code that provides functionality that is not natively supported by a web browser. It allows you to use new JavaScript features or APIs in older browsers that do not support them.

Polyfills are typically used to bridge the gap between older browsers and the latest web standards. They emulate the behavior of the new features or APIs by implementing them using the existing JavaScript capabilities available in the browser.

Let’s consider an example where we want to use the Array.prototype.includes() method, which was introduced in ECMAScript 2016 (ES7), but we need to support older browsers that do not have native support for this method. We can create a polyfill to emulate the functionality of includes() using existing JavaScript methods.

Here’s a simple polyfill for Array.prototype.includes():

if (!Array.prototype.includes) {
Array.prototype.includes = function(element) {
for (let i = 0; i < this.length; i++) {
if (this[i] === element) {
return true;
}
}
return false;
};
}

Event Bubling & Event Capturing(Trickling)

  • Event Bubling

A bubbling event goes from the target element straight up. Normally it goes upwards till <html>, and then to document object, and some events even reach window, calling all handlers on the path.

  • Event Capturing(Trickling)

Event capturing is the event starts from the top element to the target element.

#grand-parent
#parent
#child

Example 1: Child Clicked

Example 2: Child Clicked

Example 3: Child Clicked

Example 4: Parent Clicked

Go to top

Thanks for reading, check out my article on Javascript FAQs .

--

--