JavaScript Interview Questions
Introduction
JavaScript is one of the most popular programming languages in the world, powering the interactive elements of millions of websites. As a result, JavaScript knowledge is highly sought after in the job market, and you're likely to face JavaScript-specific questions in technical interviews.
This guide covers the most common JavaScript interview questions you might encounter, along with detailed explanations and examples to help you prepare effectively. Whether you're a beginner looking to land your first programming job or an experienced developer brushing up on your skills, this guide will help you navigate JavaScript interview questions with confidence.
Fundamental JavaScript Interview Questions
1. What is JavaScript and how does it differ from Java?
Answer: JavaScript is a high-level, interpreted programming language primarily used for enhancing web pages with interactive elements. Despite the similar name, JavaScript has little in common with Java:
- JavaScript was created by Brendan Eich at Netscape in 1995, while Java was developed by James Gosling at Sun Microsystems
- JavaScript is primarily a client-side scripting language (though it can now run server-side with Node.js), while Java is a general-purpose programming language
- JavaScript is dynamically typed, while Java is statically typed
- JavaScript is prototype-based for object-oriented programming, while Java is class-based
2. Explain the difference between let
, const
, and var
Answer: These three keywords declare variables in JavaScript, but they have important differences:
var
(traditional variable declaration):
- Function-scoped or globally-scoped, not block-scoped
- Can be redeclared and updated
- Hoisted to the top of its scope and initialized with
undefined
var x = 10;
var x = 20; // Legal redeclaration
if (true) {
var x = 30; // Same variable, not block-scoped
}
console.log(x); // Output: 30
let
(introduced in ES6):
- Block-scoped
- Can be updated but not redeclared in the same scope
- Hoisted but not initialized (results in ReferenceError if accessed before declaration)
let y = 10;
// let y = 20; // Error: Cannot redeclare
y = 20; // Legal update
if (true) {
let y = 30; // Different variable, block-scoped
}
console.log(y); // Output: 20
const
(introduced in ES6):
- Block-scoped
- Cannot be updated or redeclared
- Must be initialized at declaration
- For objects and arrays, the contents can still be modified (only the reference is constant)
const z = 10;
// z = 20; // Error: Assignment to constant variable
// const z = 20; // Error: Cannot redeclare
const obj = { name: "John" };
obj.name = "Jane"; // Legal: object contents can be modified
console.log(obj.name); // Output: "Jane"
3. What is hoisting in JavaScript?
Answer: Hoisting is JavaScript's default behavior of moving declarations to the top of their containing scope during compilation, before code execution. This means you can use variables and functions before they're declared in your code.
console.log(hoistedVar); // Output: undefined
console.log(notHoisted); // ReferenceError: Cannot access before initialization
hoistedFunction(); // Output: "This function is hoisted"
notHoistedFunction(); // TypeError: not a function
var hoistedVar = "I'm hoisted";
let notHoisted = "I'm not hoisted";
function hoistedFunction() {
console.log("This function is hoisted");
}
const notHoistedFunction = function() {
console.log("This function expression is not hoisted");
};
Important notes about hoisting:
- Only declarations are hoisted, not initializations
var
variables are initialized withundefined
when hoistedlet
andconst
are hoisted but not initialized (creating a "temporal dead zone")- Function declarations are fully hoisted
- Function expressions and arrow functions are not hoisted
4. Explain event bubbling and event capturing
Answer: Event bubbling and capturing are two ways of event propagation in the HTML DOM:
Event Bubbling: When an event happens on an element, it first runs the handlers on it, then on its parent, and all the way up the DOM tree.
Event Capturing: The event is first captured by the outermost element and propagated to the inner elements.
// Event Bubbling (default)
document.querySelector("#child").addEventListener("click", function() {
console.log("Child element clicked - bubbling phase");
});
document.querySelector("#parent").addEventListener("click", function() {
console.log("Parent element clicked - bubbling phase");
});
// Event Capturing
document.querySelector("#child").addEventListener("click", function() {
console.log("Child element clicked - capturing phase");
}, true);
document.querySelector("#parent").addEventListener("click", function() {
console.log("Parent element clicked - capturing phase");
}, true);
In the example above, the third parameter of addEventListener
specifies whether to use the capturing phase (true
) or the bubbling phase (false
, which is the default).
Intermediate JavaScript Interview Questions
5. What is closure in JavaScript?
Answer: A closure is a function that has access to its own scope, the outer function's variables, and global variables - even after the outer function has finished executing.
function outerFunction(outerVariable) {
return function innerFunction(innerVariable) {
console.log('outerVariable:', outerVariable);
console.log('innerVariable:', innerVariable);
}
}
const newFunction = outerFunction('outside');
newFunction('inside');
// Output:
// outerVariable: outside
// innerVariable: inside
Closures are useful for:
- Data encapsulation and privacy
- Creating function factories
- Implementing module patterns
6. Explain prototypal inheritance in JavaScript
Answer: JavaScript uses prototypal inheritance, where objects can inherit properties and methods from other objects through their prototype chain.
// Constructor function
function Person(name) {
this.name = name;
}
// Adding a method to the prototype
Person.prototype.greet = function() {
return `Hello, my name is ${this.name}`;
};
// Creating an instance
const john = new Person('John');
console.log(john.greet()); // Output: "Hello, my name is John"
// Inheritance
function Employee(name, position) {
Person.call(this, name); // Call parent constructor
this.position = position;
}
// Set up inheritance chain
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
// Add method to Employee prototype
Employee.prototype.introduce = function() {
return `${this.greet()} and I work as a ${this.position}`;
};
const jane = new Employee('Jane', 'Developer');
console.log(jane.introduce()); // Output: "Hello, my name is Jane and I work as a Developer"
With ES6 classes (syntactic sugar over prototypal inheritance):
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, my name is ${this.name}`;
}
}
class Employee extends Person {
constructor(name, position) {
super(name);
this.position = position;
}
introduce() {
return `${this.greet()} and I work as a ${this.position}`;
}
}
const jane = new Employee('Jane', 'Developer');
console.log(jane.introduce()); // Output: "Hello, my name is Jane and I work as a Developer"
7. What are Promises in JavaScript and how do they work?
Answer: Promises in JavaScript represent operations that haven't completed yet but are expected to in the future. They provide a cleaner way to handle asynchronous operations compared to callbacks.
A Promise can be in one of three states:
- Pending: Initial state, neither fulfilled nor rejected
- Fulfilled: Operation completed successfully
- Rejected: Operation failed
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
// Simulating an asynchronous operation
setTimeout(() => {
const success = true;
if (success) {
resolve('Operation succeeded');
} else {
reject('Operation failed');
}
}, 1000);
});
// Consuming a Promise
myPromise
.then(result => {
console.log(result); // Output: "Operation succeeded"
})
.catch(error => {
console.error(error);
})
.finally(() => {
console.log('Promise settled (fulfilled or rejected)');
});
Promise methods:
Promise.all()
: Waits for all promises to resolvePromise.race()
: Settles as soon as any promise settlesPromise.allSettled()
: Waits for all promises to settle (resolve or reject)Promise.any()
: Resolves if any promise resolves, rejects only if all reject
8. Explain the concept of the event loop in JavaScript
Answer: The event loop is a mechanism that allows JavaScript to perform non-blocking operations despite being single-threaded. It works by continuously checking the call stack and the callback queue:
- When the call stack is empty, the event loop takes the first event from the queue
- Adds it to the call stack
- Executes it until the stack is empty again
console.log('Start'); // 1
setTimeout(() => {
console.log('Timeout callback'); // 4
}, 0);
Promise.resolve().then(() => {
console.log('Promise resolved'); // 3
});
console.log('End'); // 2
// Output:
// Start
// End
// Promise resolved
// Timeout callback
The event loop handles different types of tasks with different priorities:
- Microtasks (Promises, queueMicrotask, MutationObserver)
- Macrotasks (setTimeout, setInterval, I/O, UI rendering)
Microtasks have higher priority and are processed before the next macrotask.
Advanced JavaScript Interview Questions
9. What are the differences between ==
and ===
operators?
Answer:
==
(Equality): Compares values after attempting type conversion===
(Strict Equality): Compares both values and types, without type conversion
console.log(5 == '5'); // true - values equal after type conversion
console.log(5 === '5'); // false - different types
console.log(0 == false); // true - both convert to 0
console.log(0 === false); // false - different types
console.log(null == undefined); // true - they're treated as equal
console.log(null === undefined); // false - different types
Using ===
is generally recommended as it avoids unexpected type coercion issues.
10. Explain the this
keyword in JavaScript
Answer: The this
keyword refers to the object that is executing the current function. Its value depends on how the function is called:
- Function Invocation:
this
refers to the global object (window
in browsers,global
in Node.js) - Method Invocation:
this
refers to the object that owns the method - Constructor Invocation:
this
refers to the newly created object - Explicit Binding: Using
call()
,apply()
, orbind()
to setthis
explicitly
// Regular function (function invocation)
function regularFunction() {
console.log(this); // Window or global object
}
// Object method (method invocation)
const obj = {
name: 'Example',
method() {
console.log(this); // obj
}
};
// Constructor (constructor invocation)
function Person(name) {
this.name = name;
console.log(this); // the new Person object
}
// Explicit binding
function greet() {
console.log(`Hello, ${this.name}`);
}
greet.call({ name: 'Alice' }); // Output: "Hello, Alice"
Arrow functions are special - they don't have their own this
context but inherit it from the enclosing lexical context:
const arrowObj = {
name: 'Arrow Example',
regularMethod: function() {
console.log(`Regular method: ${this.name}`); // "Arrow Example"
const arrowFunction = () => {
console.log(`Arrow function: ${this.name}`); // "Arrow Example"
};
arrowFunction();
}
};
arrowObj.regularMethod();
11. What are Web Workers and how do they work?
Answer: Web Workers provide a way to run JavaScript in background threads, separate from the main thread. They allow for multithreading in JavaScript without blocking the UI.
// main.js
if (window.Worker) {
const myWorker = new Worker('worker.js');
myWorker.postMessage({data: 'Hello from main thread'});
myWorker.onmessage = function(e) {
console.log('Message received from worker:', e.data);
};
} else {
console.log('Web Workers not supported');
}
// worker.js
self.onmessage = function(e) {
console.log('Message received in worker:', e.data);
// Do some CPU-intensive work
const result = performComplexCalculation();
self.postMessage({result: result});
};
Limitations of Web Workers:
- No direct DOM access
- No direct access to the
window
object or parent page functions - Limited communication through
postMessage()
- Separate memory space from the main thread
Types of workers:
- Dedicated Workers (used by a single script)
- Shared Workers (can be shared between multiple scripts)
- Service Workers (act as proxy servers, enabling offline access)
12. Explain async/await in JavaScript
Answer: async/await
is a modern way to handle asynchronous operations in JavaScript, built on top of Promises. It makes asynchronous code look and behave more like synchronous code.
// Using Promises
function fetchDataWithPromises() {
return fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => {
console.log('Data:', data);
return data;
})
.catch(error => {
console.error('Error:', error);
throw error;
});
}
// Using async/await
async function fetchDataWithAsync() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log('Data:', data);
return data;
} catch (error) {
console.error('Error:', error);
throw error;
}
}
// Calling async functions
fetchDataWithAsync().then(data => {
console.log('Processing:', data);
});
Key points about async/await:
- An
async
function always returns a Promise await
can only be used inside anasync
functionawait
pauses the execution of the function until the Promise is resolved- Error handling is done with try/catch blocks, similar to synchronous code
- You can use Promise methods like
.then()
and.catch()
with the result of anasync
function
Practical JavaScript Interview Questions
13. How would you implement debouncing in JavaScript?
Answer: Debouncing is a technique used to limit the rate at which a function can fire. It's especially useful for functions that might be called frequently in a short period of time, like resize or scroll event handlers.
function debounce(func, delay) {
let timeoutId;
return function(...args) {
const context = this;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(context, args);
}, delay);
};
}
// Example usage:
const debouncedResize = debounce(() => {
console.log('Resize event handled');
// Update UI or perform calculations
}, 300);
window.addEventListener('resize', debouncedResize);
In this example, when the resize
event fires repeatedly, the actual handler function will only execute after 300ms of "quiet time" (no new resize events).
14. How would you check if an object is an array in JavaScript?
Answer: There are several ways to check if an object is an array in JavaScript:
const arr = [1, 2, 3];
const obj = { a: 1 };
// Method 1: Array.isArray() (preferred)
console.log(Array.isArray(arr)); // true
console.log(Array.isArray(obj)); // false
// Method 2: instanceof (can fail with multiple frames/windows)
console.log(arr instanceof Array); // true
console.log(obj instanceof Array); // false
// Method 3: Object.prototype.toString
console.log(Object.prototype.toString.call(arr) === '[object Array]'); // true
console.log(Object.prototype.toString.call(obj) === '[object Array]'); // false
// Method 4: constructor property (not reliable)
console.log(arr.constructor === Array); // true
console.log(obj.constructor === Array); // false
Array.isArray()
is the most reliable method and is supported in all modern browsers.
15. What is the output of the following code and why?
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
Answer: The output will be:
5
5
5
5
5
This happens because:
- The loop runs quickly, setting up 5 timeouts
- By the time the timeouts execute after 1 second, the loop has completed and
i
is 5 - All timeouts reference the same
i
variable (function scope withvar
) - Each timeout logs the current value of
i
, which is 5
To get the expected 0,1,2,3,4 output, you could use:
// Solution 1: Using an IIFE to create a new scope
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
// Solution 2: Using let for block scoping
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Solution 3: Using the third parameter of setTimeout
for (var i = 0; i < 5; i++) {
setTimeout(function(j) {
console.log(j);
}, 1000, i);
}
ES6+ and Modern JavaScript Questions
16. Describe some key features introduced in ES6 (ECMAScript 2015)
Answer: ES6 introduced many significant features that modernized JavaScript:
1. let and const declarations
let x = 10; // Block-scoped variable
const PI = 3.14; // Block-scoped constant
2. Arrow functions
// Traditional function
function add(a, b) {
return a + b;
}
// Arrow function
const add = (a, b) => a + b;
3. Template literals
const name = 'World';
console.log(`Hello, ${name}!`); // String interpolation
4. Destructuring assignment
// Array destructuring
const [a, b] = [1, 2];
// Object destructuring
const { name, age } = { name: 'John', age: 30 };
5. Default parameters
function greet(name = 'Guest') {
return `Hello, ${name}!`;
}
6. Rest and spread operators
// Rest parameters
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
// Spread operator
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5]; // [1, 2, 3, 4, 5]
7. Classes
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, I'm ${this.name}`;
}
}
8. Modules
// Exporting
export const PI = 3.14;
export function square(x) {
return x * x;
}
// Importing
import { PI, square } from './math.js';
9. Promises
const fetchData = () => {
return new Promise((resolve, reject) => {
// Asynchronous operation
});
};
10. Map and Set collections
const map = new Map();
map.set('key', 'value');
const set = new Set([1, 2, 3, 1]); // Unique values: 1, 2, 3
17. What are Map and WeakMap in JavaScript?
Answer: Map and WeakMap are collection types introduced in ES6 that allow storing key-value pairs.
Map:
- Can use any value (primitive or object) as keys
- Maintains insertion order when iterating
- Has a size property
- Provides methods like
set()
,get()
,has()
,delete()
, andclear()
- Iterable with
for...of
,.forEach()
, etc.
const userMap = new Map();
userMap.set('name', 'John');
userMap.set(42, 'Answer');
userMap.set({id: 1}, 'Object as key');
console.log(userMap.get('name')); // "John"
console.log(userMap.size); // 3
for (const [key, value] of userMap) {
console.log(`${key}: ${value}`);
}
WeakMap:
- Only objects can be used as keys (not primitives)
- Keys are weakly referenced (can be garbage-collected if no other references exist)
- Not iterable (no methods for listing all keys/values)
- Has no size property
- Provides limited methods:
set()
,get()
,has()
, anddelete()
- Useful for storing private data associated with objects or DOM elements
const weakMap = new WeakMap();
let obj = { id: 1 };
weakMap.set(obj, 'Associated data');
console.log(weakMap.get(obj)); // "Associated data"
// If obj is later set to null, it can be garbage collected
// and the entry will automatically be removed from the WeakMap
WeakMaps are particularly useful for scenarios where you need to associate metadata with objects without preventing garbage collection.
18. Explain the concept of generators in JavaScript
Answer: Generators are special functions that can be paused and resumed, allowing for on-demand value generation and custom iterators. They are defined using the function*
syntax and use the yield
keyword to pause execution and return values.
function* countUpTo(max) {
for (let i = 1; i <= max; i++) {
yield i;
}
}
const counter = countUpTo(5);
console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.next()); // { value: 3, done: false }
console.log(counter.next()); // { value: 4, done: false }
console.log(counter.next()); // { value: 5, done: false }
console.log(counter.next()); // { value: undefined, done: true }
Key features of generators:
- Can pause and resume execution with
yield
- Return an iterator when called
- Each call to
next()
executes until the nextyield
statement next()
returns an object withvalue
anddone
properties- Can receive values from outside with
next(value)
- Can delegate to other generators with
yield*
Practical applications:
- Implementing lazy evaluation strategies
- Working with potentially infinite data streams
- Simplifying asynchronous code with generator-based control flow libraries
- Creating custom iterators
Example of a two-way communication:
function* conversation() {
const name = yield "What's your name?";
const hobby = yield `Hello ${name}, what's your hobby?`;
return `${name} likes ${hobby}`;
}
const talk = conversation();
console.log(talk.next().value); // "What's your name?"
console.log(talk.next('Alice').value); // "Hello Alice, what's your hobby?"
console.log(talk.next('coding').value); // "Alice likes coding"
JavaScript Performance Questions
19. How would you optimize JavaScript code for better performance?
Answer: Here are key strategies for optimizing JavaScript performance:
1. Minimize DOM manipulation
- Batch DOM updates
- Use document fragments
- Consider Virtual DOM libraries for complex UIs
// Inefficient
for (let i = 0; i < 1000; i++) {
document.body.appendChild(document.createElement('div'));
}
// Optimized
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
fragment.appendChild(document.createElement('div'));
}
document.body.appendChild(fragment);
2. Efficient event handling
- Use event delegation
- Debounce or throttle expensive listeners
// Event delegation example
document.getElementById('list').addEventListener('click', function(e) {
if (e.target.tagName === 'LI') {
// Handle the click on list item
}
});
3. Optimize loops
- Cache array length in for loops
- Use for...of for arrays and for...in for objects appropriately
- Consider using array methods like map/reduce/filter
// Less optimized
for (let i = 0; i < array.length; i++) {
// Do something
}
// More optimized
const len = array.length;
for (let i = 0; i < len; i++) {
// Do something
}
// Functional approach
const results = array.map(item => transform(item));
4. Memory management
- Avoid memory leaks by properly cleaning up event listeners and references
- Use WeakMap/WeakSet for temporary object references
- Be cautious with closures that might capture large scopes
5. Efficient data structures and algorithms
- Choose appropriate data structures for your operations
- Use Set or Map instead of arrays for frequent lookups
- Consider the Big O complexity of your algorithms
6. Code splitting and lazy loading
- Load JavaScript only when needed
- Use dynamic imports for code splitting
// Dynamic import example
button.addEventListener('click', async () => {
const module = await import('./heavy-feature.js');
module.initFeature();
});
7. Web Workers for CPU-intensive tasks
- Move heavy computations to a separate thread
8. Optimize rendering
- Use requestAnimationFrame for animations
- Minimize layout thrashing by batching reads and writes
// Poor performance - causes layout thrashing
elements.forEach(el => {
const width = el.offsetWidth; // read
el.style.width = (width * 2) + 'px'; // write
});
// Better performance - batch reads and writes
const widths = elements.map(el => el.offsetWidth); // read
elements.forEach((el, i) => {
el.style.width = (widths[i] * 2) + 'px'; // write
});
20. Explain the concept of memoization in JavaScript
Answer: 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's particularly useful for recursive functions or computationally expensive operations.
// Without memoization
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// With memoization
function memoizedFibonacci() {
const cache = {};
return function fib(n) {
if (n in cache) {
return cache[n];
}
if (n <= 1) {
return n;
}
const result = fib(n - 1) + fib(n - 2);
cache[n] = result;
return result;
};
}
const fibMemo = memoizedFibonacci();
console.time('Without memoization');
fibonacci(35); // Very slow, many redundant calculations
console.timeEnd('Without memoization');
console.time('With memoization');
fibMemo(35); // Much faster
console.timeEnd('With memoization');
Generic memoization function:
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
return cache[key];
}
const result = fn.apply(this, args);
cache[key] = result;
return result;
};
}
// Usage
const memoizedFunction = memoize(expensiveOperation);
Key considerations for memoization:
- Most effective for pure functions (same output for same input)
- Trade-off between memory and speed
- Consider cache invalidation or size limits for long-running applications
- JSON.stringify can be limiting for complex objects or circular references
Summary
JavaScript interview questions often test your understanding of core language concepts, asynchronous programming, performance optimization, and modern JavaScript features. To prepare effectively:
- Master the fundamentals: scope, closures, prototypes, and the
this
keyword - Understand asynchronous JavaScript deeply: callbacks, promises, async/await
- Be familiar with ES6+ features and their advantages
- Learn about performance optimization techniques
- Practice explaining complex concepts clearly and concisely
- Be prepared to write code by hand and trace through execution step by step
- Study common coding patterns and best practices
By understanding these core topics and practicing your explanations, you'll be well-prepared for JavaScript technical interviews at any level.
Additional Resources
-
Books:
- "Eloquent JavaScript" by Marijn Haverbeke
- "You Don't Know JS" series by Kyle Simpson
- "JavaScript: The Good Parts" by Douglas Crockford
-
Online Learning:
- MDN Web Docs - JavaScript section
- JavaScript30 by Wes Bos (30 practical exercises)
- FreeCodeCamp JavaScript curriculum
-
Practice Platforms:
- LeetCode (JavaScript problems)
- HackerRank (JavaScript section)
- CodeWars (JavaScript katas)
-
Advanced Topics to Explore:
- Functional programming in JavaScript
- Design patterns in JavaScript
- TypeScript fundamentals
- JavaScript engine internals (V8, SpiderMonkey)
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)