Most Frequently asked JavaScript Interview Questions

author image Hirely
at 05 Jan, 2025

Question: How does JavaScript handle asynchronous operations?

Answer:

JavaScript handles asynchronous operations using a combination of mechanisms, including callbacks , Promises , and async/await . As JavaScript is single-threaded (meaning it can only execute one task at a time), these mechanisms allow JavaScript to perform tasks like reading files, making HTTP requests, or waiting for timers, without blocking the main thread. This non-blocking behavior ensures that applications can remain responsive, especially in the browser, where operations like UI rendering and event handling should not be delayed.

Key Mechanisms JavaScript Uses to Handle Asynchronous Operations:

  1. Callbacks:
  • Callbacks are functions that are passed as arguments to other functions and are executed once a task completes.

  • The idea behind callbacks is that instead of blocking the execution, the code continues running, and when an asynchronous operation completes, the callback is triggered. Example:

function fetchData(callback) {
  setTimeout(() => {
    callback('Data fetched');
  }, 2000);
}

fetchData(function(data) {
  console.log(data);  // Will print "Data fetched" after 2 seconds
});

console.log('Fetching data...');
  • In this example, the setTimeout() function simulates an asynchronous operation. The callback passed to fetchData will be executed after 2 seconds when the data is “fetched”.

  • Downside of callbacks: Callback functions can lead to “callback hell” (or “pyramid of doom”), where callbacks are nested inside each other, making the code harder to read and maintain.

  1. Promises:
  • Promises are objects that represent the eventual completion (or failure) of an asynchronous operation. They provide a more structured way to handle asynchronous code and avoid callback hell.

  • A Promise can be in one of three states:

    • Pending : The operation is still in progress.

    • Resolved (Fulfilled) : The operation has completed successfully.

    • Rejected : The operation failed. Example:

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('Data fetched');
    }, 2000);
  });
}

fetchData()
  .then((data) => {
    console.log(data);  // Will print "Data fetched" after 2 seconds
  })
  .catch((error) => {
    console.error(error);  // Handles errors if any
  });

console.log('Fetching data...');
  • Explanation:
    • The fetchData function returns a Promise . After 2 seconds, the Promise resolves with the value 'Data fetched'.

    • then() is used to handle the resolved value when the Promise is fulfilled.

    • catch() is used to handle any errors that may occur during the asynchronous operation.

  • Advantages of Promises: Promises avoid deeply nested callbacks and provide a cleaner way to handle asynchronous flow.
  1. Async/Await:
  • async and async and await are syntactic sugar built on top of Promises, designed to make asynchronous code easier to read and write.

  • async is used to declare a function that will always return a Promise.

  • await is used inside an async function to pause the execution until the Promise is resolved or rejected. Example:

async function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('Data fetched');
    }, 2000);
  });
}

async function getData() {
  console.log('Fetching data...');
  const data = await fetchData();  // Pauses here until fetchData resolves
  console.log(data);  // Will print "Data fetched" after 2 seconds
}

getData();
  • Explanation:
    • The fetchData function is asynchronous, and it returns a Promise.

    • Inside the getData function, await pauses the function’s execution until the Promise from fetchData() resolves, at which point the result is stored in the data variable.

  • Advantages of async/await: It allows asynchronous code to be written in a synchronous style, making it more readable and easier to manage. It eliminates the need for then() chaining, and error handling is simpler with try/catch.
  1. The Event Loop and Task Queue: JavaScript uses an event-driven model to handle asynchronous operations. This is where the event loop comes into play.
  • The event loop constantly checks if the call stack is empty. If the call stack is empty and there are tasks in the task queue (such as the resolution of a Promise or the execution of a setTimeout), the event loop will move the task from the task queue to the call stack for execution.

  • The call stack is where the currently executing functions are stored, and the task queue holds asynchronous operations that are waiting to be executed. Example of the Event Loop:

console.log('Start');

setTimeout(() => {
  console.log('This is asynchronous');
}, 0);  // Task queued to be executed after the call stack is empty

console.log('End');
  • Output:
    • Start

    • End

    • This is asynchronous (after the synchronous code has executed) Explanation:

  • Even though the setTimeout function has a delay of 0 milliseconds, it is still an asynchronous operation. Therefore, it is placed in the task queue and only executed after the synchronous code (the two console.log() statements) has finished executing.
  1. Microtasks and Macrotasks:
  • The event loop handles two types of queues: the microtask queue and the macrotask queue .

  • Microtasks include Promise callbacks (then(), catch(), finally()), and they have a higher priority. They are executed after the current script and before the next macrotask (e.g., setTimeout).

  • Macrotasks include things like setTimeout, setInterval, I/O operations, etc. Example:

console.log('Start');

setTimeout(() => {
  console.log('Macrotask');
}, 0);

Promise.resolve().then(() => {
  console.log('Microtask');
});

console.log('End');
  • Output:
    • Start

    • End

    • Microtask (from the microtask queue)

    • Macrotask (from the macrotask queue) Explanation:

  • The microtask (Promise.resolve().then(...)) is executed before the macrotask (setTimeout(...)), even though both have the same delay of 0 milliseconds, due to the priority of microtasks over macrotasks.

Summary:

JavaScript handles asynchronous operations primarily through three mechanisms:

  1. Callbacks : Functions passed as arguments to be executed after a task finishes. While simple, they can lead to callback hell.

  2. Promises : Objects that represent the eventual outcome of an asynchronous operation, allowing chaining of then() and catch() to handle resolved values and errors.

  3. Async/Await : Modern syntax built on Promises that allows asynchronous code to be written in a synchronous style, improving readability and making error handling simpler. The event loop and task queues are at the heart of JavaScript’s non-blocking behavior, ensuring that while asynchronous tasks run in the background, the main thread can continue processing other tasks without being blocked.

Question: What is the spread operator in JavaScript?

Answer:

The spread operator in JavaScript is denoted by three consecutive dots (...) and is used to expand or spread elements from an iterable (like an array, object, or string) into individual elements. It is a useful tool for working with arrays, objects, and function arguments, making code more concise and readable.1. Using the Spread Operator with Arrays: The spread operator can be used to expand elements from an array into another array or to create a shallow copy of an array. Example - Copying an Array:

const arr1 = [1, 2, 3];
const arr2 = [...arr1]; // Spread arr1 into arr2

console.log(arr2); // [1, 2, 3]
  • Here, arr1 is spread into arr2, effectively creating a shallow copy of arr1. Example - Combining Arrays:
const arr1 = [1, 2];
const arr2 = [3, 4];
const arr3 = [...arr1, ...arr2]; // Combining arrays

console.log(arr3); // [1, 2, 3, 4]
  • The elements of arr1 and arr2 are “spread” into a new array, arr3. Example - Adding Elements to an Array:
const arr = [1, 2, 3];
const newArr = [0, ...arr, 4]; // Adding elements before and after the original array

console.log(newArr); // [0, 1, 2, 3, 4]
  • The spread operator can also be used to insert elements before or after an existing array’s elements. 2. Using the Spread Operator with Objects: The spread operator can also be used to copy properties from one object to another or to merge multiple objects. Example - Copying an Object:
const obj1 = { name: 'John', age: 30 };
const obj2 = { ...obj1 }; // Shallow copy of obj1

console.log(obj2); // { name: 'John', age: 30 }
  • Here, obj1 is spread into obj2, creating a shallow copy of the object. Example - Merging Objects:
const obj1 = { name: 'John' };
const obj2 = { age: 30 };
const mergedObj = { ...obj1, ...obj2 };

console.log(mergedObj); // { name: 'John', age: 30 }
  • You can use the spread operator to combine properties from multiple objects into a new object. Example - Overriding Properties:
const obj1 = { name: 'John', age: 30 };
const obj2 = { age: 35 };
const mergedObj = { ...obj1, ...obj2 };

console.log(mergedObj); // { name: 'John', age: 35 }
  • When merging objects, if there are conflicting keys, the properties of the later object (obj2 in this case) will overwrite those of the earlier object (obj1). 3. Using the Spread Operator in Function Arguments: The spread operator can also be used to spread elements from an array or object into individual arguments when calling a function. Example - Passing Elements as Function Arguments:
const numbers = [1, 2, 3];
function add(a, b, c) {
  return a + b + c;
}

console.log(add(...numbers)); // 6
  • The elements of the numbers array are spread into individual arguments when calling the add() function. 4. Spread Operator with Strings: The spread operator can also be used with strings, which are iterable, to spread each character into an array. Example - Spreading a String into an Array:
const str = "hello";
const chars = [...str];

console.log(chars); // ['h', 'e', 'l', 'l', 'o']
  • Here, each character of the string str is spread into individual elements of the array chars. 5. Rest Parameter vs. Spread Operator: While both the rest parameter (...) and the spread operator use the same syntax, they serve different purposes:
  • Rest parameter : Gathers remaining arguments into an array when defining a function.
function sum(...numbers) {
  return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
  • The rest parameter is used to capture all arguments passed to the function into a single array.

  • Spread operator : Expands an array or object into individual elements when calling a function or constructing a new array/object.

const arr = [1, 2, 3];
const newArr = [...arr, 4, 5];
console.log(newArr); // [1, 2, 3, 4, 5]
  • The spread operator is used to unpack elements of an array or properties of an object. Key Differences Between Spread and Rest: | Feature | Spread Operator | Rest Parameter | | --- | --- | --- | | Usage | Expands elements (arrays, objects) | Collects multiple arguments into a single array | | Where Used | Function arguments, arrays, objects | Function parameter declaration | | Purpose | To break an iterable into individual elements | To gather remaining arguments into an array | | Example | const newArr = […arr, 4, 5] | function sum(…nums) { … } | Summary:

  • The spread operator (...) is used to expand or unpack elements of an iterable (array, object, string) into individual elements.

  • It is useful for creating shallow copies of arrays and objects, combining arrays, merging objects, and spreading elements as function arguments.

  • It improves code readability and reduces the need for more verbose approaches to copying or merging data structures.

Read More

If you can’t get enough from this article, Aihirely has plenty more related information, such as JavaScript interview questions, JavaScript interview experiences, and details about various JavaScript job positions. Click here to check it out.

Related Posts

Trace Job opportunities

Hirely, your exclusive interview companion, empowers your competence and facilitates your interviews.

Get Started Now