paint-brush
Compilation of the Most Common JavaScript Interview Questionsby@ljaviertovar
250 reads

Compilation of the Most Common JavaScript Interview Questions

by L Javier TovarOctober 6th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

This post provides a detailed list of JavaScript interview questions, from basic to advanced levels, to help you practice and prepare for technical interviews.
featured image - Compilation of the Most Common JavaScript Interview Questions
L Javier Tovar HackerNoon profile picture

Basic

What are the different data types in JavaScript?

Primitive Types:

  • String: Represents textual data, e.g., "Hello".

  • Number: Represents numerical values, e.g., 42 or 3.14.

  • Boolean: Represents logical values, either true or false.

  • Undefined: Represents an uninitialized variable, e.g., let x;.

  • Null: Represents the absence of any value, e.g., let y = null;.

  • Symbol: Represents a unique and immutable value, often used as object keys.

  • BigInt: Represents integers larger than Number.MAX_SAFE_INTEGER.


Non-Primitive Types:

  • Object: Represents collections of key-value pairs, e.g., {name: "John", age: 30}.

  • Function: A special type of object that can be invoked, e.g., function() {}.


What is the difference between == and ===?

  • The ==operator (loose equality) compares two values for equality after converting both values to a common type.
  • The=== operator (strict equality) compares two values for equality without performing type conversion. Both the value and the type must be the same.


What is the difference between null and undefined?

  • null is an assignment value that represents the intentional absence of any object value.
  • undefined means a variable has been declared but has not yet been assigned a value.


What is the difference between let and const?

  • let allows you to reassign values to the variable.
  • const does not allow reassignment of the variable value and must be initialized at the time of declaration.


What is the purpose of typeof in JavaScript?

The typeof operator is used to determine the type of a given variable or expression. It returns a string indicating the type of the operand.


What is NaN in JavaScript?

NaN stands for "Not-a-Number" and represents a value that is not a legal number. It typically occurs when a mathematical operation fails or when trying to convert a non-numeric string to a number.


How do you check if a value is an array in JavaScript?

Use Array.isArray() to check if a value is an array.


const arr = [1, 2, 3];
console.log(Array.isArray(arr));  // true


How do you add or remove elements from an array?

  • Adding Elements:

    • push(): Adds elements to the end of an array.

      jsCopy codearr.push(4);  // [1, 2, 3, 4]
      
      
    • unshift(): Adds elements to the beginning of an array.

      jsCopy codearr.unshift(0);  // [0, 1, 2, 3, 4]
      
      


  • Removing Elements:

    • pop(): Removes the last element.

      jsCopy codearr.pop();  // [0, 1, 2, 3]
      
      
    • shift(): Removes the first element.

      jsCopy codearr.shift();  // [1, 2, 3]
      


What is the purpose of the this keyword in JavaScript?

The this keyword refers to the object from which the function was called. Its value depends on the context in which the function is called:

  • In a method, this refers to the object that owns the method.

  • Alone, this refers to the global object (in browsers, it's window).

  • In a function, this refers to the global object (in strict mode, this is undefined).

  • In an event, this refers to the element that received the event.

  • In an arrow function, this retains the value of the enclosing lexical context's this.


const persona = { 
  nombre : 'Alice' , 
  saludo : función () { 
              console.log( ' Hola , ' + this.name ); 
           }   
} 

person.greet(); // "Hola, Alice"


What ES6 features do you use?

Some of the most commonly used ES6 features include let and const, arrow functions, template literals, destructuring, default parameters, rest and spread operators, classes, and modules.

How do you handle errors in JavaScript?

Errors in JavaScript can be handled using try, catch, finally, and throw statements.

  • try: Wraps a block of code that might throw an error.

  • catch: Executes a block of code if an error is thrown in the try block.

  • finally: Executes a block of code after the try and catch blocks, regardless of whether an error was thrown.

  • throw: Throws a custom error.


try {
  let result = riskyOperation();
} catch (error) {
  console.error('An error occurred:', error.message);
} finally {
  console.log('This code runs regardless of success or error.');
}

function riskyOperation() {
  throw new Error('Something went wrong!');
}


What is the difference between if-else and the ternary operator?

  • The if-else statement is used for conditional branching where multiple lines of code can be executed based on the condition.

  • The ternary operator is a shorthand for if-else that can be used when you need to assign a value based on a condition. It is more concise but is typically used for simple expressions.


What is hoisting?

Hoisting is JavaScript’s behavior of moving declarations to the top of the current scope (global or function scope). This means variables and function declarations can be used before they are declared.

What are JavaScript built-in methods for string manipulation?

Some common built-in methods for string manipulation include:

charAt() concat() includes() indexOf() lastIndexOf() replace() split() substring() toLowerCase() toUpperCase() trim()


Name some methods you know to work with arrays.

Some array methods include push, pop, shift, unshift, map, filter, reduce, find, forEach, some, every, concat, slice, and splice.


What is the use of the map method? How about find and filter?

  • map creates a new array with the results of calling a function on every element in the calling array.
  • find returns the first element in the array that satisfies the provided testing function.
  • filter creates a new array with all elements that pass the test implemented by the provided function.


What is the difference between slice() and splice() in arrays?

  • slice():

    • Returns a shallow copy of a portion of an array.
    • It does not modify the original array.
    • Takes two arguments: the start index and the end index (end not inclusive).


    const arr = [1, 2, 3, 4, 5];
    const sliced = arr.slice(1, 3);  // [2, 3]
    console.log(arr);  // [1, 2, 3, 4, 5]
    
    
  • splice():

    • Adds, removes, or replaces elements in an array in place (it modifies the original array).
    • Takes three arguments: start index, number of elements to remove, and the elements to add.


    const arr = [1, 2, 3, 4, 5];
    const spliced = arr.splice(1, 2);  // Removes 2 and 3
    console.log(arr);  // [1, 4, 5]
    


What is the difference between map and reduce?

map creates a new array with the results of calling a provided function on every element in the calling array.

reduce executes a reducer function on each element of the array, resulting in a single output value.


Explain the difference between for, for...of, and for...in loops.

  • for: General-purpose, often used for numeric and index-based iteration.

  • for...of: Iterates over values of an iterable (arrays, strings, etc.).

  • for...in: Iterates over object keys or property names. Be cautious with arrays, as it iterates over index keys, not values.


What is the use of callback functions?

Callback functions are used to handle asynchronous operations. They are passed as arguments to other functions and are invoked after an operation is completed, allowing you to execute code in response to the outcome of the asynchronous operation.


What are arrow functions?

Arrow functions are a concise way to write function expressions in JavaScript. They use the => syntax and do not have their own this context, which makes them useful in situations where you want to preserve the context of this from the enclosing scope.


What is the difference between promises and callbacks?

  • Callbacks are functions passed into other functions as arguments to be executed later.

  • Promises are objects representing the eventual completion or failure of an asynchronous operation, allowing you to chain operations and handle errors more gracefully.


How can you handle errors when fetching data?

You can handle errors when fetching data by using try...catch blocks or handling the promise rejections using .catch() method.


What is an IIFE (Immediately Invoked Function Expression)?

An IIFE is a function that is executed immediately after its definition. It is commonly used to create a local scope and avoid polluting the global namespace.


( función () { 
  const message = 'IIFE ejecutado' ; 
  console . log (mensaje); 
})(); // "IIFE ejecutado"


What are promises?

Promises are objects representing the eventual completion or failure of an asynchronous operation. They provide a way to handle asynchronous operations more gracefully by allowing you to chain .then() and .catch() methods to handle successful outcomes and errors, respectively.


What is the difference between setTimeout and setInterval?

  • setTimeout executes a function once after a specified delay.
  • setInterval executes a function repeatedly at specified intervals.


Do you know the spread operator? What is its use?

The spread operator (...) allows an iterable such as an array or string to be expanded in places where zero or more arguments (for function calls) or elements (for array literals) are expected, or an object expression to be expanded in places where zero or more key-value pairs (for object literals) are expected.


Where do you usually use the rest operator?

The rest operator (`…`) is used in function parameters to collect all remaining arguments into an array, and in array and object destructuring to collect the rest of the elements/properties.


What are template literals, and how do you use them?

Template literals are string literals enclosed in backticks (`) that allow for multi-line strings and interpolation of expressions.


const name = 'John';
const greeting = `Hello, ${name}!`;
console.log(greeting); // Output: Hello, John!


What is the purpose of JSON.stringify() and JSON.parse()?

  • JSON.stringify(): Converts a JavaScript object or array into a JSON string.

    jsCopy codeconst obj = { name: 'John' };
    const jsonString = JSON.stringify(obj); // '{"name":"John"}'
    
    


  • JSON.parse(): Converts a JSON string back into a JavaScript object.

    jsCopy codeconst jsonString = '{"name":"John"}';
    const obj = JSON.parse(jsonString); // { name: 'John' }
    
    


  • Purpose:

    • Used for data storage (e.g., in localStorage) or sending/receiving data over HTTP in APIs.


Can you explain the Observer pattern in JavaScript and provide a real-world use case where it would be useful?

The Observer pattern is a design pattern where an object (called the subject) maintains a list of observers that are notified of any state changes. When the subject's state changes, all registered observers automatically get updated.

Real-world use case: In JavaScript, the Observer pattern is useful in scenarios like event handling. For example, in a chat application, when a new message arrives (subject changes), all the chat windows (observers) need to update with the new message.


class Subject {
  constructor() {
    this.observers = [];
  }
  addObserver(observer) {
    this.observers.push(observer);
  }
  notifyObservers(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  update(data) {
    console.log(`Received update: ${data}`);
  }
}

const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notifyObservers("New message"); // Both observers get the update


What are some common design patterns used in JavaScript?

  • Singleton Pattern: Ensures only one instance of a class exists. Useful for global app settings or state management.
  • Factory Pattern: Creates objects without specifying the exact class. This adds flexibility to create various types of objects dynamically.
  • Module Pattern: Encapsulates code within modules, providing privacy (by using closures) and allowing reusable, maintainable code.
  • Observer Pattern: Decouples components by notifying observers of changes, improving reusability and event-driven architecture.
  • Decorator Pattern: Enhances objects with additional functionality without altering the original object structure.


How would you implement the Singleton pattern in JavaScript, and when is it appropriate to use it?

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. It's appropriate when you need to maintain a single global instance of a class (e.g., a database connection or a configuration manager).


class Singleton {
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    Singleton.instance = this;
    this.data = "Singleton instance";
  }
  
  getData() {
    return this.data;
  }
}

const instance1 = new Singleton();
const instance2 = new Singleton();

console.log(instance1 === instance2);  // true

Use cases: Logging, configuration management, or managing a single instance of database connections.


What is data streaming in JavaScript, and how does it differ from traditional data handling methods?

Data streaming refers to continuously receiving or sending data in small chunks, rather than waiting for the entire dataset to be available. This is especially useful for handling large data (like video, audio, or file downloads).

Difference from traditional methods:

  • Streaming: Data is processed as it arrives, allowing faster performance and lower memory usage.
  • Traditional methods: Wait for the entire data to load before processing, which can lead to delays and higher memory consumption.

In JavaScript, streaming is often implemented using Streams API (ReadableStream, WritableStream).


What is the difference between unit testing, integration testing, and end-to-end testing in JavaScript?

  • Unit Testing: Focuses on testing individual units of code, such as functions or components, in isolation. The goal is to verify that each unit works correctly on its own.

  • Integration Testing: Tests how different units work together. It ensures that interactions between components or systems (like an API call and its response handling) behave as expected.

  • End-to-End (E2E) Testing: Simulates real user scenarios, testing the entire application workflow from the UI to the backend. Tools like Cypress or Puppeteer are used for E2E testing.


How would you mock API calls in unit tests when testing components that rely on external data?

To mock API calls in unit tests, you can use libraries like Jest with jest.mock() or axios-mock-adapter to simulate HTTP responses, ensuring your tests remain independent from actual API behavior.


import axios from 'axios';
import MyComponent from './MyComponent';
import { render, screen } from '@testing-library/react';

jest.mock('axios');

test('fetches data and renders component', async () => {
  axios.get.mockResolvedValue({ data: { name: 'John' } });

  render(<MyComponent />);
  
  const element = await screen.findByText('John');
  expect(element).toBeInTheDocument();
});


What are some best practices for writing tests with Jest and React Testing Library?

  • Write tests that mimic real user behavior: Focus on how users interact with your application rather than testing internal implementation details.

  • Use screen for queries: It makes tests more readable and avoids dependency on implementation details.

  • Use await findBy... for asynchronous elements: Wait for elements that load asynchronously, like fetched data or delayed UI changes.

  • Mock external services: Use mocking to simulate external API calls and avoid testing network-related issues.

  • Use snapshot testing sparingly: While snapshot tests are useful for detecting UI changes, rely more on tests that check actual DOM structure and behavior.


Write a function to print “fizz” if a number is divisible by 3, “buzz” if the number is divisible by 5, and “fizzbuzz” if the number is divisible by both 3 and 5.

const fun = () => {
    for (let i = 1; i <= 50; i++) {
      if (i % 3 === 0 && i % 5 === 0) {
        console.log(i, 'FIZBUZZ');
      } else if (i % 5 === 0) {
        console.log(i, 'BUZZ');
      } else if (i % 3 === 0) {
        console.log(i, 'FIZ');
      }
    }
  };


Reverse a string.

function reverseString(str) {
    return str.split('').reverse().join('');
}

function reverseString(str) {
  let reversed = '';
  for (let i = str.length - 1; i >= 0; i--) {
    reversed += str[i];
  }
  return reversed;
}


Find the Largest Number in an Array

findLargest(arr) {
  return Math.max(...arr);
}
console.log(findLargest([1, 3, 7, 2, 9])); // Output: 9


Check for Palindrome

function isPalindrome(str) {
  return str === str.split('').reverse().join('');
}
console.log(isPalindrome("racecar")); // Output: true


Write a function to calculate the factorial of a number.

function factorial(n) {
    if (n === 0 || n === 1) {
        return 1;
    } else {
        return n * factorial(n - 1);
    }
}


Return the longest word in a string.

function longestWord(str) {
    const words = str.split(' ');
    let longest = '';
    
    for (let word of words) {
        if (word.length > longest.length) {
            longest = word;
        }
    }
    
return longest;
}


What will be the output?

let a={name: 'Suman', age:22}
let b={name: 'Suman', age:22}

console.log(a === b)
// false 
console.log(a)
console.log(b)

var a = b = 5

// undefined
// ReferenceError: b is not defined
for(var i = 0; i<5; i++) {
  setTimeout(() =>{
    console.log(i)
  },1000)
}

// 5 5 5 5 5

for(let i = 0; i<5; i++) {
  setTimeout(() =>{
    console.log(i)
  },1000)
}
// 0 1 2 3 4
console.log('6' + 5 * 6)
console.log('6' * 5 + 6)
console.log('5' - '3' + 6)

// 630
// 36
// 8
isNAN("Hello")
// true
function fun1 ( ){
    setTimeout(() => {
        console.log(x)
        console.log(y)
    },3000)
    
    var x = 2
    let y = 12
}

fun1()
// 2
// 12
var a = 5
console.log(a++)
console.log(a)
// 5
// 6

console.log(++a)
console.log(a)
// 6
// 6
console.log(1<2<3)
console.log(3>2>1)
// true
// false




Intermediate

Explain the difference between synchronous and asynchronous code.

  • Synchronous code is executed in a sequential manner. Each operation waits for the previous one to complete before moving on to the next one.

  • Asynchronous code allows multiple operations to run concurrently. An asynchronous operation does not block the execution of subsequent code. Instead, it delegates the task to the environment (e.g., the browser or Node.js) and continues executing the rest of the code. Once the asynchronous operation completes, it notifies the main thread through callbacks, promises, or async/await.


What is the purpose of the Promise.all() method?

The Promise.all() method is used to run multiple promises concurrently and wait until all of them have resolved or at least one has rejected. It takes an iterable (like an array) of promises as an input and returns a single promise that:

  • Resolves when all of the input promises have resolved. The resolved value is an array of the resolved values of the input promises, in the same order as the original promises.
  • Rejects when any of the input promises rejects. The rejection reason is the reason of the first promise that rejects.

This method is useful when you need to perform multiple asynchronous operations concurrently and want to proceed only when all of them are completed.


const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3])
  .then((values) => {
    console.log(values); // [3, 42, "foo"]
  })
  .catch((error) => {
    console.error(error);
  });


Explain the difference between Promise.all() and Promise.race() .

  • Promise.all(): Takes an array of promises and resolves when all promises resolve. If any promise rejects, the entire Promise.all() rejects.
  • Promise.race(): Takes an array of promises and resolves/rejects as soon as the first promise resolves or rejects.


What is the difference between function declaration and function expression?

  • Function Declaration: Declared with the function keyword, it is hoisted, meaning it can be used before it's defined.

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


  • Function Expression: Assigned to a variable, and it's not hoisted, meaning it can only be used after the line where it is defined.

    const sayHello = function() {
      console.log('Hello');
    };
    


How does JavaScript handle scope and what is lexical scoping?

  • Scope: Refers to the accessibility of variables. JavaScript has function scope and block scope (using let and const).

  • Lexical Scoping: A function's scope is determined by its position in the code during definition, not where it's called. Inner functions can access variables from their outer functions.


    function outer() {
      const name = "John";
      function inner() {
        console.log(name); // Lexical scoping allows access to 'name'
      }
      inner();
    }
    


What is currying in JavaScript and how does it work?

Currying is a technique where a function is transformed into a series of nested functions that take one argument at a time.


function add(a) {
  return function(b) {
    return a + b;
  };
}

const add5 = add(5);
console.log(add5(3));  // 8

Currying allows partial application of functions and can be useful in functional programming.


How do you debounce or throttle a function in JavaScript?

  • Debounce: Delays a function call until a certain period has passed since the last time it was invoked. Useful for limiting how often a function is executed (e.g., input events).


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


  • Throttle: Ensures a function is only called once in a specified period, regardless of how many times it's triggered (e.g., scroll events).


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


What is the difference between synchronous and asynchronous iteration in JavaScript?

  • Synchronous Iteration: Loops like for or for...of execute iteratively and block the next iteration until the current one finishes.

  • Asynchronous Iteration: Introduced with for await...of, this allows you to iterate over asynchronous data sources (like Promises) where each iteration waits for the asynchronous task to complete.


    async function fetchData() {
      const promises = [promise1, promise2];
      for await (const result of promises) {
        console.log(result);
      }
    }
    


What is the Temporal Dead Zone in JavaScript?

The Temporal Dead Zone (TDZ) refers to the period between the start of a block and the moment a variable is declared. Accessing the variable during this time results in a ReferenceError.

It occurs with variables declared using let and const.


{
  console.log(a); // ReferenceError: Cannot access 'a' before initialization
  let a = 5;
}


How does destructuring assignment work for arrays and objects?

Destructuring allows you to unpack values from arrays or properties from objects into distinct variables.

  • Array Destructuring:

    const [a, b] = [1, 2];
    console.log(a, b);  // 1, 2
    


  • Object Destructuring:

    jsCopy codeconst { name, age } = { name: 'John', age: 30 };
    console.log(name, age);  // John, 30
    

You can also provide default values, rename variables, and extract nested properties.


Explain what Object.freeze() and Object.seal() do.

  • Object.freeze(): Makes an object immutable, meaning you cannot add, remove, or modify properties.


    const obj = Object.freeze({ name: 'John' });
    obj.name = 'Doe';  // Does not change
    
    


  • Object.seal(): Prevents adding or deleting properties, but allows modifying existing properties.


    const obj = Object.seal({ name: 'John' });
    obj.name = 'Doe';  // Modifies name
    obj.age = 30;  // Cannot add new property
    
    


What is the difference between deep copy and shallow copy in JavaScript?

  • Shallow Copy: Only copies the top-level properties of an object. Nested objects are still referenced, so changes to nested objects in the copy affect the original object.


    const original = { a: 1, b: { c: 2 } };
    const shallowCopy = { ...original };
    shallowCopy.b.c = 3;  // Modifies the original object's nested property
    
    


  • Deep Copy: Recursively copies all levels of the object, creating entirely independent objects.


    const original = { a: 1, b: { c: 2 } };
    const deepCopy = JSON.parse(JSON.stringify(original));
    deepCopy.b.c = 3;  // Does not affect the original object
    


What is a Symbol in JavaScript, and why would you use it?

A Symbol is a unique, immutable primitive value used to create property keys that won’t collide with any other keys (even those with the same name).

  • Usage: Symbols are often used for adding metadata to objects or creating private properties that won’t interfere with other code or libraries.


    jsCopy codeconst sym = Symbol('description');
    const obj = { [sym]: 'value' };
    console.log(obj[sym]);  // 'value'
    
    

Symbols are often used in JavaScript frameworks or libraries, such as iterators (e.g., Symbol.iterator).


What are events in JavaScript?

Events in JavaScript are actions or occurrences that happen in the system you are programming, which the system tells you about so you can respond to them. They are used to handle user interactions and other activities that occur in a web page, such as clicking a button, hovering over an element, or submitting a form.

Common event types include:

  • Mouse events: click, dblclick, mouseover, mouseout, mousemove
  • Keyboard events: keydown, keypress, keyup
  • Form events: submit, change, focus, blur
  • Window events: load, resize, scroll, unload


What is the difference between call(), apply(), and bind()?

call(), apply(), and bind() are methods available on JavaScript functions that allow you to control the this value and pass arguments to the function.


The call() method calls a function with a given this value and arguments provided individually.

function greet(greeting, punctuation) {
  console.log(greeting + ', ' + this.name + punctuation);
}

const person = { name: 'Alice' };

greet.call(person, 'Hello', '!'); // Hello, Alice!


apply() method calls a function with a given this value and arguments provided as an array (or an array-like object).

función saludar ( saludo, puntuación ) { 
  console.log (saludo + ', ' + this.nombre + puntuación); 
}
 
const persona = { nombre : 'Bob' }; 

saludar.apply (persona, [ ' Hola' , ' .' ] ); // Hola, Bob.


The bind() method creates a new function that, when called, has its this value set to the value provided, with a given sequence of arguments preceding any value provided when the new function is called. Unlike call() and apply(), bind() does not execute the function immediately it returns a new function.

función  saludar ( saludo , puntuación ) { 
  console.log (saludo + ', ' + this.nombre + puntuación ); 
} 

const persona = { nombre : 'Charlie' }; 

const personaSaludo = saludar.bind ( persona, 'Hola' , '?' ); 

personaSaludo (); // Se puede llamar más tarde


What is the event loop?

The event loop is a mechanism that allows JavaScript to perform non-blocking I/O operations despite being single-threaded. It continuously checks the call stack and the task queue, executing functions from the task queue when the call stack is empty.


In the event loop, what is executed first, a promise or a setTimeout?

Promises are executed before setTimeout. When both a promise and a setTimeout are scheduled to run at the same time, the .then() callback of the promise will execute first because promises are part of the microtask queue, which has higher priority than the macrotask queue where setTimeout resides.


Why is JavaScript single-threaded?

JavaScript is single-threaded to simplify the execution model and avoid concurrency issues like race conditions. It uses an event loop to manage asynchronous operations, allowing it to perform non-blocking I/O operations despite being single-threaded.


Explain the DOMContentLoaded event.

The DOMContentLoaded event fires when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading. This event is useful for executing JavaScript code as soon as the DOM is ready.


Can you explain memoization?

Memoization is an optimization technique that stores the results of expensive function calls and returns the cached result when the same inputs are provided again. It helps improve the performance of functions by avoiding redundant calculations.


What are JavaScript modules, and how do you implement them?

JavaScript modules allow you to break up your code into smaller, reusable chunks, which can be imported and exported between files. Modules help maintain modularity and encapsulation, making code more organized and manageable.

  • Exporting a Module: You can export functions, objects, or variables from one file.

    jsCopy codeexport const greet = () => console.log('Hello, World!');
    
    
  • Importing a Module: You can import the exported module into another file.

    Example (main.js):

    jsCopy codeimport { greet } from './module.js';
    greet();  // Output: 'Hello, World!'
    
    

Modern JavaScript (ES6+) uses import and export for module implementation. You can also use CommonJS (Node.js modules) with require and module.exports.


Can you explain the concept of immutability and how it can be achieved?

Immutability refers to the inability to change an object after it has been created. In JavaScript, immutability can be achieved by using methods that do not modify the original object but instead return a new object with the desired changes. Examples include Object.freeze() for objects and methods like concat(), slice(), and map() for arrays.


Are arrow functions hoisted?

No, arrow functions are not hoisted. They behave like function expressions, meaning they are not available before their declaration


Explain the concept of closures and provide an example

A closure is a feature in JavaScript where an inner function has access to the outer (enclosing) function’s variables, even after the outer function has finished executing. This allows the inner function to “remember” the environment in which it was created.


function outerFunction() {
    let outerVariable = "I am outside!";

    function innerFunction() {
        console.log(outerVariable);
    }

    return innerFunction;
}

const myClosure = outerFunction();
myClosure(); // Output: "I am outside!"


What is event delegation?

Event delegation is a powerful pattern in web development that allows you to manage events more efficiently by taking advantage of the event bubbling (or propagation) feature in the DOM. Instead of attaching an event listener to each individual element, event delegation involves attaching a single event listener to a parent element. This listener can then detect events triggered by any of its child elements.


How do you implement lazy loading in JavaScript?

  • Intersection Observer API: Best for lazy loading images and other elements.
  • loading Attribute: Simplest way to lazy load images and iframes if supported by the browser.
  • Dynamic Script Loading: Defer or dynamically load scripts for non-essential functionalities.
  • Lazy Loading Background Images: Use Intersection Observer along with CSS for background images.
  • Dynamic Imports: Lazy load JavaScript modules or components when needed.


What are higher-order functions?

A higher-order function is a function that either:

  1. Takes one or more functions as arguments.
  2. Returns a function as its result.

Higher-order functions are commonly used in JavaScript for functional programming techniques like callbacks, map, filter, or reduce.


function higherOrder(fn) {
  return function() {
    return fn();
  };
}

function sayHello() {
  return 'Hello!';
}

const greet = higherOrder(sayHello);
console.log(greet());  // Output: 'Hello!'


What is a prototype in JavaScript, and how does inheritance work?

In JavaScript, every object has a hidden property called [[Prototype]], which refers to another object, known as its prototype. Prototype inheritance allows objects to inherit properties and methods from their prototype chain.

Inheritance works by delegating property or method lookups to the prototype object if they are not found on the instance itself.


function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}`);
};

const person1 = new Person('John');
person1.sayHello();  // Output: 'Hello, my name is John'

In this example, person1 inherits the sayHello method from Person.prototype.


Explain the concept of debouncing and throttling, and how they can be implemented.

  • Debouncing: Ensures a function is called after a specified delay, and if invoked again during that delay, the timer resets. This is useful for scenarios like search inputs, where you want to trigger a search request only after the user has stopped typing for a moment.


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

const debouncedFunction = debounce(() => console.log('Debounced!'), 300);


  • Throttling: Ensures that a function is called at most once in a specified time period, regardless of how many times it is triggered. This is useful for events like scrolling or resizing, where you don’t want the event handler to fire too frequently.


function throttle(func, limit) {
  let lastFunc;
  let lastRan;
  return function(...args) {
    if (!lastRan) {
      func.apply(this, args);
      lastRan = Date.now();
    } else {
      clearTimeout(lastFunc);
      lastFunc = setTimeout(() => {
        if (Date.now() - lastRan >= limit) {
          func.apply(this, args);
          lastRan = Date.now();
        }
      }, limit - (Date.now() - lastRan));
    }
  };
}

const throttledFunction = throttle(() => console.log('Throttled!'), 300);


  • Key Difference: Debouncing delays the function call until a certain period of inactivity, while throttling enforces a rate limit on how often the function is called.


Explain the concept of Dependency Injection. How would you implement it in a JavaScript application?

Dependency Injection (DI) is a design pattern where an object’s dependencies are injected rather than being hard-coded within the object. This promotes flexibility, reusability, and testing.

  • How DI Works: Instead of creating an instance of a dependency directly in a class, you pass it into the class either via constructor or method.
class Logger {
  log(message) {
    console.log(message);
  }
}

class UserService {
  constructor(logger) {
    this.logger = logger;
  }

  createUser(user) {
    this.logger.log(`User created: ${user.name}`);
  }
}

// Inject the dependency (logger) into UserService
const logger = new Logger();
const userService = new UserService(logger);

userService.createUser({ name: 'John' });

In this example, the UserService class depends on the Logger class, but instead of creating a Logger object inside UserService, it's passed in.


How would you implement real-time data streaming in a web application?

Dependency Injection (DI) is a design pattern where an object’s dependencies are injected rather than being hard-coded within the object. This promotes flexibility, reusability, and testing.

  • How DI Works: Instead of creating an instance of a dependency directly in a class, you pass it into the class either via constructor or method.
class Logger {
  log(message) {
    console.log(message);
  }
}

class UserService {
  constructor(logger) {
    this.logger = logger;
  }

  createUser(user) {
    this.logger.log(`User created: ${user.name}`);
  }
}

// Inject the dependency (logger) into UserService
const logger = new Logger();
const userService = new UserService(logger);

userService.createUser({ name: 'John' });

In this example, the UserService class depends on the Logger class, but instead of creating a Logger object inside UserService, it's passed in.


Explain how the ReadableStream and WritableStream APIs work in JavaScript.

  • ReadableStream: Represents a source of data that can be read in small chunks (streams). It allows you to handle data asynchronously as it arrives, without waiting for the entire data set to be available.


    const stream = new ReadableStream({
      start(controller) {
        controller.enqueue('Hello, ');
        controller.enqueue('World!');
        controller.close();
      }
    });
    
    const reader = stream.getReader();
    reader.read().then(({ value, done }) => console.log(value));  // Output: 'Hello, '
    
    


  • WritableStream: Represents a destination where data can be written, chunk by chunk.


    const writableStream = new WritableStream({
      write(chunk) {
        console.log('Writing:', chunk);
      }
    });
    
    const writer = writableStream.getWriter();
    writer.write('Streaming data');
    
    

These APIs are commonly used in handling files, network responses, or large data processing in small chunks.


Can you explain the Observer pattern and provide a use case where it would be beneficial in a JavaScript application?

The Observer Pattern is a behavioral design pattern where an object (called the subject) maintains a list of dependents (observers) and notifies them of any state changes.

  • How it works: Observers subscribe to the subject. When the subject changes state, it broadcasts updates to all its observers.

Use Case: The Observer Pattern is useful in event-driven architectures, like user interface updates or real-time data feeds.


class Subject {
  constructor() {
    this.observers = [];
  }

  subscribe(observer) {
    this.observers.push(observer);
  }

  unsubscribe(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  update(data) {
    console.log('Observer received:', data);
  }
}

// Usage
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

subject.subscribe(observer1);
subject.subscribe(observer2);

subject.notify('New data available!');  // Both observers will be notified


In a real-world scenario, you could use the Observer Pattern for notifications in a chat app or real-time updates in a collaborative document editing tool.


Can you explain the Factory pattern and provide a use case where it would be beneficial in a JavaScript application?

The Factory Pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. Instead of directly instantiating objects, you delegate this responsibility to a separate factory function or class, which decides what specific instance to create.


Use Case: The Factory Pattern is useful when you have multiple subclasses of an object or need dynamic object creation based on certain conditions.


class Car {
  constructor(model) {
    this.model = model;
  }
}

class Bike {
  constructor(model) {
    this.model = model;
  }
}

class VehicleFactory {
  static createVehicle(type, model) {
    if (type === 'car') {
      return new Car(model);
    } else if (type === 'bike') {
      return new Bike(model);
    }
  }
}

// Usage
const car = VehicleFactory.createVehicle('car', 'Sedan');
const bike = VehicleFactory.createVehicle('bike', 'Mountain Bike');

console.log(car instanceof Car);  // true
console.log(bike instanceof Bike);  // true


Benefit: The Factory Pattern is beneficial in scenarios where the exact type of object needs to be determined dynamically at runtime, such as in a vehicle or game entity creation system.


Can you explain the Strategy pattern and provide a use case where it would be beneficial in a JavaScript application?

The Strategy Pattern is a behavioral design pattern that defines a family of algorithms and makes them interchangeable. The strategy pattern lets the algorithm vary independently from the clients that use it. It is often used when you have multiple ways to achieve a task, and you want to switch between them at runtime.


Use Case: The Strategy Pattern is useful in scenarios where you want to swap between different business rules, such as different payment methods or sorting algorithms.


class PayPal {
  pay(amount) {
    console.log(`Paid ${amount} using PayPal`);
  }
}

class CreditCard {
  pay(amount) {
    console.log(`Paid ${amount} using Credit Card`);
  }
}

class PaymentContext {
  constructor(strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  executeStrategy(amount) {
    this.strategy.pay(amount);
  }
}

// Usage
const payment = new PaymentContext(new PayPal());
payment.executeStrategy(100);  // Output: Paid 100 using PayPal

payment.setStrategy(new CreditCard());
payment.executeStrategy(200);  // Output: Paid 200 using Credit Card


Benefit: The Strategy Pattern is beneficial when you need to switch between multiple algorithms or behaviors at runtime, making it great for payment processing or data sorting.

How would you test asynchronous code in JavaScript, especially promises and async/await functions?

Testing asynchronous code in JavaScript can be done using Jest or similar testing frameworks. For promises and async/await, you can use the following approaches:


  • Using done(): Call the done callback for manual completion of async tests.

  • Using .resolves and .rejects: For testing promises that resolve or reject.

  • Using async/await: Jest allows you to write tests in an async function directly.


Example of Promise Test (Using async/await):

jsCopy code// Function to test
const fetchData = () => {
  return new Promise(resolve => setTimeout(() => resolve('data'), 100));
};

// Jest Test
test('fetches data asynchronously', async () => {
  const data = await fetchData();
  expect(data).toBe('data');
});


Example of Promise Test (Using .resolves):

jsCopy codetest('fetches data resolves', () => {
  return expect(fetchData()).resolves.toBe('data');
});


Testing Error Handling in Promises:

jsCopy codeconst fetchWithError = () => Promise.reject('Error occurred');

test('fetch fails with an error', () => {
  return expect(fetchWithError()).rejects.toBe('Error occurred');
});


Benefits: Jest automatically handles async code, and these techniques ensure that tests handle promises and async/await cleanly, providing flexibility for testing asynchronous operations, including API calls and timers.


How does snapshot testing work in Jest, and what are the advantages and limitations of using it?

Snapshot Testing in Jest is a way to ensure that a component's output (usually its rendered UI) matches a previously saved snapshot of the component. If the output differs, Jest will flag the test as failed, which may indicate that the UI has changed unintentionally.


How Snapshot Testing Works:

  1. When you run a snapshot test for the first time, Jest saves the component's output in a snapshot file.
  2. On subsequent runs, Jest compares the new output with the saved snapshot.
  3. If the output has changed, Jest will either fail the test or allow you to update the snapshot.


import React from 'react';
import renderer from 'react-test-renderer';
import MyComponent from './MyComponent';

test('renders correctly', () => {
  const tree = renderer.create(<MyComponent />).toJSON();
  expect(tree).toMatchSnapshot();
});
  • Advantages:

    1. Fast and Easy: Snapshot tests are quick to write and execute, making them ideal for testing UI components.
    2. Catch Unintended Changes: You can catch unintentional UI changes, especially useful in a large, complex application.
    3. Documenting: Snapshots act as documentation, showing the expected output of a component.


  • Limitations:

    1. Overuse: If overused, snapshot tests can be brittle and may fail frequently due to small, intentional changes in the UI.

    2. Lack of Precision: Snapshots are more of a "broad" test, verifying the entire component output. They don't provide much insight into specific behaviors or changes, making them less helpful for more granular logic testing.

    3. Manual Updates: Developers need to manually update snapshots when legitimate changes are made, which can sometimes lead to updating snapshots without realizing an unintended change has occurred.


Snapshot testing is best for UI regression testing but should be combined with other testing methods (unit, integration) for full test coverage.


Explain how you would test components that make use of the Context API or Redux for global state management.

1. Testing Components with Context API

When testing React components that rely on the Context API for global state, you need to simulate the context value by wrapping the component in its corresponding Provider during testing.


import React from 'react';
import { render, screen } from '@testing-library/react';
import UserContext from './UserContext'; // Your context
import UserComponent from './UserComponent'; // Component that uses the context

test('renders user name from context', () => {
  const mockUser = { name: 'John Doe' };
  
  // Render the component wrapped in the context provider
  render(
    <UserContext.Provider value={mockUser}>
      <UserComponent />
    </UserContext.Provider>
  );

  // Assert that the component correctly displays the context value
  expect(screen.getByText(/John Doe/i)).toBeInTheDocument();
});
  • Explanation:
    • We wrap the component inside the UserContext.Provider and pass a mock context value (mockUser).
    • The test checks if the component correctly renders the context value (in this case, the user’s name).

2. Testing Components with Redux

For Redux, testing involves wrapping the component with the Redux Provider and passing a mock store to it. This allows you to simulate Redux state and actions during tests.


import React from 'react';
import { render, screen } from '@testing-library/react';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import MyComponent from './MyComponent'; // Component connected to Redux

const mockStore = configureStore([]);

test('renders value from Redux state', () => {
  const store = mockStore({
    myState: { value: 'Hello World' }, // Mock Redux state
  });

  // Render component wrapped in the Redux provider
  render(
    <Provider store={store}>
      <MyComponent />
    </Provider>
  );

  // Assert that the component correctly renders data from the mock state
  expect(screen.getByText(/Hello World/i)).toBeInTheDocument();
});


  • Explanation:
    • A mock store is created using redux-mock-store.
    • The Provider component wraps the component and provides the mock store.
    • The test ensures that the component renders the correct data from the Redux state.

Key Considerations:

  • Mocking Context or Redux State: You can create mock values or states that simulate actual global state in your tests, ensuring components behave as expected.
  • Testing Actions: In Redux, besides testing state rendering, you can mock and assert if actions (such as dispatching actions) are called correctly by using mock functions.
  • Use of Libraries: Libraries like React Testing Library are recommended as they provide functions like render and screen, making it easy to interact with and test component outputs.


Implement a custom observer pattern in JavaScript

  1. Subject: The subject is responsible for keeping track of the observers and notifying them when a change happens.
  2. Observer: Observers subscribe to the subject, and when the subject changes, the observers are notified.
// Subject Class
class Subject {
  constructor() {
    this.observers = []; // List to store observers (subscribers)
  }

  // Subscribe an observer
  subscribe(observer) {
    this.observers.push(observer);
  }

  // Unsubscribe an observer
  unsubscribe(observer) {
    this.observers = this.observers.filter(sub => sub !== observer);
  }

  // Notify all observers
  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

// Observer Class
class Observer {
  constructor(name) {
    this.name = name;
  }

  // Update method called by the subject when changes occur
  update(data) {
    console.log(`${this.name} received update:`, data);
  }
}

// Example usage:

// Create a subject (e.g., weather station)
const weatherStation = new Subject();

// Create observers (e.g., weather displays)
const display1 = new Observer('Display 1');
const display2 = new Observer('Display 2');

// Subscribe displays to the weather station
weatherStation.subscribe(display1);
weatherStation.subscribe(display2);

// Notify observers with data (e.g., temperature update)
weatherStation.notify('Temperature is 25°C');

// Unsubscribe one observer
weatherStation.unsubscribe(display1);

// Notify again
weatherStation.notify('Temperature is 28°C');


Explanation:

  1. Subject Class:
    • It maintains a list of observers and has methods to add (subscribe), remove (unsubscribe), and notify (notify) observers.
    • When the subject’s state changes, it calls notify() to inform all observers.
  2. Observer Class:
    • It represents the subscribers who are interested in being updated when the subject changes.
    • The update() method is called when the subject notifies them, and it can handle the received data.

Example Output:

Display 1 received update: Temperature is 25°C
Display 2 received update: Temperature is 25°C
Display 2 received update: Temperature is 28°C

Use Case:

  • This pattern is useful in scenarios like event-driven systems, for example, real-time data updates (e.g., a weather station notifying multiple displays), or model-view synchronization in MVC architectures.


Flatten a Nested Array

function flattenArray(arr) {
  for (let i = 0; i < arr.length; i++) {
      const value = arr[i];
      if (Array.isArray(value)) {
        // If the value is an array, recursively call the function flatten
        result = result.concat(flatten(value));
      } else {
        // If it is not an array, simply add the value to the result
        result.push(value);
      }
  }

  return result;
};

function flattenArray(arr) {
    return arr.reduce((flat, toFlatten) => {
        return flat.concat(Array.isArray(toFlatten) ? flattenArray(toFlatten) : toFlatten);
    }, []);
}


const arr = [1, [2, [3, [4]], 5]];
const flattened = flattenArray(arr);
console.log(flattened); // [1, 2, 3, 4, 5]

// using flat()
const arr1 = [1, [2, [3, [4]], 5]];
const flatArr1 = arr1.flat(Infinity);
console.log(flatArr1); // [1, 2, 3, 4, 5]



function  memoize ( fn ) {
     const  cache = {};
     return  function ( ...args ) {
         const  key = JSON.stringify ( args);
         if (cache[key]) {
             return cache[key];
        } else {
             const  result = fn ( ...args ) ;
            cache[key] = result;
             return result;
        }
    };
}




Advanced

What is the Temporal Dead Zone, and how does it relate to variable declarations in JavaScript?

  • The Temporal Dead Zone (TDZ) is the period between the start of a scope (e.g., function or block scope) and when the variable is declared. During this time, variables declared with let and const cannot be accessed. If you try to access them before they are initialized, a ReferenceError will occur. The TDZ ensures that let and const declarations are block-scoped and prevents hoisting issues.


Explain what memoization is and how it can improve performance in JavaScript.

Memoization is a programming technique used to improve the performance of functions by caching the results of expensive function calls and returning the cached result when the same inputs occur again.

  • Reduces Duplicate Calculations: By storing the results of function calls, subsequent calls with the same arguments avoid recomputation, reducing redundant processing.

  • Useful in Recursion: Memoization is particularly helpful in recursive algorithms (like Fibonacci, or dynamic programming problems) where the same sub-problems are solved multiple times.


What is a Proxy object in JavaScript and how can you use it?

A Proxy object in JavaScript allows you to intercept and customize operations performed on an object, such as property access, assignment, function invocation, etc.

const target = {
  name: 'John',
  age: 25
};

const handler = {
  get: (obj, prop) => {
    return prop in obj ? obj[prop] : `Property "${prop}" does not exist`;
  },
  set: (obj, prop, value) => {
    if (prop === 'age' && typeof value !== 'number') {
      throw new TypeError('Age must be a number');
    }
    obj[prop] = value;
    return true;
  }
};

const proxy = new Proxy(target, handler);

// Intercepted property access
console.log(proxy.name);  // John
console.log(proxy.height);  // Property "height" does not exist

// Intercepted property assignment
proxy.age = 30;
console.log(proxy.age);  // 30

// Error when setting invalid type
proxy.age = 'thirty';  // Throws TypeError


Use Cases:

  • Validation: Ensuring certain properties are of the correct type or format.

  • Data Binding: Implement reactive frameworks or state management systems.

  • Object Access Control: Restrict or log access to certain object properties.




What are the performance implications of using large numbers of event listeners in JavaScript?

Using a large number of event listeners can have a negative impact on the performance of a web application, especially when attached to many elements or when handling complex events.

Performance Implications:

  • Increased Memory Usage: Every event listener takes up memory, so having many of them can lead to increased memory consumption.
  • Slower DOM Manipulation: If event listeners are attached to a large number of DOM elements, any DOM updates (such as adding or removing elements) can become slower due to the overhead of adding/removing the listeners.
  • More Processing Overhead: Each time an event is triggered, the browser has to check all registered listeners for that event type. This can cause performance bottlenecks, especially if many listeners are doing heavy processing.
  • Event Delegation: To mitigate performance issues, event delegation is a technique where a single event listener is attached to a parent element, and events from child elements "bubble up" to that parent listener, reducing the number of listeners needed.

Example of Event Delegation:

// Instead of attaching listeners to each button, we use one listener on the parent
document.getElementById('parent').addEventListener('click', (event) => {
  if (event.target.tagName === 'BUTTON') {
    console.log('Button clicked:', event.target.textContent);
  }
});


How do WebAssembly and JavaScript interact, and why might you use them together?

WebAssembly (Wasm) is a low-level binary instruction format designed to run in web browsers alongside JavaScript. It allows developers to write code in languages like C, C++, or Rust and compile it to WebAssembly for use on the web.

Interaction Between WebAssembly and JavaScript:

  • JavaScript Calls WebAssembly: WebAssembly can be imported and invoked from JavaScript. For example, performance-critical operations (like image processing or cryptography) can be written in WebAssembly for speed and called from JavaScript.
  • WebAssembly Calls JavaScript: WebAssembly modules can also interact with JavaScript functions and use JavaScript libraries (e.g., for DOM manipulation).
  • Shared Memory: WebAssembly and JavaScript can share memory through ArrayBuffer objects, allowing efficient data exchange between the two.

Why Use WebAssembly with JavaScript:

  • Performance: WebAssembly is faster than JavaScript for computation-heavy tasks, as it is optimized for execution speed. It runs closer to the hardware and can handle tasks that would be slow in JavaScript.

  • Portability: Code written in other languages can be reused in web applications without rewriting it in JavaScript.

  • Complements JavaScript: WebAssembly is not meant to replace JavaScript but to complement it for use cases like video editing, game engines, and data visualization, where performance is critical.


Explain the concept of event bubbling and event capturing.

When an event is triggered on a DOM element, there are two main phases through which the event travels: event capturing and event bubbling.

Event Bubbling:

  • Definition: In event bubbling, the event starts from the target element where it was triggered and propagates upwards through the DOM hierarchy, triggering any parent element’s event listeners along the way.
  • Example: If you click on a button inside a <div>, the event will first trigger the button's click handler, then the <div>’s click handler, and continue to the root.

Event Capturing (also known as Event Trickling):

  • Definition: In event capturing, the event starts from the root of the DOM and propagates down to the target element before triggering the event handlers attached to the target.
  • Example: During the capture phase, any event listeners set to capture mode ({ capture: true }) will trigger first, starting from the outermost element and moving inward.

Event Propagation Flow:

  1. Capture Phase: The event travels from the root to the target element.
  2. Target Phase: The event is processed at the target element.
  3. Bubbling Phase: The event bubbles up from the target back to the root, triggering parent listeners.

Use Case Example:

<div id="parent">
  <button id="child">Click Me</button>
</div>

<script>
  document.getElementById('parent').addEventListener('click', () => {
    console.log('Parent clicked');
  });

  document.getElementById('child').addEventListener('click', () => {
    console.log('Child clicked');
  });
</script>
  • Output: Clicking the button will print:
    • Child clicked
    • Parent clicked (because of event bubbling)

Capturing Example:

document.getElementById('parent').addEventListener('click', () => {
  console.log('Parent clicked');
}, true);  // Capture mode

document.getElementById('child').addEventListener('click', () => {
  console.log('Child clicked');
});
  • Output: Clicking the button will now print:
    • Parent clicked (first, due to capturing)
    • Child clicked (second)

Preventing Bubbling:

You can prevent an event from propagating up the DOM by using event.stopPropagation().

document.getElementById('child').addEventListener('click', (event) => {
  event.stopPropagation();  // Stops the event from bubbling up
  console.log('Child clicked');
});


How does JavaScript handle memory management and garbage collection?

JavaScript uses automatic memory management and relies on a process called garbage collection to reclaim memory that is no longer in use.

Key Concepts:

  • Memory Allocation: When variables and objects are created, memory is allocated for them. This can happen at runtime when a new object is instantiated or when a variable is declared.
  • Reference Counting: JavaScript uses reference counting to keep track of how many references point to an object. If an object has no references, it is eligible for garbage collection.
  • Garbage Collection: The JavaScript engine automatically identifies and frees memory occupied by objects that are no longer accessible or referenced in the program.
    • Mark-and-Sweep Algorithm: The most common garbage collection algorithm. It involves two phases:
      • Mark Phase: The garbage collector marks all reachable objects starting from a set of root objects (global variables, function scopes, etc.).
      • Sweep Phase: It then sweeps through the memory, freeing memory from all unmarked (unreachable) objects.
  • Memory Leaks: These occur when memory that is no longer needed is not released. Common causes include:
    • Global variables

    • Forgotten timers or callbacks

    • Closures holding onto large objects


What are generators and how do you use them?

Generators are a special type of function in JavaScript that can be paused and resumed, allowing for the creation of iterators in a more straightforward manner.


Key Features:

  • Function Declaration: Generators are defined using the function* syntax and yield values using the yield keyword.

  • Statefulness: Generators maintain their state between calls, allowing for complex iteration patterns.


  • Defining a Generator:

function* myGenerator() {
  yield 1;
  yield 2;
  yield 3;
}
  1. Creating an Iterator:
const iterator = myGenerator();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
  1. Use Case: Generators are useful for implementing iterators, managing asynchronous code with async/await, and creating sequences.


What are WeakMap and WeakSet? How are they different from Map and Set?

WeakMap and WeakSet are collections in JavaScript that hold "weak" references to objects, meaning that they do not prevent garbage collection if there are no other references to the objects.


WeakMap:

  • Structure: A WeakMap is a collection of key-value pairs where the keys are objects and the values can be any value.
  • Garbage Collection: If there are no other references to a key object, it can be garbage collected, even if it is still in the WeakMap.
  • No Enumeration: WeakMap does not provide methods to iterate over its elements, and you cannot obtain the size of the map.


let obj = {};
let weakMap = new WeakMap();
weakMap.set(obj, 'value');
console.log(weakMap.get(obj)); // 'value'
obj = null; // The object can be garbage collected

WeakSet:

  • Structure: A WeakSet is a collection of unique objects. You can add or check for the presence of objects, but the objects are held weakly.
  • Garbage Collection: Like WeakMap, if there are no other references to an object in a WeakSet, it can be garbage collected.
  • No Enumeration: WeakSet does not allow iteration or size checking.


let obj = {};
let weakSet = new WeakSet();
weakSet.add(obj);
console.log(weakSet.has(obj)); // true
obj = null; // The object can be garbage collected


Key Differences:

  • References: Both WeakMap and WeakSet allow for garbage collection of their keys/values if there are no other references, whereas standard Map and Set do not.
  • Methods: Map and Set provide methods for iteration, size checking, and other utilities, while WeakMap and WeakSet do not support these features.


What is the difference between the stack and the heap in JavaScript memory management?

  • Stack: The stack is a place where primitive data types (e.g., numbers, booleans) and function calls are stored. It follows the LIFO (Last In, First Out) principle. Memory in the stack is limited and gets allocated and deallocated quickly.

  • Heap: The heap is where objects and reference types are stored. It’s an unordered memory pool used for dynamically allocated memory. The heap can store large amounts of data, but it requires garbage collection to free up unused memory.


What are service workers, and how can they be used in JavaScript?

Service Workers are scripts that run in the background in the browser, independent of the web page, enabling features like offline support, push notifications, and background sync.


Key Features:

  • Caching: Service workers can intercept network requests and cache assets, allowing for offline access to a website. They enable the creation of Progressive Web Apps (PWAs).

  • Background Tasks: Service workers can run tasks in the background, like syncing data or sending push notifications.

  • Non-blocking: They run on a separate thread from the main JavaScript thread, so they don't block the UI or user interactions.


Usage:

  1. Registering a Service Worker:
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then((registration) => {
    console.log('Service Worker registered:', registration);
  }).catch((error) => {
    console.log('Registration failed:', error);
  });
}
  1. In sw.js (Service Worker File):
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('static-cache').then((cache) => {
      return cache.addAll(['/index.html', '/styles.css', '/app.js']);
    })
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    })
  );
});
  1. Use Case: Service workers are commonly used to improve the performance and reliability of web apps by allowing them to work offline and load faster using cached content.


What is the Decorator pattern, and how can it be implemented in JavaScript to extend object behavior?

The Decorator pattern is a structural design pattern that allows behavior to be added to individual objects, dynamically, without affecting the behavior of other objects from the same class. It provides a flexible alternative to subclassing for extending functionality.


  1. Basic Object:

    class Car {
      getDescription() {
        return "Basic car";
      }
      cost() {
        return 10000;
      }
    }
    
    


  2. Decorator Class:

    class CarDecorator {
      constructor(car) {
        this.car = car;
      }
      getDescription() {
        return this.car.getDescription();
      }
      cost() {
        return this.car.cost();
      }
    }
    
    class SportsPackage extends CarDecorator {
      getDescription() {
        return this.car.getDescription() + ', Sports Package';
      }
      cost() {
        return this.car.cost() + 5000;
      }
    }
    
    class Sunroof extends CarDecorator {
      getDescription() {
        return this.car.getDescription() + ', Sunroof';
      }
      cost() {
        return this.car.cost() + 1500;
      }
    }
    
    


  3. Using the Decorators:

    let myCar = new Car();
    myCar = new SportsPackage(myCar);
    myCar = new Sunroof(myCar);
    
    console.log(myCar.getDescription()); // "Basic car, Sports Package, Sunroof"
    console.log(myCar.cost()); // 16500
    
    


  4. Use Case: This pattern is beneficial when you need to add responsibilities to objects dynamically. For example, in a UI framework, decorators can be used to add extra functionality (like adding borders or shadows) to components without altering their base implementation.


Explain how JavaScript can be used for real-time data streaming and which APIs or techniques are used.

  • JavaScript can handle real-time data streaming using technologies like WebSockets, Server-Sent Events (SSE), or Streams API:
    • WebSockets: Allows for bidirectional communication between a client and server. It’s useful for chat applications, live notifications, or online gaming.

    • Server-Sent Events (SSE): Enables a server to push updates to the client, suitable for one-way data streams like live scores or news updates.

    • Streams API: Works with ReadableStream and WritableStream objects to process data chunks on the fly. This is useful for handling large datasets or real-time media (audio/video) streaming.


What is test-driven development (TDD), and how would you apply it to a JavaScript project?

Test-Driven Development (TDD) is a software development approach where tests are written before the actual code. The development process follows a Red-Green-Refactor cycle:

  1. Red: Write a failing test (because the feature does not exist yet).
  2. Green: Write just enough code to make the test pass.
  3. Refactor: Improve the code quality without changing its behavior, ensuring the test still passes.

TDD Process:

  1. Write a Test:

    • Write a test case that defines a specific functionality.
    // sum.test.js
    const sum = require('./sum');
    
    test('adds 2 + 3 to equal 5', () => {
      expect(sum(2, 3)).toBe(5);  // Test will fail because sum function doesn't exist yet.
    });
    
    


  2. Write the Minimum Code:

    • Implement the function that satisfies the test.
    // sum.js
    function sum(a, b) {
      return a + b;
    }
    module.exports = sum;
    
    


  3. Refactor:

    • After the test passes, clean up or optimize the code while ensuring it still meets the test requirements.


  4. Repeat the Cycle:

    • Write another test, make it pass, refactor, and continue.

Benefits:

  • Code Reliability: Ensures that every piece of functionality is tested.
  • Fewer Bugs: Catch errors early during development.
  • Cleaner Code: Encourages writing modular, maintainable code.

Example Use Case:

  • TDD is particularly useful in large projects where changes in code could potentially break other parts of the system. In JavaScript, it's used for both frontend (e.g., testing components in React) and backend (e.g., Node.js APIs) development.


Write a function that performs a deep clone of a given object.

function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  
  if (Array.isArray(obj)) {
    return obj.map(item => deepClone(item));
  }
  
  const clone = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key]);
    }
  }
  
  return clone;
}

// Example:
const obj = { a: 1, b: { c: 2, d: [3, 4] }};
const clonedObj = deepClone(obj);
console.log(clonedObj);


structuredClone() is a built-in function in modern JavaScript (introduced in ECMAScript 2021) that creates a deep clone of a given value. It supports cloning of many built-in types, including objects, arrays, maps, sets, dates, and more, without many of the limitations.


const original = {
  a: 1,
  b: {
    c: 2,
    d: [3, 4, 5]
  },
  e: new Date(),
  f: new Map([['key', 'value']])
};

const deepCopy = structuredClone(original);

console.log(deepCopy); // { a: 1, b: { c: 2, d: [3, 4, 5] }, e: Date, f: Map }
console.log(deepCopy !== original); // true
console.log(deepCopy.b !== original.b); // true
console.log(deepCopy.b.d !== original.b.d); // true
console.log(deepCopy.e !== original.e); // true
console.log(deepCopy.f !== original.f); // true

// Modifying the copy does not affect the original
deepCopy.b.c = 10;
deepCopy.b.d.push(6);
deepCopy.e.setFullYear(2000);
deepCopy.f.set('key', 'newValue');

console.log(original.b.c); // 2
console.log(original.b.d); // [3, 4, 5]
console.log(original.e.getFullYear()); // Current year
console.log(original.f.get('key')); // "value"

Write a function to compares objects.

  • Shallow Comparison

Shallow comparison involves comparing the objects’ own properties and values but not nested objects.


function shallowEqual(obj1, obj2) {
  if (obj1 === obj2) return true;
  
  if (typeof obj1 !== 'object' || obj1 === null || 
      typeof obj2 !== 'object' || obj2 === null) {
    return false;
  }
  
  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);
  
  if (keys1.length !== keys2.length) return false;
  
  for (let key of keys1) {
    if (obj1[key] !== obj2[key]) {
      return false;
    }
  }
  
  return true;
}
// Example usage:
const obj1 = { a: 1, b: 2 };
const obj2 = { a: 1, b: 2 };
const obj3 = { a: 1, b: 3 };
console.log(shallowEqual(obj1, obj2)); // true
console.log(shallowEqual(obj1, obj3)); // false


  • Deep Comparison

Deep comparison involves recursively comparing all nested objects and arrays.


function deepEqual(obj1, obj2) {
  if (obj1 === obj2) return true;
  
  if (typeof obj1 !== 'object' || obj1 === null || 
      typeof obj2 !== 'object' || obj2 === null) {
    return false;
  }
  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);
  
  if (keys1.length !== keys2.length) return false;
  
  for (let key of keys1) {
    if (!keys2.includes(key)) return false;
    if (!deepEqual(obj1[key], obj2[key])) return false;
  }
  return true;
}
// Example usage:
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
const obj3 = { a: 1, b: { c: 3 } };
console.log(deepEqual(obj1, obj2)); // true
console.log(deepEqual(obj1, obj3)); // false

Implement a memoize function,

function memoize(fn) {
  const cache = {};
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache[key]) {
      return cache[key];
    }
    const result = fn(...args);
    cache[key] = result;
    return result;
  };
}

function slowFunction(num) {
  console.log("Computing...");
  return num * 2;
}

const memoizedFn = memoize(slowFunction);
console.log(memoizedFn(5)); // Output: "Computing..." 10
console.log(memoizedFn(5)); // Output: 10 (cached result)


Implement the Observer pattern (publish/subscribe model) in JavaScript.

class Subject {
  constructor() {
    this.observers = [];
  }

  subscribe(observer) {
    this.observers.push(observer);
  }

  unsubscribe(observer) {
    this.observers = this.observers.filter(sub => sub !== observer);
  }

  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  update(data) {
    console.log('Observer received:', data);
  }
}

const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

subject.subscribe(observer1);
subject.subscribe(observer2);

subject.notify('Event 1');
subject.unsubscribe(observer2);
subject.notify('Event 2');


Implement a custom version of Promise.all() that resolves when all promises have resolved or rejects if any promise rejects.

function customPromiseAll(promises) {
  return new Promise((resolve, reject) => {
    const results = [];
    let completed = 0;

    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then(result => {
          results[index] = result;
          completed++;

          if (completed === promises.length) {
            resolve(results);
          }
        })
        .catch(reject);
    });
  });
}

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'foo'));
const promise3 = Promise.resolve(42);

customPromiseAll([promise1, promise2, promise3]).then((results) => {
  console.log(results); // Output: [3, "foo", 42]
});