Essential JavaScript Interview Questions: Mastering the Fundamentals ๐Ÿš€

Essential JavaScript Interview Questions: Mastering the Fundamentals ๐Ÿš€

ยท

12 min read

JavaScript is a foundational programming language that powers the dynamic and interactive aspects of modern web development. As you prepare for a JavaScript interview, having a solid grasp of its core concepts is paramount. In this blog post, we'll delve into some of the most important JavaScript interview questions to help you master the fundamentals and approach your interviews with confidence.

var vs let vs const

When it comes to declaring variables in JavaScript, the choice between var, let, and const can significantly impact our code's behavior. Variables declared with var and let are mutable whereas variables declared with the keyword const are immutable. Let's look at some of the differences between them.

  • let and const variables are hoisted but they are not initialized with a default value like variables declared with var.

      console.log(a); // can not access before initialization.
      let a = 10;
      console.log(b); // undefined
      var b = '10';
    
  • Variables declared with keyword var is attached to the window object whereas variables declared with let and const are not.

      window.b; // '10'
      window.a; // undefined
    
  • We can't redeclare a let or const variable while with var it is allowed.

      let a = 5;
      a = 10; // No issues!
    
      const b = "You can't change me!"; // TypeError: Assignment to constant variable.
      b = 'Really?';
    
  • Variables with let and var can be declared and assigned value later but variables declared with const is bound to be assigned while declaration.

      const c; // SyntaxError: Missing initializer in const declaration
    

Scopes, Scope Chain and Shadowing

Scope

In JavaScript, scope determines where variables and functions are accessible and visible within your code. There are three main types of scope: global, function, and block. Let's explore each:

  • Global Scope: Variables declared outside of any function or block have a global scope. They can be accessed from any part of the code, including within functions or blocks.

      // App.js
      var x = '10'; // x is a global variable now
    
  • Function Scope: Variables declared inside a function are limited to that function's scope. They can't be accessed outside the function.

      function greet() {
        var message = "Hello, function scope!"; 
        console.log(message); // message can be accessed throughout the function.
      }
    
      console.log(message); //
    
  • Block Scope: Introduced with ES6, variables declared inside a block (within curly braces) have block scope. They're only accessible within that block and any nested blocks.

      for(let i = 0; i < 5; i++)
          console.log(i); // i is accessible only inside this for loop
      }
    

Scope Chain

The scope chain is a hierarchical structure that determines the accessibility and visibility of variables and functions in a JavaScript program. It's created based on the nesting of functions and blocks within the code. When a variable or function is accessed, JavaScript searches through the scope chain to find where it's defined.

Here's a simplified explanation of how the scope chain works:

  1. Global Scope: At the top level, there's the global scope. Variables declared outside of any functions or blocks belong to this scope. They are accessible throughout the entire code.

  2. Function Scope: When a function is defined, it creates its scope. This scope can access variables from its parent scope (outer function or global), but the parent scope cannot access variables from within the child scope.

  3. Lexical Scope: JavaScript uses lexical scoping, which means that the scope chain is determined by the physical arrangement of code in the source file. Inner scopes can access variables from outer scopes, but not vice versa.

Here's an example to illustrate the scope chain:

var globalVar = "I'm global";

function outer() {
  var outerVar = "I'm in outer scope";

  function inner() {
    var innerVar = "I'm in inner scope";
    console.log(globalVar);   // Accessible (global scope)
    console.log(outerVar);    // Accessible (outer scope)
    console.log(innerVar);    // Accessible (current scope)
  }

  inner();
}

outer();

console.log(globalVar);       // Accessible (global scope)
// console.log(outerVar);     // Not accessible (outer scope of 'outer')
// console.log(innerVar);     // Not accessible (inner scope of 'inner')

Shadowing

Shadowing refers to the situation where a variable declared within a certain scope has the same name as a variable in an outer scope. When this occurs, the inner variable "shadows" or takes precedence over the outer variable within its scope, effectively blocking access to the outer variable.

Let's see an example,

var x = 10;  // Outer variable

function example() {
  var x = 5; // Inner variable, shadows the outer 'x'
  console.log(x); // Outputs: 5 (inner variable)
}

example();
console.log(x); // Outputs: 10 (outer variable)

Hoisting

Hoisting is a JavaScript behavior where variable and function declarations are moved to the top of their containing scope during the compilation phase before the code is executed.

The variables declared with the keyword var are initialized to undefined whereas the functions are initialized with the code they have been declared with.

console.log(a); // undefined
var a = 10;

console.log(fun); // f(){...}
function fun(a){
    console.log('Hola Amigos!');
}

It is worth noting that variables and constants declared with var and const are hoisted but they are not initialized with a default value and they are in a special state known as the Temporal Dead Zone which doesn't let us use the variables and constants before its declaration.

Types of Error in JS

Various kinds of errors in JavaScript are discussed below

  • ReferenceError: These occur when you try to use a variable or function that hasn't been declared or is out of scope. This often happens due to typos or incorrect variable names.

  • Syntax Errors: These occur when you write code that violates the rules of the JavaScript language syntax. It could be a missing parenthesis, an unclosed string, or a typo that prevents the code from being properly parsed.

  • Type Errors: These occur when you try to operate on a value that's of the wrong data type or when a value is not defined. For example, attempting to call a non-function or accessing properties of undefined will result in a type error.

  • Range Errors: These occur when you try to use a value that's outside the range of acceptable values. For example, using a negative array index or setting a value that exceeds a maximum limit could lead to a range error.

Closures

  • A closure is a combination of functions bundled together with references to its surrounding state or lexical environment.

  • Every closure has three scopes,

    1. Local,

    2. Enclosing Scope ( block and function)

    3. Global

  • Usage of closures:-

    • Module Design Pattern

    • Currying

    • Memoize

    • Data hiding and encapsulation

    • setTimeout()

  • Disadvantages:-

    • Over memory consumption

    • Memory leak

    • Freeze browser

Callback, Promise and Async...Await

Callback, Promise, and Async/await are three different approaches in JavaScript for managing asynchronous operations and handling asynchronous code. Each approach has its advantages and uses cases. Let's take a brief look at each of them:

Callback

Callbacks are a traditional way of handling asynchronous operations in JavaScript. A callback function is passed as an argument to an asynchronous function. Once the asynchronous operation is completed, the callback is executed. Callbacks can lead to callback hell or the "Pyramid of Doom" when dealing with nested asynchronous operations.

function fetchData(callback) {
  setTimeout(() => {
    const data = "Fetched data";
    callback(data);
  }, 1000);
}

fetchData((result) => {
  console.log(result);
});

Promise

  • Promises are used to handle asynchronous operations in JavaScript.

  • A promise object represents the eventual completion or failure of an asynchronous operation and its resulting value.

  • A promise has one of the three states,

    1. pending: Initial state, neither fulfilled nor completed

    2. fulfilled: Represents the operation is successful.

    3. rejected: Represents the operation has failed.

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = "Fetched data";
      resolve(data);
    }, 1000);
  });
}

fetchData()
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error(error);
  });

Async...Await

Async/await is a modern syntax for handling asynchronous code that makes it look more like synchronous code. The async keyword is used to define a function that returns a Promise and the await keyword is used inside that function to pause execution until the Promise is resolved or rejected.

async function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      const data = "Fetched data";
      resolve(data);
    }, 1000);
  });
}

async function main() {
  try {
    const result = await fetchData();
    console.log(result);
  } catch (error) {
    console.error(error);
  }
}

main();

Array methods

There are so many array methods in JavaScript, but three of them are the most important. You might also be asked to implement polyfills of these methods.

map()

The map() returns a new array by transforming each element of the array with the provided callback function.

const numbers = [1, 2, 3, 4, 5];
const squaredNumbers = numbers.map((num) => num * num);
console.log(squaredNumbers); // Outputs: [1, 4, 9, 16, 25]

filter()

The filter() returns a new array by filtering elements of the array based on a login provided in the callback function.

const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter((num) => num % 2 === 0);
console.log(evenNumbers); // Outputs: [2, 4]

reduce()

The reduce() returns a single value by accumulating the results obtained by performing on each of the array elements.

const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
console.log(sum); // Outputs: 15 (1 + 2 + 3 + 4 + 5)

this object

The this keyword in JavaScript is a reference to the object that is currently executing or being operated upon. Its value depends on the context in which it is used, and it can behave differently in different situations. Here's an overview of how this works in various contexts:

  1. Global Context: In the global context (outside of any function or object), this refers to the global object. In a browser environment, this is usually the window object.

     console.log(this); // Outputs: [object Window] (in a browser environment)
    
  2. Function Context: In a regular function, this refers to the global object (in non-strict mode) or undefined (in strict mode). However, when a function is a method of an object, this refers to the object itself.

     function showThis() {
       console.log(this);
     }
    
     showThis(); // Outputs: [object Window] (non-strict mode)
    
     const person = {
       name: "Alice",
       sayName: function() {
         console.log(this.name);
       }
     };
    
     person.sayName(); // Outputs: "Alice"
    
  3. Arrow Functions: Arrow functions behave differently with this compared to regular functions. In arrow functions, this is lexically scoped, meaning it retains the value of this from its surrounding code.

     const person = {
       name: "Bob",
       sayName: () => {
         console.log(this.name); // 'this' refers to the global object, not 'person'
       }
     };
    
     person.sayName(); // Outputs: undefined (if in browser environment)
    
  4. Constructor Functions: In constructor functions, this refers to the newly created instance of the object.

    Example:

     function Person(name) {
       this.name = name;
     }
    
     const alice = new Person("Alice");
     console.log(alice.name); // Outputs: "Alice"
    

Prototypes and Prototypal Inheritance in JavaScript

Prototypes

Prototypes are a fundamental mechanism that allows objects to inherit properties and methods from other objects. Every object in JavaScript has an associated prototype, which serves as a template for defining shared properties and behaviors

Each object is linked to a prototype object, and this linkage forms a chain known as the "prototype chain." When you access a property or method on an object, JavaScript first checks the object itself. If the property or method is not found, it continues searching in the prototype chain until it reaches the top-level prototype

Prototypal Inheritance

Prototypal inheritance is a core concept in JavaScript's object-oriented programming model. It allows objects to inherit properties and methods from other objects, forming a hierarchical structure where shared behaviors and attributes are defined in prototypes.

Event Loop

An event loop is something that pulls tasks out of the callback queue and places them into the call stack whenever the call stack becomes empty.

Watch this to know more

Timers in JavaScript โ€“ setTimeout, setInterval, clearInterval

The setTimeout() method calls a function or evaluates an expression after a specified time (number of milliseconds).

setTimeout(() => {
    console.log('I will be called after 2 seconds.')
}, 2000);

The clearInterval() is used to stop the timer.

const timer = setTimeout(() => {
    console.log('I will be called after 2 seconds.')
}, 2000);

clearInterval(timer);

The setInterval() method calls a function at specified intervals (in milliseconds).

setInterval(()=>{
    console.log('Hi');
}, 1000);

Debounce and Throttling

Debouncing ensures that a function is executed only after a certain period of inactivity has passed since the last time the function was invoked. It's useful when you want to delay the execution of a function until a series of rapid-fire events have settled down.

Example use cases: Handling search suggestions as a user types or resizing elements on window resize.

function debounce(func, delay) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay);
  };
}

const debouncedFunction = debounce(() => {
  console.log("Debounced function executed");
}, 300);

// Attach the debounced function to an event listener
window.addEventListener("scroll", debouncedFunction);

Throttling limits the frequency of function execution by ensuring that the function is executed at a fixed interval. It's useful when you want to prevent a function from being invoked too often, even if the events are coming in rapidly.

Example use cases: Limiting the rate of API requests or preventing excessive scrolling events.

function throttle(func, delay) {
  let isThrottled = false;
  return function(...args) {
    if (!isThrottled) {
      func.apply(this, args);
      isThrottled = true;
      setTimeout(() => {
        isThrottled = false;
      }, delay);
    }
  };
}

const throttledFunction = throttle(() => {
  console.log("Throttled function executed");
}, 300);

// Attach the throttled function to an event listener
window.addEventListener("scroll", throttledFunction);

Storage in JavaScript

In JavaScript, "storage" generally refers to the mechanisms provided by browsers to store data persistently or temporarily within a web application. There are two main types of storage: Local Storage and Session Storage.

Local Storage

Local Storage allows you to store key-value pairs in a web browser with no expiration date. The data remains available even after closing the browser or navigating away from the page. Local Storage is often used to store user preferences, settings, or cached data.

Example of setting and retrieving data in Local Storage:

// Set data in Local Storage
localStorage.setItem("theme", "dark");

// Retrieve data from Local Storage
const username = localStorage.getItem("theme");
console.log(username); // Outputs: "john_doe"

Session Storage

Session Storage is similar to Local Storage, but the data is available only for the duration of the session. It is accessible within the same browser tab or window and is cleared when the tab or window is closed. Session Storage is often used for maintaining the state between different pages of a web application.

Example of setting and retrieving data in Session Storage:

// Set data in Session Storage
sessionStorage.setItem("language", "english");

// Retrieve data from Session Storage
const language = sessionStorage.getItem("language");
console.log(language); // Outputs: "english"

That's it in this article, I hope it helps you while preparing for frontend interviews ๐Ÿš€.

ย