This repository contains a few hundred curated JavaScript interview questions with high quality answers for acing your Front End Engineer interviews.
In JavaScript, let
, var
, and const
are all keywords used to declare variables, but they differ significantly in terms of scope, initialization rules, whether they can be redeclared or reassigned and the behavior when they are accessed before declaration:
Behavior | var |
let |
const |
---|---|---|---|
Scope | Function or Global | Block | Block |
Initialization | Optional | Optional | Required |
Redeclaration | Yes | No | No |
Reassignment | Yes | Yes | No |
Accessing before declaration | undefined |
ReferenceError |
ReferenceError |
==
is the abstract equality operator while ===
is the strict equality operator. The ==
operator will compare for equality after doing any necessary type conversions. The ===
operator will not do type conversion, so if two values are not the same type ===
will simply return false
.
Operator | == |
=== |
---|---|---|
Name | (Loose) Equality operator | Strict equality operator |
Type coercion | Yes | No |
Compares value and type | No | Yes |
Event Bubbling is a concept in the DOM (Document Object Model). It happens when an element receives an event, and that event bubbles up (or you can say is transmitted or propagated) to its parent and ancestor elements in the DOM tree until it gets to the root element.
- Event bubbling is essential for event delegation, where a single event handler manages events for multiple child elements, enhancing performance and code simplicity. While convenient, failing to manage event propagation properly can lead to unintended behavior, such as multiple handlers firing for a single event.
- Use event.stopPropagation() to stop the event bubbling to the parent / root elements
const div = document.getElementById("div");
const span = document.getElementById("span");
const button = document.getElementById("button");
div.addEventListener("click", () => {
console.log("div was clicked");
});
span.addEventListener("click", () => {
console.log("span was clicked");
});
button.addEventListener("click", (event) => {
// Use stopPropagation() to stop event bubbling
event.stopPropagation();
console.log("button was clicked");
});
Event delegation is a event handling pattern in which you handle the events at a higher level in the DOM tree instead of the actual level where the event was received. The event delegation is based on event bubbling concept.
Advantages -
- Improved performance: Attaching a single event listener is more efficient than attaching multiple event listeners to individual elements, especially for large or dynamic lists. This reduces memory usage and improves overall performance.
- Dynamic Content Handling: Event delegation can automatically handle events on new child elements that are added later.
<div id="div">
<span id="span">
<button>button1</button>
<button>button2</button>
<button>button3</button>
<!-- Elements can be added without thinking much about the backend JS function -->
<button>button4</button>
</span>
</div>
const div = document.getElementById("div");
div.addEventListener("click", (event) => {
const target = event.target;
if (target.tagName === "BUTTON") {
console.log(target.innerHTML);
}
});
There's no simple explanation for this; it is one of the most confusing concepts in JavaScript because it's behavior differs from many other programming languages. The one-liner explanation of the this keyword is that it is a dynamic reference to the context in which a function is executed.
A longer explanation follows is that this follows these rules:
- If the new keyword is used when calling the function, meaning the function was used as a function constructor, the this inside the function is the newly-created object instance.
- If this is used in a class constructor, the this inside the constructor is the newly-created object instance.
- If apply(), call(), or bind() is used to call/create a function, this inside the function is the object that is passed in as the argument.
- If a function is called as a method (e.g. obj.method()) — this is the object that the function is a property of.
- If a function is invoked as a free function invocation, meaning it was invoked without any of the conditions present above, this is the global object. In the browser, the global object is the window object. If in strict mode ('use strict';), this will be undefined instead of the global object.
- If multiple of the above rules apply, the rule that is higher wins and will set the this value.
- If the function is an ES2015 arrow function, it ignores all the rules above and receives the this value of its surrounding scope at the time it is created.
For an in-depth explanation, do check out Arnav Aggrawal's article on Medium & this is JS, Simplified.
Callback function is a function which is passed as an argument to another function. Using callback helps you to call a function from another function.
function log(value) {
console.log(value);
}
function findSum(num1, num2, print) {
const sum = num1 + num2;
print(sum);
}
findSum(20, 30, log);
// Example -
// window.addEventListener(event, callback function)
The event loop is concept within the browser runtime environment regarding how asynchronous operations are executed within JavaScript engines. It works as such:
- The JavaScript engine starts executing scripts, placing synchronous operations on the call stack.
- When an asynchronous operation is encountered (e.g.,
setTimeout()
, HTTP request), it is offloaded to the respective Web API or Node.js API to handle the operation in the background. - Once the asynchronous operation completes, its callback function is placed in the respective queues – task queues (also known as macrotask queues / callback queues) or microtask queues. We will refer to "task queue" as "macrotask queue" from here on to better differentiate from the microtask queue.
- The event loop continuously monitors the call stack and executes items on the call stack. If/when the call stack is empty:
- Microtask queue is processed. Microtasks include promise callbacks (
then
,catch
,finally
),MutationObserver
callbacks, and calls toqueueMicrotask()
. The event loop takes the first callback from the microtask queue and pushes it to the call stack for execution. This repeats until the microtask queue is empty. - Macrotask queue is processed. Macrotasks include web APIs like
setTimeout()
, HTTP requests, user interface event handlers like clicks, scrolls, etc. The event loop dequeues the first callback from the macrotask queue and pushes it onto the call stack for execution. However, after a macrotask queue callback is processed, the event loop does not proceed with the next macrotask yet! The event loop first checks the microtask queue. Checking the microtask queue is necessary as microtasks have higher priority than macrotask queue callbacks. The macrotask queue callback that was just executed could have added more microtasks!- If the microtask queue is non-empty, process them as per the previous step.
- If the microtask queue is empty, the next macrotask queue callback is processed. This repeats until the macrotask queue is empty.
- Microtask queue is processed. Microtasks include promise callbacks (
- This process continues indefinitely, allowing the JavaScript engine to handle both synchronous and asynchronous operations efficiently without blocking the call stack.
The following are resources explaining the event loop:
- JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue (2024): Lydia Hallie is a popular educator on JavaScript and this is the best recent videos explaining the event loop. There's also an accompanying blog post for those who prefer detailed text-based explanations.
- In the Loop (2018): Jake Archibald previously from the Chrome team provides a visual demonstration of the event loop during JSConf 2018, accounting for different types of tasks.
- What the heck is the event loop anyway? (2014): Philip Robert's gave this epic talk at JSConf 2014 and it is one of the most viewed JavaScript videos on YouTube.
- The arr.map method is one of the most useful and often used.
- It calls the function for each element of the array and returns the array of results.
// The syntax -
let result = arr.map(function(item, index, array) {
// returns the new value instead of item
});
Usage -
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length);
alert(lengths); // 5,7,6
- The
find
method looks for a single (first) element that makes the function return true. - If there may be many, we can use
arr.filter(fn)
. filter
returns an array of all matching elements
// The syntax -
let results = arr.filter(function(item, index, array) {
// if true item is pushed to results and the iteration continues
// returns empty array if nothing found
});
// Example -
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
];
// returns array of the first two users
let someUsers = users.filter(item => item.id < 3);
alert(someUsers.length); // 2
- The method
arr.reduce
used to calculate a single value based on the array. - The function is applied to all array elements one after another and “carries on” its result to the next call.
// The syntax is:
let value = arr.reduce(function(accumulator, item, index, array) {
// ...
}, [initial]);
// Example -
let arr = [1, 2, 3, 4, 5];
let result = arr.reduce((sum, current) => sum + current, 0);
alert(result); // 15
- accumulator – is the result of the previous function call, equals initial the first time (if initial is provided).
- item – is the current array item.
- index – is its position.
- array – is the array.
For an in-depth explanation do check - Array methods in JS - javascript.info
- For detailed explanation for the polyfills of map, filter, reduce do check - Array Methods Polyfills
// Polyfill for map
Array.prototype.myMap = function (cb) {
let temp = [];
for (let i = 0; i < this.length; i++) {
temp.push(cb(this[i], i, this));
}
return temp;
};
const multiplyThree = nums.myMap((num, index, arr) => {
return num * 3;
});
console.log(multiplyThree);
// Polyfill for filter
Array.prototype.myFilter = function (cb) {
let temp = [];
for (let i = 0; i < this.length; i++) {
if (cb(this[i], i, this)) temp.push(this[i]);
}
return temp;
};
const moreThanTwo = nums.myFilter((num) => {
return num > 2;
});
console.log(moreThanTwo);
// Polyfill for reduce
Array.prototype.myReduce = function (cb, initialValue) {
let acc = initialValue;
for (let i = 0; i < this.length; i++) {
acc = acc ? cb(acc, this[i], i, this) : this[i];
}
return acc;
};
const sum = nums.myReduce((acc, curr, i, arr) => {
return acc + curr;
}, 0);
console.log(sum);
- The first difference between
map()
andforEach()
is the returning value. TheforEach()
method returnsundefined
andmap()
returns a new array with the transformed elements. Even if they do the same job, the returning value remains different. - The second difference between these array methods is the fact that
map()
is chainable. This means that you can attachreduce()
,sort()
,filter()
and so on after performing amap()
method on an array. That's something you can't do withforEach()
because, as you might guess, it returnsundefined
.
For an in-depth explanation do check - Main Differences Between forEach and map
Map is a collection of keyed data items, just like an Object
. But the main difference is that Map
allows keys of any type. Methods and properties are:
new Map()
– creates the map.map.set(key, value)
– stores the value by the key.map.get(key)
– returns the value by the key,undefined
ifkey
doesn’t exist in map.map.has(key)
– returnstrue
if thekey
exists,false
otherwise.map.delete(key)
– removes the element (the key/value pair) by the key.map.clear()
– removes everything from the map.map.size
– returns the current element count.
A Set
is a special type collection – “set of values” (without keys), where each value may occur only once. Its main methods are:
new Set([iterable])
– creates the set, and if aniterable
object is provided (usually an array), copies values from it into the set.set.add(value)
– adds a value, returns the set itself.set.delete(value)
– removes the value, returnstrue
ifvalue
existed at the moment of the call, otherwisefalse
.set.has(value)
– returnstrue
if the value exists in the set, otherwisefalse
.set.clear()
– removes everything from the set.set.size
– is the elements count.
Map | WeakMap |
---|---|
A Map is an unordered list of key-value pairs where the key and the value can be of any type like string, boolean, number, etc. | In a Weak Map, every key can only be an object and function. It used to store weak object references. |
Maps are iterable. | WeakMaps are not iterable. |
Maps will keep everything even if you don’t use them. | WeakMaps holds the reference to the key, not the key itself. |
The garbage collector doesn’t remove a key pointer from Map and also doesn’t remove the key from memory. |
The garbage collector goes ahead and removes the key pointer from WeakMap and also removes the key from memory. WeakMap allows the garbage collector to do its task but not the Map. |
Maps have some properties : .set, .get, .delete, .size, .has, .forEach, Iterators. | WeakMaps have some properties : .set, .get, .delete, .has. |
The virtual DOM is an in-memory representation of the real DOM elements. Instead of interacting directly with the real DOM, which can be slow and costly in terms of performance, React creates a virtual representation of the UI components. This virtual representation is a lightweight JavaScript object that mirrors the structure of the real DOM.
Here's a step-by-step process of how the virtual DOM works:
- Step 1 – Initial Rendering: when the app starts, the entire UI is represented as a Virtual DOM. React elements are created and rendered into the virtual structure.
- Step 2 – State and Props Changes: as the states and props change in the app, React re-renders the affected components in the virtual DOM. These changes do not immediately impact the real DOM.
- Step 3 – Comparison Using Diff Algorithm: React then uses a diffing algorithm to compare the current version of the Virtual DOM with the previous version. This process identifies the differences (or "diffs") between the two versions.
- Step 4 – Reconciliation Process: based on the differences identified, React determines the most efficient way to update the real DOM. Only the parts of the real DOM that need to be updated are changed, rather than re-rendering the entire UI. This selective updating is quick and performant.
- Step 5 – Update to the Real DOM: finally, React applies the necessary changes to the real DOM. This might involve adding, removing, or updating elements based on the differences detected in step 3.
Read the following article for detailed understanding - What is the Virtual DOM in React?
Lifting state up is an important pattern for React developers because sometimes we have state that's located within a particular component that also needs to be shared with sibling components.
Instead of using an entire state management library like Redux or React Context, we can just lift the state up to the closest common ancestor and pass both the state variables the state values down as well as any callbacks to update that state.
Read the following article for detailed understanding - What Is "Lifting State Up" in React?
HOCs are functions that wrap existing components, providing them with additional props or behaviors. Like a gift wrap, wrapping an existing component and adding additional feature to the gift.
The main benefit of HOCs is that they enable us to extend the functionality of multiple components without repeating the same code in each of them. This promotes code reuse and enhances the maintainability of your React applications. Examples - Auth Check, Dark Mode / Light Mode Application etc.
Read the following article for detailed understanding - Mastering Higher Order Components (HOCs) in React | HackerNoon
The useState
hook is perhaps the most basic and essential hook in React. It enables you to add state to your functional components, allowing them to keep track of data that changes over time. Let's dive into how useState
works with a simple example.
import React, { useState } from 'react';
const Counter = () => {
// Declare a state variable named 'count' with an initial value of 0
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
export default Counter;
The useEffect
hook is used to perform side effects in your functional components, such as fetching data, subscribing to external events, or manually changing the DOM. It combines the functionality of componentDidMount
, componentDidUpdate
, and componentWillUnmount
in class components.
import React, { useState, useEffect } from 'react';
const DataFetcher = () => {
const [data, setData] = useState(null);
useEffect(() => {
// Fetch data from an API
fetch('https://api.example.com/data')
.then((response) => response.json())
.then((result) => setData(result))
.catch((error) => console.error('Error fetching data:', error));
}, []); // Empty dependency array means this effect runs once after the initial render
return (
<div>
{data ? (
<ul>
{data.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
) : (
<p>Loading data...</p>
)}
</div>
);
};
export default DataFetcher;
Cleanup using useEffect
Sometimes, side effects need to be cleaned up, especially when dealing with subscriptions or timers to prevent memory leaks. The useEffect
hook can return a cleanup function that will be executed when the component unmounts.
// In this example, the setInterval function is used to update the seconds state every second.
// The cleanup function returned by useEffect clears the interval when the component is unmounted.
import React, { useState, useEffect } from 'react';
const Timer = () => {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setSeconds((prevSeconds) => prevSeconds + 1);
}, 1000);
// Cleanup function to clear the interval when the component unmounts
return () => clearInterval(intervalId);
}, []); // Empty dependency array for initial setup only
return <p>Seconds: {seconds}</p>;
};
export default Timer;
The useContext
hook is used to consume values from a React context. Context provides a way to pass data through the component tree without having to pass props manually at every level. Let's explore how useContext
works with a simple example.
// We create an AuthContext using createContext and provide an AuthProvider component. The AuthProvider component wraps its children with the context provider and includes functions for logging in and out.
import React, { createContext, useContext, useState } from 'react';
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const login = () => {
setIsAuthenticated(true);
};
const logout = () => {
setIsAuthenticated(false);
};
return (
<AuthContext.Provider value={{ isAuthenticated, login, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
return useContext(AuthContext);
};
// Consuming useContext in the children components
// Here, the useAuth hook is used to access the values provided by the AuthContext. The AuthStatus component displays the user's login status and provides buttons to log in and out.
import React from 'react';
import { useAuth } from './AuthContext';
const AuthStatus = () => {
const { isAuthenticated, login, logout } = useAuth();
return (
<div>
<p>User is {isAuthenticated ? 'logged in' : 'logged out'}</p>
<button onClick={login}>Login</button>
<button onClick={logout}>Logout</button>
</div>
);
};
export default AuthStatus;
The useReducer
hook in React is an alternative to useState
for managing more complex state logic. It is particularly useful when the state depends on previous states or involves multiple sub-values, enabling better organization and control.
How useReducer
Works
useReducer
is based on the Reducer Pattern:
- Reducer Function: A pure function that takes the current state and an action, then returns the new state.
- Dispatch: A function used to send actions to the reducer.
- State: The state managed by the reducer.
Read the following article for detailed understanding - How to useReducer in React
Read the following article for detailed understanding - Custom Hooks in React
const useBoolean = () => {
const [state, setState] = React.useState();
const handleTrue = () => setState(true);
const handleFalse = () => setState(false);
const handleToggle = () => setState(!state);
return [
state,
{
setTrue: handleTrue,
setFalse: handleFalse,
setToggle: handleToggle,
},
];
};
function App() {
const [isToggle, {
setToggle,
setTrue,
setFalse,
}] = useBoolean(false);
return (
<div>
<button type="button" onClick={setToggle}>
Toggle
</button>
<button type="button" onClick={setTrue}>
To True
</button>
<button type="button" onClick={setFalse}>
To False
</button>
{isToggle.toString()}
</div>
);
}
To update the state of a parent component from a child component, you can pass a state-updating function (defined in the parent) as a prop to the child component. The child can then invoke this function to update the parent's state.
Steps to Update Parent State from Child
- Define State in the Parent Component: The parent component owns the state and provides a function to update it.
- Pass the Update Function as a Prop: The parent's state-updating function is passed to the child component as a prop.
- Invoke the Update Function in the Child: The child component calls the function to modify the parent's state. Example
Parent Component
import React, { useState } from "react";
import Child from "./Child";
const Parent = () => {
const [message, setMessage] = useState("Hello from Parent");
// Function to update state
const updateMessage = (newMessage) => {
setMessage(newMessage);
};
return (
<div>
<h1>{message}</h1>
<Child updateMessage={updateMessage} />
</div>
);
};
export default Parent;
Child Component
import React from "react";
const Child = ({ updateMessage }) => {
const handleChange = () => {
updateMessage("Message updated from Child!");
};
return (
<button onClick={handleChange}>Update Parent Message</button>
);
};
export default Child;
Alternative Approaches
- Using Context API:
- State Management Libraries
Prop drilling refers to the process of passing data (props) from a parent component to a deeply nested child component through multiple intermediary components, even if those intermediary components do not directly need the data.
Problems with Prop Drilling
- Unnecessary Complexity: Makes components tightly coupled, harder to maintain and refactor.
- Code Duplication: Repetitive passing of props through intermediate components.
- Scalability Issues: As the app grows, managing deeply nested props becomes cumbersome.
How to Avoid Prop Drilling -
- Use React Context API
- Use State Management Libraries - Redux, Zustand, MobX
- Higher Order components
- Custom Hooks
To call a parent component's method from a child component in React, you can pass the parent method as a prop to the child component. This establishes communication between the child and the parent.
Lazy loading is a design pattern used to improve application performance by deferring the loading of components or resources until they are actually needed. In React, lazy loading is commonly used for components to reduce the initial load time by splitting the code into smaller chunks (code-splitting).
How It Works When a React app is built, all components are usually bundled into a single JavaScript file. Lazy loading splits this file into smaller chunks, loading only the necessary parts as the user navigates the app. This helps in:
- Reducing initial load time.
- Improving user experience for larger applications.
- Optimizing resource utilization.
Syntax
const LazyComponent = React.lazy(() => import('./LazyComponent'));
Example
Component Setup
// LazyComponent.js
import React from 'react';
const LazyComponent = () => {
return <h1>This is a lazy-loaded component!</h1>;
};
export default LazyComponent;
Using Lazy Loading in the App
import React, { Suspense } from 'react';
// Lazy load the component
const LazyComponent = React.lazy(() => import('./LazyComponent'));
const App = () => {
return (
<div>
<h1>Welcome to My App</h1>
{/* Suspense provides a fallback UI while loading */}
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
};
export default App;
Key Elements
-
React.lazy
: Dynamically imports the component. -
Suspense
: Wraps the lazy-loaded component and displays a fallback UI (like a loading spinner) while the component is being fetched.
Benefits of Lazy Loading
- Performance Optimization:
- Efficient Resource Utilization:
- Improved User Experience:
Read the following article to understand Lazy Loading in Routes