Interview Questions & Answers
Filter by Topics:
JavaScript - Beginner
JavaScript is a versatile programming language primarily used to add interactivity and dynamic behavior to websites. It runs directly in the browser, allowing developers to manipulate the DOM, handle events, and communicate with servers without reloading the page. Beyond the browser, JavaScript powers backend development through Node.js, enabling full-stack applications. It supports both object-oriented and functional programming styles, making it flexible for different use cases. Today, JavaScript is the backbone of modern web development, used in frameworks like React, Angular, and Vue, and is essential for building responsive, interactive, and scalable applications across platforms.
JavaScript provides three ways to declare variables: var, let, and const. The var keyword is function-scoped and allows redeclaration, which can lead to unexpected behavior due to hoisting. Let is block-scoped, meaning it is only accessible within the block where it is defined, and it prevents redeclaration in the same scope. Const is also block-scoped but is used for values that should not be reassigned after initialization. While const prevents reassignment, it does not make objects immutable. In modern JavaScript, let and const are preferred over var because they provide clearer scoping and reduce bugs.
JavaScript has two categories of data types: primitive and reference. Primitive types include string, number, boolean, null, undefined, symbol, and bigint. These are immutable and represent simple values. Reference types include objects, arrays, and functions, which are mutable and stored by reference. Understanding data types is crucial because JavaScript is loosely typed, meaning variables can change types dynamically. For example, a variable can hold a number and later be reassigned to a string. Type coercion often occurs during comparisons or operations, so developers must be careful to avoid unexpected results when working with different data types.
Hoisting is JavaScript’s default behavior of moving variable and function declarations to the top of their scope during compilation. This means you can call a function before it is defined in the code, and it will still work if declared using function declarations. However, variables declared with var are hoisted but initialized as undefined, while let and const are hoisted but remain in a temporal dead zone until their declaration is reached. This difference often causes confusion for beginners. Understanding hoisting helps developers write cleaner code and avoid bugs related to variable initialization and scope handling.
A closure is a function that retains access to variables from its outer scope even after that scope has finished executing. This happens because functions in JavaScript form lexical environments, preserving references to variables they depend on. Closures are powerful for creating private variables, encapsulating logic, and building factory functions. For example, a counter function can maintain its internal state across multiple calls without exposing the variable directly. They are widely used in event handlers, callbacks, and functional programming patterns. Mastering closures is essential for writing efficient, modular, and secure JavaScript code.
Scope in JavaScript defines the accessibility of variables and functions in different parts of the code. There are three main types: global scope, function scope, and block scope. Variables declared globally are accessible throughout the program, while function-scoped variables are limited to the function they are defined in. Block scope, introduced with let and const, restricts variables to the block where they are declared, such as inside loops or conditionals. Scope ensures that variables are not accidentally overwritten and helps maintain clean, modular code. Understanding scope is critical for debugging and structuring applications effectively.
The double equals (==) operator compares values for equality but performs type coercion if the types differ. For example, '5' == 5 evaluates to true because the string is converted to a number. The triple equals (===) operator, however, checks both value and type strictly, so '5' === 5 evaluates to false. Using === is generally recommended because it avoids unexpected results caused by type coercion. Developers should use == only when intentional type conversion is desired. Understanding the difference between these operators helps prevent subtle bugs and ensures more predictable comparisons in JavaScript.
NaN stands for 'Not a Number' and represents an invalid numeric result in JavaScript. It occurs when mathematical operations fail, such as dividing zero by a string or parsing an invalid number. Interestingly, NaN is of type number, which can be confusing. Another unique property is that NaN is not equal to itself, meaning NaN === NaN returns false. To check for NaN, developers use the isNaN() function or Number.isNaN() for stricter validation. Handling NaN properly is important to avoid unexpected behavior in calculations, data processing, and user input validation.
Arrow functions are a concise syntax for writing functions in JavaScript, introduced in ES6. They use the => operator and are often used for short callbacks or inline functions. Unlike traditional functions, arrow functions do not bind their own this, instead inheriting it from the surrounding scope. This makes them particularly useful in scenarios like event handlers or methods where maintaining context is important. Arrow functions also cannot be used as constructors and do not have their own arguments object. Their simplicity and predictable behavior make them a popular choice in modern JavaScript development.
Null and undefined are both special values in JavaScript but represent different concepts. Undefined means a variable has been declared but not assigned a value, or a function does not return anything explicitly. Null, on the other hand, is an intentional assignment by the developer to indicate the absence of a value. Both are falsy values, but they are not the same: null === undefined evaluates to false. Understanding the distinction helps developers write clearer code and avoid confusion when checking for missing or empty values in applications.
JavaScript - Intermediate
The event loop is the mechanism that allows JavaScript to handle asynchronous operations despite being single-threaded. It continuously checks the call stack and the task queue. When the stack is empty, it pushes queued callbacks into execution. This enables non-blocking behavior, letting JavaScript perform tasks like handling user input, network requests, or timers without freezing the UI. The event loop works with the call stack, Web APIs, and the callback queue to manage concurrency. Understanding the event loop is crucial for writing efficient asynchronous code and avoiding issues like race conditions or blocking operations.
A promise in JavaScript represents the eventual completion or failure of an asynchronous operation. It has three states: pending, fulfilled, and rejected. Promises allow developers to chain asynchronous tasks using .then() for success and .catch() for errors, avoiding deeply nested callbacks. They make code more readable and manageable, especially when dealing with multiple asynchronous operations. Promises can also be combined using Promise.all or Promise.race to handle multiple tasks concurrently. They are foundational to modern asynchronous programming in JavaScript and are often used with async/await for cleaner syntax.
Async/await is syntactic sugar built on top of promises that makes asynchronous code look synchronous. Declaring a function as async ensures it returns a promise, while the await keyword pauses execution until the promise resolves or rejects. This eliminates the need for chaining .then() and improves readability. Error handling is simplified using try/catch blocks. Async/await is especially useful when dealing with multiple asynchronous operations in sequence, such as fetching data from APIs. It makes code easier to write, debug, and maintain while still leveraging the non-blocking nature of JavaScript.
A callback function is a function passed as an argument to another function, executed later when a task completes. Callbacks are commonly used in asynchronous operations like reading files, handling events, or making API requests. While powerful, callbacks can lead to 'callback hell' when nested deeply, making code hard to read and maintain. Promises and async/await were introduced to solve these issues, but callbacks remain fundamental in JavaScript. They are essential for event-driven programming and allow developers to control execution flow by specifying what should happen after a task finishes.
Destructuring is a JavaScript feature that allows unpacking values from arrays or properties from objects into distinct variables. It simplifies code by reducing repetitive access to object properties or array indices. For example, const {name, age} = user extracts values directly from the user object. Destructuring also supports default values, nested structures, and renaming variables. It is widely used in modern JavaScript for cleaner syntax, especially when working with function parameters, API responses, or React props. This feature improves readability and reduces boilerplate code in everyday development.
The spread (...) operator expands elements of an array or object into individual items, useful for copying, merging, or passing arguments. For example, [...arr] creates a shallow copy of an array. The rest (...) operator collects multiple elements into a single array or object, often used in function parameters to handle variable arguments. For example, function sum(...nums) gathers all arguments into an array. Both operators simplify code, reduce boilerplate, and improve flexibility. They are widely used in modern JavaScript for handling collections, immutability, and functional programming patterns.
Prototype inheritance is the mechanism by which JavaScript objects inherit properties and methods from other objects. Every object has an internal prototype reference, and when a property is accessed, JavaScript looks up the prototype chain until it finds it or reaches null. This allows objects to share behavior without duplicating code. Functions in JavaScript have a prototype property that defines methods for instances created with new. Prototype inheritance underpins JavaScript’s object-oriented programming model and is the basis for class syntax introduced in ES6, which provides a cleaner abstraction over prototypes.
Modules in JavaScript are reusable pieces of code that can be imported and exported between files. They help organize code into smaller, maintainable units. ES6 introduced native module syntax using export and import keywords. For example, export function add() allows importing it elsewhere with import { add } from './math.js'. Modules support default exports, named exports, and dynamic imports. They improve code structure, prevent global namespace pollution, and enable better dependency management. Modern frameworks and bundlers rely heavily on modules to build scalable applications.
DOM manipulation refers to interacting with and updating the Document Object Model, which represents the structure of a web page. JavaScript provides methods like getElementById, querySelector, appendChild, and innerHTML to modify elements, attributes, and styles. DOM manipulation enables dynamic updates such as adding new content, handling user input, or changing layouts without reloading the page. While powerful, excessive DOM manipulation can hurt performance, so modern frameworks like React and Vue use virtual DOMs to optimize updates. Understanding DOM manipulation is fundamental for building interactive web applications.
Debouncing and throttling are techniques to control how often a function executes in response to frequent events like scrolling, resizing, or typing. Debouncing delays execution until a certain time has passed without another event, useful for search input fields. Throttling ensures a function runs at most once in a specified interval, useful for scroll or resize events. Both improve performance by reducing unnecessary function calls and preventing UI lag. They are commonly implemented with setTimeout or requestAnimationFrame and are essential for optimizing event-driven applications.
JavaScript - Advanced
Currying is a functional programming technique where a function with multiple arguments is transformed into a sequence of functions, each taking a single argument. For example, a function add(a, b) can be curried into add(a)(b). This allows partial application, meaning you can fix some arguments and reuse the function later. Currying improves code reusability, modularity, and readability, especially in functional programming patterns. It is widely used in JavaScript when working with higher-order functions, event handlers, or composing complex logic. Currying helps developers write cleaner, more predictable code by breaking down functions into smaller, reusable units.
Memoization is an optimization technique that stores the results of expensive function calls and returns the cached result when the same inputs occur again. It reduces redundant computations and improves performance, especially in recursive functions like calculating Fibonacci numbers. In JavaScript, memoization is often implemented using closures or libraries like lodash.memoize. It is particularly useful in applications with heavy computations, repeated API calls, or rendering logic in frameworks like React. By caching results, memoization ensures faster execution and better resource utilization, making it a key strategy for performance optimization in modern applications.
Functional programming is a paradigm that treats computation as the evaluation of mathematical functions and avoids changing state or mutable data. In JavaScript, it emphasizes pure functions, immutability, and higher-order functions. Techniques like map, filter, and reduce are commonly used to process data declaratively. Functional programming improves code readability, testability, and predictability by minimizing side effects. It also encourages composition, where small functions are combined to build complex logic. With ES6 features like arrow functions and spread operators, JavaScript supports functional programming more naturally, making it a popular approach in modern development.
A shallow copy duplicates only the top-level properties of an object, while nested objects or arrays still reference the original memory. Methods like Object.assign or the spread operator create shallow copies. A deep copy, on the other hand, recursively copies all nested structures, ensuring complete independence from the original object. Deep copies can be created using JSON.parse(JSON.stringify(obj)) or libraries like lodash.cloneDeep. The difference is crucial when working with complex data structures, as shallow copies may lead to unintended mutations. Choosing between shallow and deep copy depends on the level of independence required.
Call, apply, and bind are methods used to control the value of this in JavaScript functions. call() invokes a function with a specified this and arguments passed individually. apply() is similar but takes arguments as an array. bind() returns a new function with this permanently set to the provided value, allowing later execution. These methods are useful for borrowing methods from objects, ensuring correct context in event handlers, or working with dynamic function calls. Understanding their differences helps developers manage scope and context effectively in complex applications.
A generator function is a special type of function in JavaScript defined with function* syntax. It can pause execution using the yield keyword and resume later, allowing values to be produced lazily. Generators return an iterator object that can be traversed step by step. They are useful for handling sequences, asynchronous flows, or infinite data structures without computing everything upfront. Generators improve efficiency by producing values on demand and are often combined with async/await for advanced asynchronous patterns. They provide fine-grained control over function execution and state management.
JavaScript handles concurrency using its event loop, asynchronous APIs, and promises rather than traditional multithreading. Tasks are executed on a single thread, but asynchronous operations like I/O, timers, or network requests are delegated to the browser or Node.js environment. Once completed, callbacks or promise resolutions are queued for execution. This non-blocking model allows JavaScript to manage multiple tasks efficiently without freezing the UI. Advanced techniques like Web Workers provide true parallelism for heavy computations. Overall, JavaScript’s concurrency model balances simplicity with responsiveness, making it well-suited for interactive applications.
Microtasks and macrotasks are categories of tasks in JavaScript’s event loop. Macrotasks include events like setTimeout, setInterval, and I/O operations, which are queued for later execution. Microtasks include promise callbacks and MutationObserver tasks, which run immediately after the current execution context, before any macrotasks. This means microtasks have higher priority and can execute sooner. Understanding the difference is important for predicting execution order and avoiding race conditions. Developers use this knowledge to optimize asynchronous code and ensure predictable behavior in complex applications.
A Proxy object in JavaScript allows developers to intercept and customize operations performed on another object, such as property access, assignment, or function invocation. It is created using the Proxy constructor with a target object and a handler defining traps. For example, you can validate property values, log access, or implement dynamic behavior. Proxies are powerful for creating abstractions, debugging, or enforcing rules in applications. They enable meta-programming by giving developers fine-grained control over object behavior, making them a versatile tool in advanced JavaScript development.
JavaScript handles memory management through automatic garbage collection, which frees unused objects. However, memory leaks occur when references prevent garbage collection. Common causes include global variables, forgotten timers, event listeners not removed, or closures holding unnecessary references. To prevent leaks, developers should clean up listeners, clear intervals, and avoid excessive global state. Tools like Chrome DevTools help detect leaks by monitoring heap usage. Efficient coding practices and awareness of object lifecycles are essential to ensure applications remain performant and stable over time.