Last Updated On - March 15th, 2024 Published On - Feb 19, 2024
Overview
In this post, I’ll continue the series of Javascript Interview Questions For Frontend Developers 2024. If you didn’t check the 1st part, please check it out. 1st part contains some of the latest, trending, and most important questions related to the basics of javascript, which may help you to understand the core concept of javascript and you’ll be able to crack your next technical interview round.
In this post, I’ll explore some more concepts of javascript and how they are going to be asked of you in your next interview round.
Q1: What is a callback function?
In JavaScript, a callback function is a function that you pass as an argument to another function. This “inner” function is then called back (hence the name) by the “outer” function when certain conditions are met or at specific points in its execution. This allows you to create modular, asynchronous, and event-driven code.
Key Characteristics:
- Passed as an Argument: You provide a callback function as an argument when calling another function.
- Executed Later: The callback function is not immediately invoked; it’s stored and called later.
- Triggered by Events: Callbacks are often used to handle events (like button clicks, network requests, or timers finishing) or when tasks complete asynchronously.
Benefits of Using Callbacks:
- Modularity: Decoupling code into smaller, reusable functions.
- Asynchronous Programming: Allowing your program to continue running while tasks like network requests or setTimeout() execute.
- Event-Driven Programming: Efficiently handling user interactions and system events.
Example 1: Simulating an Asynchronous Task
Here’s an example of using a callback to simulate a function that takes 2 seconds to complete:
function simulateAsyncFunction(data, callback) {
setTimeout(() => {
callback(data + " (processed asynchronously)");
}, 2000);
}
// Call the simulateAsyncFunction and process the result later
simulateAsyncFunction("hello", (result) => {
console.log(result); // Output: "hello (processed asynchronously)" after 2 seconds
});
console.log("This line will execute immediately, while the async function runs.");
In this example:
simulateAsyncFunction
takes two arguments:data
and acallback
function.- Inside
simulateAsyncFunction
,setTimeout
is used to simulate a 2-second delay. - Within the delay, the
callback
function is invoked with the processeddata
. - The main code continues to execute, printing “This line will execute immediately.”
- After 2 seconds, the
callback
function is called, printing the processed result.
Example 2: Event Handling with Clicks
Here’s how to use a callback to handle a button click:
const button = document.getElementById("myButton");
button.addEventListener("click", () => {
console.log("Button clicked!");
});
In this example:
button.addEventListener
attaches a “click” event listener to themyButton
button.- The provided callback function (
console.log("Button clicked!")
) is executed when the button is clicked.
Example 3: Error Handling with Callbacks
You can also use callbacks to handle errors from asynchronous operations:
function getDataFromServer(url, callback) {
fetch(url)
.then((response) => response.json())
.then((data) => callback(null, data)) // Pass null for no error, data as result
.catch((error) => callback(error, null)); // Pass error, null for no data
}
getDataFromServer("https://api.example.com/data", (error, data) => {
if (error) {
console.error("Error fetching data:", error);
} else {
console.log("Data:", data);
}
});
Here, the callback function receives either an error object (null
if no error) or the fetched data. This allows you to handle both successful and error cases gracefully.
Q2: Explain the concept of a pure function.
In JavaScript, a pure function is a special type of function that has two key characteristics:
1. Deterministic: Given the same input, it always returns the same output. This means its output does not depend on any external factors or changes in the program’s state. It essentially operates in isolation, considering only the arguments it receives.
2. No side effects: It does not produce any side effects, such as modifying global variables, performing I/O operations (like printing to the console), or causing changes outside its own scope. Everything it needs to compute the output is contained within its arguments.
Think of a pure function as a mathematical formula: you plug in a number, and you always get the same answer.
Advantages of using pure functions:
- Predictability: Since a pure function always returns the same output for the same input, it’s easier to reason about and test. You know exactly what it will do without worrying about external factors.
- Composability: Pure functions can be easily combined and reused without unexpected interactions, as they don’t affect each other’s state. This leads to more modular and maintainable code.
- Immutability: If a pure function doesn’t modify anything outside its scope, it promotes immutability, a programming paradigm where data is never changed directly, but rather new values are created. This can lead to fewer bugs and easier debugging.
Example:
Here’s a pure function that calculates the area of a rectangle:
function calculateArea(width, height) {
return width * height;
}
const area1 = calculateArea(5, 3); // area1 will always be 15
const area2 = calculateArea(5, 3); // area2 will also be 15
This function always returns the product of width
and height
, regardless of how many times it’s called or what other parts of the program are doing.
Impure Function Example (for Contrast):
Here’s an impure function that modifies a global variable:
let globalCounter = 0;
function incrementCounter() {
globalCounter++;
return globalCounter;
}
console.log(incrementCounter()); // 1
console.log(incrementCounter()); // 2 (globalCounter is now 2)
// This function has a side effect because it changes the value of globalCounter
This function changes the value of the global variable globalCounter
. This makes it unpredictable and harder to reason about, as its output depends on the value of globalCounter
at the time of call, not just the arguments.
Remember: While most functions you write in practice might not be completely pure due to realities of programming, striving for purity whenever possible can significantly improve your code’s quality, maintainability, and testability.
Also Read: Typescript Technical Interview Questions 2024 – Part 1
Q3: What are the differences between function.call
, function.apply
, and function.bind
?
In JavaScript, function.call
, function.apply
, and function.bind
are all methods used to control the context (this
) within which a function is executed. However, they differ in their specific functionalities:
Function.call:
- Takes two arguments:
- thisArg: The value to be used as the
this
keyword within the function. - arg1, arg2, …: An optional list of arguments to pass to the function.
- thisArg: The value to be used as the
- Immediately executes the function with the provided
thisArg
and arguments. - Useful for scenarios where you need to execute a function with a specific
this
value and pass arbitrary arguments.
Example:
function greet(name) {
console.log(this.age + " year old " + this.name + " says: Hello, " + name + "!");
}
const person = { name: "Alice", age: 30 };
greet.call(person, "Bob"); // Output: 30 year old Alice says: Hello, Bob!
Function.apply:
- Works similarly to
call
, but takes two arguments:- thisArg: Same as in
call
. - argsArray: An array containing all the arguments to pass to the function.
- thisArg: Same as in
- Immediately executes the function with the provided
thisArg
and the elements of theargsArray
. - Useful when you already have an array of arguments and want to use it directly.
Example:
const argumentsArray = ["Bob", "Charlie"];
greet.apply(person, argumentsArray); // Output: same as previous
Function.bind:
- Takes two arguments (optional second argument):
- thisArg: Same as in
call
. - arg1, arg2, …: Optional arguments to be pre-bound to the function (similar to currying).
- thisArg: Same as in
- Returns a new function with the provided
thisArg
bound to it. - This new function can be later invoked without affecting the original function’s
this
value. - Useful for creating functions with a fixed
this
value for later use, particularly in asynchronous or event-driven contexts.
Example:
const boundGreet = greet.bind(person, "Bob"); // Pre-bind "Bob" as the second argument
boundGreet(); // Output: 30 year old Alice says: Hello, Bob! (Without needing args)
Key Differences:
Feature | function.call | function.apply | function.bind |
---|---|---|---|
Immediate execution | Yes | Yes | No |
Argument passing | Individual args | Array of args | Individual args (optional) |
Returns | Nothing | Nothing | A new function |
Choosing the right method:
- Use
call
when you have individual arguments and need immediate execution. - Use
apply
when you have an existing array of arguments and need immediate execution. - Use
bind
when you want to pre-bind athis
value and arguments for later use without affecting the original function.
Q4: What is the purpose of the arguments object in a function?
The arguments
object plays a valuable role in function flexibility, but it’s important to be aware of its limitations and modern alternatives.
Purpose:
The arguments
object is an array-like object available within non-arrow functions in JavaScript. It provides access to all the arguments passed to the function, regardless of the number or names of defined parameters. This allows you to:
- Handle functions with a variable number of arguments: You can iterate through the
arguments
object to process each argument, even if the function doesn’t explicitly declare them. - Access arguments by index: If you need to reference specific arguments within the function, you can use their index in the
arguments
object (starting from 0).
Example:
function sum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
console.log(sum(1, 2, 3)); // Output: 6
Limitations:
- Not a true array: The
arguments
object is array-like but not a real array. It lacks some useful array methods likemap
,filter
, andforEach
. - No named properties: Arguments don’t have names attached, so you can only access them by their index.
- Not accessible in arrow functions: Arrow functions have their own way of handling arguments using the rest parameter (
...
).
Modern Alternatives:
Rest parameter (...
)
Introduced in ES6, the rest parameter allows you to collect an indefinite number of arguments into an array within the function parameters. This is generally preferred over arguments
due to its clarity and array-like nature.
Example:
function sum(...numbers) {
let total = 0;
for (const num of numbers) {
total += num;
}
return total;
}
console.log(sum(1, 2, 3)); // Output: 6
Array spread operator (...
)
When you have an existing array of arguments, you can use the spread operator to pass them individually to a function.
Example:
const args = [1, 2, 3];
console.log(sum(...args)); // Output: 6
Also Read: Coding Challenge: Typescript Interview Questions 2024 – Part 2
Q5: What is closure and How do you create a closure in JavaScript?
a closure is a powerful concept that combines a function with its lexical environment (the environment in which it was created). Essentially, it allows an inner function to remember and access variables from the outer function’s scope, even after the outer function has finished executing.
Key Characteristics:
- Created through nesting: When you define a function inside another function, the inner function has access to the outer function’s variables, even if the outer function has finished running.
- Preserves state: This “remembering” ability gives closures a sense of state, as they can store and modify values even when the parent function is gone.
- Modular and isolated: Each closure creates its own private environment, preventing accidental modification of external variables and promoting good code organization.
How to Create a Closure:
- Nest a function inside another function: This creates the basic structure for a closure.
- Access variables from the outer function: The inner function can freely use variables defined in the outer function’s scope.
Example:
function createCounter() {
let count = 0; // Outer function's variable
function increment() {
count++; // Inner function accessing and modifying the outer variable
return count;
}
return increment; // Returning the inner function (the closure)
}
const counter1 = createCounter();
const counter2 = createCounter();
console.log(counter1()); // 1 (first counter starts at 0)
console.log(counter2()); // 1 (second counter also starts at 0)
console.log(counter1()); // 2 (counter1 remembers its state)
console.log(counter2()); // 2 (counter2 is separate)
Applications of Closures:
- Private variables: Closures can hold private variables within functions, protecting them from modification by external code.
- Simulating stateful behavior: Closures can mimic stateful behavior, even though JavaScript is primarily functional.
- Event handlers: Closures are commonly used in event handlers to maintain state and context between events.
- Callback functions: Closures can be used to create callback functions with specific context or state.
Remember, closures are powerful but can sometimes make code harder to read. Use them thoughtfully and strategically for code that is modular, private, and stateful.
Q6: What is the use of the bind method?
the bind()
method serves a specific purpose: controlling the this
context within which a function is executed. Here’s a breakdown of its key uses:
1. Setting this
explicitly:
Sometimes, you want a function to have a specific this
value, regardless of how it’s called. This is crucial when a function relies on a particular this
object to access its properties or methods.
Example:
const person = {
name: "Alice",
greet: function() {
console.log(this.name + " says hello!");
}
};
const boundGreet = person.greet.bind(person); // Bind "person" as thisArg
boundGreet(); // Output: Alice says hello!
Here, bind()
ensures that person
is always the this
value when boundGreet
is called, even if called in a different context.
2. Dealing with event listeners:
In event-driven programming, the this
value inside event listeners often differs from what you expect. bind()
helps fix this:
Example:
const button = document.getElementById("myButton");
button.addEventListener("click", function() {
console.log(this); // This might not be what you expect
}.bind(document)); // Bind "document" as thisArg
By binding the event listener to document
, you guarantee that this
refers to the document object when the click event occurs.
3. Creating functions with fixed this
:
You can create functions with pre-bound this
values for later use:
Example:
function createCounter() {
let count = 0;
return function() {
count++;
return count;
}.bind(this); // Bind the current "this" to the returned function
}
const counter1 = createCounter();
const counter2 = createCounter.call({count: 10}); // Create counter with initial value
console.log(counter1()); // 1
console.log(counter2()); // 11 (separate counters)
Here, bind()
fixes the this
value within createCounter()
‘s returned function, allowing separate counters with different initial values.
Key points to remember:
bind()
doesn’t immediately execute the function; it returns a new function with the boundthis
value.- You can optionally pre-bind arguments along with
this
. - Be mindful of using
bind()
too extensively, as it can sometimes impact code readability.
Also Read: PNPM vs NPM: Why should we use PNPM over NPM?
Q7: What is the difference between a shallow copy and a deep copy?
Both shallow and deep copies are methods of creating a new copy of an object or data structure in JavaScript. However, they differ fundamentally in how they handle nested references and the level of duplication:
Shallow Copy:
- Creates a new object with the same structure as the original object.
- Copies the values of top-level properties or elements.
- Maintains references to any nested objects or arrays within the original object.
- Changes made to the nested objects in the copy will also affect the original object due to the shared references.
Example:
const original = { name: "Alice", address: { city: "New York" } };
const shallowCopy = Object.assign({}, original);
shallowCopy.name = "Bob"; // Only name changes in the copy
shallowCopy.address.city = "London"; // Modifies city in both objects (shared reference)
Deep Copy:
- Creates a completely independent copy of the original object.
- Recursively copies the values of all properties and elements, including nested objects and arrays.
- Creates new copies of any nested objects or arrays, breaking the connection with the original.
- Changes made to the copy will not affect the original object.
Example:
const original = { name: "Alice", address: { city: "New York" } };
const deepCopy = JSON.parse(JSON.stringify(original)); // Common deep copy method
deepCopy.name = "Bob"; // Only name changes in the copy
deepCopy.address.city = "London"; // Only modifies city in the copy (new object)
Choosing the Right Method:
- Use a shallow copy when you only need a new reference to the same data structure and any changes might be desired to affect both the original and the copy.
- Use a deep copy when you need a completely independent copy that won’t be affected by modifications to the original, especially if dealing with complex objects or data structures with nested references.
Q8: How does the call stack work in JavaScript?
In JavaScript, the call stack is a crucial mechanism that maintains order and context during function execution. It’s essentially a LIFO (Last In, First Out) data structure, similar to a stack of plates. Imagine each plate representing a function being called.
Here’s how it works:
- Empty Stack: When your JavaScript code starts, the call stack is initially empty.
- Function Call: When you call a function, that function is pushed onto the call stack. This creates a stack frame within the stack, which stores information about the function, including:
- The function’s arguments
- Local variables declared within the function
- The value of
this
- The return address (where execution should resume after the function finishes)
- Recursive Function Calls: If a function calls itself (recursion), new stack frames are created for each recursive call, pushing them deeper onto the stack.
- Function Execution: Inside a stack frame, the function’s code is executed line by line.
- Return Statement: When the function reaches a
return
statement or encounters an error, it “returns” by:- Popping its own stack frame from the call stack.
- Returning the value specified in the
return
statement (orundefined
if there’s noreturn
). - Resuming execution from the return address stored in the previous stack frame.
Example:
function foo() {
console.log('foo');
}
function bar() {
foo();
console.log('bar');
}
bar();
// Call Stack: bar -> foo
// Output: foo, bar
Key Characteristics:
- Single-threaded: Due to LIFO and limited resources, JavaScript engines typically execute code in a single thread, meaning they handle one function call at a time. Asynchronous operations often use alternative mechanisms like event loops and queues.
- Error Handling: Stack frames help trace errors, as the call stack holds information about function calls leading up to the error.
- Debugging: Call stacks are invaluable for debugging, as they show the current active function and its context, aiding in analyzing issues and pinpointing errors.
Limitations:
- Stack Overflow: Recursion or deeply nested function calls can potentially cause a stack overflow if the call stack exceeds a predefined limit, leading to a program crash.
- Asynchronous Operations: The call stack doesn’t directly manage asynchronous operations like setTimeout or network requests. These are handled differently by the event loop or Promise queues.
Q9: What is function currying? What are its pros and cons? Explain with example and use case
In JavaScript, function currying is a technique that involves transforming a function that takes multiple arguments into a series of functions that each take a single argument. This essentially “breaks down” the function into smaller, modular units.
Key Idea:
- Instead of calling a function with all arguments at once (
f(a, b, c)
), you create a chain of nested functions, where each function takes only one argument and returns another function:
const originalFunction = (a, b, c) => ...;
const curriedFunction = (a) => (b) => (c) => originalFunction(a, b, c);
- Each invocation of a curried function “fixes” one argument and returns a new function expecting the next argument.
Pros of Currying:
- Partial application: You can create partially applied functions by calling the curried function with some but not all arguments. This is useful for storing and passing around pre-configured functions with fixed settings.
- Modularization: Breaking down a complex function into smaller units can improve code readability and maintainability.
- Composition: Curried functions can be easily combined and composed to create new functions with different functionalities.
Cons of Currying:
- Readability: Curried functions can sometimes be less readable than traditional multi-argument functions, especially for simple cases.
- Performance: In some scenarios, currying might introduce slight overhead due to creating additional function closures.
Example:
Here’s a traditional function for converting temperature from Celsius to Fahrenheit:
function celsiusToFahrenheit(celsius) {
return celsius * (9/5) + 32;
}
Using currying, we can create a more modular version:
const toFahrenheit = celsius => (fahrenheit) => {
return celsius * (9/5) + 32;
};
const convert = toFahrenheit(10); // Partially apply to fix Celsius value
const fahrenheit = convert(); // Call to get the result
console.log(fahrenheit); // Output: 50
Use Case:
A common use case for currying is creating “configuration functions” that take various settings and return customized functions. For example, you could create a curried function for filtering data based on different criteria, allowing users to specify only the desired filters they need.
Also Read: How to Integrate Google Calendar API?
Q10: What is callback hell? How can you avoid callback hell in JavaScript?
Callback hell describes a situation where you chain multiple functions together using callbacks, resulting in nested code that becomes difficult to read, understand, and maintain. These nested callbacks create a pyramid-like structure, hence the “hell” analogy.
Causes of Callback Hell:
- Asynchronous operations like network requests or timers often rely on callbacks to signal completion.
- Nesting these callbacks for multiple asynchronous operations leads to tangled and hard-to-follow code.
Consequences:
- Debugging becomes challenging due to the complex nesting and lack of clear execution flow.
- Error handling becomes cumbersome, as errors can propagate through multiple layers of callbacks.
- Code readability and maintainability suffer, making it difficult to understand and modify later.
Avoiding Callback Hell:
Fortunately, you have options to escape the clutches of callback hell:
- Promises: Introduced in ES6, promises provide a cleaner way to handle asynchronous operations. They offer a more intuitive syntax and chaining mechanism, improving code readability and error handling.
- Async/Await: Built upon promises, async/await offers a synchronous-like syntax for working with asynchronous code. It eliminates the need for explicit callback chaining, making your code look and feel more linear.
- Third-party libraries: Libraries like
async
andbluebird
offer tools and abstractions to simplify asynchronous programming and avoid callback hell.
Example (Callback Hell vs. Promise):
Ordering a Pizza with and without Callback Hell:
Callback Hell:
- You call a restaurant function
orderPizza(size, toppings, callback)
. orderPizza
calls the kitchen functionpreparePizza(size, toppings, callback)
.preparePizza
bakes the pizza and then calls the delivery functiondeliverPizza(address, callback)
.deliverPizza
delivers the pizza and then calls yourcallback
function to let you know it’s arrived.
But now imagine you want to add drinks and sides:
- You nest another callback inside the initial
orderPizza
callback to handle drinks. - You nest another callback inside the drinks callback to handle sides.
- The code becomes deeply nested and hard to follow.
Using Promises:
- You call
orderPizza(size, toppings)
and it returns a promise. - You use
then
on the promise to wait for the pizza to be ready. - Inside the
then
, you callorderDrinks()
andorderSides()
, each returning their own promises. - You use
then
again on each of those promises to wait for them to finish. - Once everything is ready, your final
then
receives all the data and you can enjoy your meal!
The promise approach keeps the code cleaner and easier to read, even with multiple asynchronous tasks.
Callback Hell:
function getData(url, callback) {
fetch(url)
.then(response => response.json())
.then(data => {
console.log(data);
processData(data, (processedData) => {
// More nested callbacks...
});
})
.catch(error => {
// Error handling within the pyramid
});
}
Using Promise:
function getData(url) {
return fetch(url)
.then(response => response.json())
.then(data => {
console.log(data);
return processData(data);
})
.catch(error => {
// Handle error centrally
});
}
getData("https://api.example.com/data")
.then(processedData => {
// Use the processed data
});
Remember:
- Choose the approach that best suits your project’s requirements and team’s familiarity.
- Consider the complexity of your asynchronous operations and the overall readability of your code.
- Embrace modern JavaScript features like promises and async/await to write cleaner and more maintainable asynchronous code.
Closing Note
Thank you for taking the time to read through these JavaScript interview questions. I hope they provide you with the insight needed to excel in your upcoming interviews. If you’re interested in learning more about my work or if you have any questions, don’t hesitate to visit my portfolio page. Let’s connect and build the future of web development together!