Intermediate
15 min read

JavaScript Interview Questions

Core JavaScript concepts, ES6+, and common interview questions

Essential JavaScript Interview Questions

Master JavaScript concepts that are frequently asked in interviews. This guide covers closures, prototypes, async/await, promises, the event loop, and ES6+ features.

1. Explain Closures with Examples

A closure is a function that has access to variables in its outer (enclosing) lexical scope, even after the outer function has returned.

// Basic closure
function outer() {
  const message = 'Hello';
  
  function inner() {
    console.log(message); // Accesses outer's variable
  }
  
  return inner;
}

const myFunc = outer();
myFunc(); // "Hello" - closure maintains access to message

// Practical example: Private variables
function createCounter() {
  let count = 0; // Private variable
  
  return {
    increment() {
      count++;
      return count;
    },
    decrement() {
      count--;
      return count;
    },
    getCount() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
// count is not accessible directly

// Common pitfall with loops
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3 (var is function-scoped)

// Solution 1: Use let (block-scoped)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2

// Solution 2: IIFE (Immediately Invoked Function Expression)
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 100);
  })(i);
}
// Prints: 0, 1, 2

2. Explain the Event Loop

The event loop is how JavaScript handles asynchronous operations despite being single-threaded.

console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve().then(() => console.log('3'));

console.log('4');

// Output: 1, 4, 3, 2

// Explanation:
// 1. '1' - synchronous, prints immediately
// 4. '4' - synchronous, prints immediately
// 3. '3' - microtask (Promise), executes before macrotasks
// 2. '2' - macrotask (setTimeout), executes last
Event Loop Phases:
  • Call Stack: Executes synchronous code
  • Microtask Queue: Promises, queueMicrotask (higher priority)
  • Macrotask Queue: setTimeout, setInterval, I/O (lower priority)

Process: Execute all synchronous code → Process all microtasks → Process one macrotask → Repeat

3. Promises vs Async/Await

Promises:

// Creating a Promise
const fetchData = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = { id: 1, name: 'John' };
      resolve(data); // Success
      // reject(new Error('Failed')); // Failure
    }, 1000);
  });
};

// Using Promises
fetchData()
  .then(data => {
    console.log(data);
    return fetchData(); // Chaining
  })
  .then(data => console.log(data))
  .catch(error => console.error(error))
  .finally(() => console.log('Done'));

// Promise.all - wait for all
Promise.all([
  fetch('/api/user'),
  fetch('/api/posts'),
  fetch('/api/comments')
])
  .then(([users, posts, comments]) => {
    // All resolved
  })
  .catch(error => {
    // Any rejection fails all
  });

// Promise.race - first to complete
Promise.race([
  fetch('/api/fast'),
  fetch('/api/slow')
])
  .then(result => console.log('First wins:', result));

// Promise.allSettled - wait for all (success or failure)
Promise.allSettled([
  Promise.resolve(1),
  Promise.reject('error'),
  Promise.resolve(3)
])
  .then(results => {
    // [
    //   { status: 'fulfilled', value: 1 },
    //   { status: 'rejected', reason: 'error' },
    //   { status: 'fulfilled', value: 3 }
    // ]
  });

Async/Await:

// Async function always returns a Promise
async function getData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
}

// Sequential execution
async function sequential() {
  const user = await fetchUser(); // Wait
  const posts = await fetchPosts(user.id); // Then wait
  return { user, posts };
}

// Parallel execution
async function parallel() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchComments()
  ]);
  return { user, posts, comments };
}

// Error handling
async function handleErrors() {
  try {
    const data = await fetchData();
  } catch (error) {
    if (error.status === 404) {
      console.log('Not found');
    } else {
      throw error; // Re-throw
    }
  } finally {
    console.log('Cleanup');
  }
}

4. Explain Prototypes and Prototypal Inheritance

// Every object has a prototype
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true

// Constructor function
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// Methods on prototype (shared by all instances)
Person.prototype.greet = function() {
  return `Hi, I'm ${this.name}`;
};

const john = new Person('John', 30);
console.log(john.greet()); // "Hi, I'm John"
console.log(john.__proto__ === Person.prototype); // true

// Inheritance
function Developer(name, age, language) {
  Person.call(this, name, age); // Call parent constructor
  this.language = language;
}

// Set up prototype chain
Developer.prototype = Object.create(Person.prototype);
Developer.prototype.constructor = Developer;

Developer.prototype.code = function() {
  return `${this.name} codes in ${this.language}`;
};

const jane = new Developer('Jane', 25, 'JavaScript');
console.log(jane.greet()); // Inherited from Person
console.log(jane.code()); // "Jane codes in JavaScript"

// Modern ES6 classes (syntactic sugar)
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    return `${this.name} makes a sound`;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Call parent constructor
    this.breed = breed;
  }
  
  speak() {
    return `${this.name} barks`;
  }
}

const dog = new Dog('Rex', 'Labrador');
console.log(dog.speak()); // "Rex barks"

5. var vs let vs const

// var - function scoped, hoisted
console.log(x); // undefined (hoisted, but not initialized)
var x = 5;

function test() {
  var y = 10;
  if (true) {
    var y = 20; // Same variable
  }
  console.log(y); // 20 - var is function scoped
}

// let - block scoped, not hoisted
// console.log(a); // ReferenceError: Cannot access before initialization
let a = 5;

if (true) {
  let b = 10;
}
// console.log(b); // ReferenceError: b is not defined

// const - block scoped, cannot be reassigned
const PI = 3.14;
// PI = 3.14159; // TypeError: Assignment to constant variable

// But object properties can be modified
const obj = { name: 'John' };
obj.name = 'Jane'; // OK
obj.age = 30; // OK
// obj = {}; // TypeError: Assignment to constant variable

// Best practice
const config = Object.freeze({
  API_URL: 'https://api.example.com',
  TIMEOUT: 5000
});
// config.API_URL = 'new'; // Silently fails in strict mode

6. this Keyword - Different Contexts

// Global context
console.log(this); // window (browser) or global (Node.js)

// Object method
const person = {
  name: 'John',
  greet() {
    console.log(this.name); // 'John'
  }
};
person.greet();

// Losing context
const greetFunc = person.greet;
greetFunc(); // undefined (this is window/global)

// Arrow functions - lexical this
const person2 = {
  name: 'Jane',
  greet: function() {
    setTimeout(() => {
      console.log(this.name); // 'Jane' - arrow function uses parent's this
    }, 100);
  },
  greetRegular: function() {
    setTimeout(function() {
      console.log(this.name); // undefined - regular function has own this
    }, 100);
  }
};

// call, apply, bind
function introduce(greeting, punctuation) {
  return `${greeting}, I'm ${this.name}${punctuation}`;
}

const user = { name: 'Alice' };

// call - invoke immediately with arguments
console.log(introduce.call(user, 'Hello', '!')); // "Hello, I'm Alice!"

// apply - invoke immediately with array of arguments
console.log(introduce.apply(user, ['Hi', '.'])); // "Hi, I'm Alice."

// bind - return new function with bound this
const boundIntroduce = introduce.bind(user);
console.log(boundIntroduce('Hey', '?')); // "Hey, I'm Alice?"

// Class context
class Counter {
  constructor() {
    this.count = 0;
    
    // Bind in constructor
    this.increment = this.increment.bind(this);
  }
  
  increment() {
    this.count++;
  }
  
  // Arrow function as class property
  decrement = () => {
    this.count--;
  }
}

const counter = new Counter();
const inc = counter.increment;
inc(); // Works - bound in constructor
counter.decrement(); // Always works - arrow function

7. Array Methods - map, filter, reduce

const numbers = [1, 2, 3, 4, 5];

// map - transform each element
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// filter - select elements
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4]

// reduce - accumulate to single value
const sum = numbers.reduce((acc, n) => acc + n, 0);
console.log(sum); // 15

// Complex example - group by property
const people = [
  { name: 'John', age: 30 },
  { name: 'Jane', age: 25 },
  { name: 'Bob', age: 30 }
];

const groupedByAge = people.reduce((acc, person) => {
  const age = person.age;
  if (!acc[age]) {
    acc[age] = [];
  }
  acc[age].push(person);
  return acc;
}, {});
// { 30: [{name: 'John', age: 30}, {name: 'Bob', age: 30}], 25: [{name: 'Jane', age: 25}] }

// Chaining
const result = numbers
  .filter(n => n > 2)
  .map(n => n * 2)
  .reduce((acc, n) => acc + n, 0);
console.log(result); // 24 (3*2 + 4*2 + 5*2 = 6 + 8 + 10)

// forEach - side effects (no return value)
numbers.forEach(n => console.log(n));

// find - first matching element
const firstEven = numbers.find(n => n % 2 === 0);
console.log(firstEven); // 2

// some - at least one matches
const hasEven = numbers.some(n => n % 2 === 0);
console.log(hasEven); // true

// every - all match
const allPositive = numbers.every(n => n > 0);
console.log(allPositive); // true

8. ES6+ Features

// Destructuring
const user = { name: 'John', age: 30, city: 'NYC' };
const { name, age, country = 'USA' } = user; // with default value

const arr = [1, 2, 3, 4, 5];
const [first, second, ...rest] = arr; // rest operator

// Spread operator
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]

const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = { ...obj1, ...obj2 }; // { a: 1, b: 3, c: 4 } (b is overwritten)

// Template literals
const name = 'John';
const message = `Hello, ${name}!
This is a multi-line
string.`;

// Arrow functions
const add = (a, b) => a + b;
const square = x => x * x; // single parameter, no parentheses
const log = () => console.log('Hi'); // no parameters

// Default parameters
function greet(name = 'Guest', greeting = 'Hello') {
  return `${greeting}, ${name}!`;
}

// Rest parameters
function sum(...numbers) {
  return numbers.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3, 4); // 10

// Object shorthand
const name = 'John';
const age = 30;
const person = { name, age }; // { name: 'John', age: 30 }

// Computed property names
const key = 'dynamicKey';
const obj = {
  [key]: 'value',
  [`${key}_2`]: 'value2'
};

// Optional chaining
const user = { profile: { name: 'John' } };
console.log(user?.profile?.name); // 'John'
console.log(user?.settings?.theme); // undefined (no error)

// Nullish coalescing
const value = null;
console.log(value ?? 'default'); // 'default'
console.log(value || 'default'); // 'default'
console.log(0 ?? 'default'); // 0 (0 is not null/undefined)
console.log(0 || 'default'); // 'default' (0 is falsy)

9. Deep vs Shallow Copy

// Shallow copy - only first level is copied
const original = {
  name: 'John',
  address: {
    city: 'NYC',
    zip: '10001'
  }
};

// Spread operator (shallow)
const copy1 = { ...original };
copy1.name = 'Jane'; // OK
copy1.address.city = 'LA'; // Modifies original too!

// Object.assign (shallow)
const copy2 = Object.assign({}, original);

// Array shallow copy
const arr = [1, 2, [3, 4]];
const arrCopy = [...arr];
arrCopy[2][0] = 99; // Modifies original!

// Deep copy methods
// 1. JSON (limitations: loses functions, dates, undefined, symbols)
const deepCopy1 = JSON.parse(JSON.stringify(original));

// 2. structuredClone (modern, supports most types)
const deepCopy2 = structuredClone(original);

// 3. Custom recursive function
function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof Array) return obj.map(item => deepClone(item));
  
  const clonedObj = {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      clonedObj[key] = deepClone(obj[key]);
    }
  }
  return clonedObj;
}

10. Debouncing and Throttling

// Debounce - execute after delay, reset timer on new call
function debounce(func, delay) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay);
  };
}

// Usage: Search as user types
const searchInput = document.querySelector('#search');
const debouncedSearch = debounce((query) => {
  console.log('Searching for:', query);
  // API call here
}, 300);

searchInput.addEventListener('input', (e) => {
  debouncedSearch(e.target.value);
});

// Throttle - execute at most once per delay
function throttle(func, delay) {
  let lastCall = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastCall >= delay) {
      lastCall = now;
      func.apply(this, args);
    }
  };
}

// Usage: Scroll event
const throttledScroll = throttle(() => {
  console.log('Scroll position:', window.scrollY);
}, 200);

window.addEventListener('scroll', throttledScroll);
Key Interview Takeaways:
  • Closures allow functions to remember their lexical scope
  • Event loop: Sync → Microtasks → Macrotasks
  • Async/await is syntactic sugar over Promises
  • Use let/const over var for block scoping
  • Arrow functions don't have their own this
  • Use map for transformation, filter for selection, reduce for accumulation
  • Spread/destructuring for cleaner code
  • Debounce for delayed execution, throttle for rate limiting