Intermediate0 questionsFull Guide

JavaScript Class Interview Questions

ES6 classes are syntactic sugar over prototypes. Learn constructor, inheritance, private fields, static methods, and common patterns.

The Mental Model

Picture a cookie cutter and the cookies it makes. The cutter defines the shape — the template, the blueprint. Each cookie pressed from it is a separate instance that has that shape, but its own frosting, its own decoration, its own existence. Changing the cutter's shape changes what new cookies look like — but doesn't reshape the cookies already made. A class is the cookie cutter. Instances are the cookies. The class defines what properties each instance gets (the shape), what methods they all share (the cutter's design), and how to initialize a fresh one (the constructor). Methods aren't copied onto each cookie — they're shared from the cutter. That's the prototype chain working invisibly underneath. The key insight: JavaScript classes are syntactic sugar over constructor functions and prototypes. There is no new object model. class Person is a function. Person.prototype holds the methods. new Person() creates an object linked to that prototype. Classes add cleaner syntax, private fields, static blocks, and better inheritance ergonomics — but at runtime, it's the same prototype machinery JavaScript has always had.

The Explanation

Anatomy of a class

class BankAccount {
  // Public class field — own property, initialized before constructor body
  currency = 'INR'

  // Private field — only accessible inside this class body
  #balance = 0
  #transactionLog = []

  // Static field — on the class itself, not instances
  static defaultCurrency = 'INR'
  static #totalAccounts = 0

  constructor(owner, initialBalance = 0) {
    this.owner  = owner      // own property set in constructor
    this.#balance = initialBalance
    BankAccount.#totalAccounts++
  }

  // Public method — on BankAccount.prototype (shared by all instances)
  deposit(amount) {
    if (amount <= 0) throw new RangeError('Amount must be positive')
    this.#balance += amount
    this.#transactionLog.push({ type: 'deposit', amount, at: new Date() })
    return this
  }

  withdraw(amount) {
    if (amount > this.#balance) throw new Error('Insufficient funds')
    this.#balance -= amount
    this.#transactionLog.push({ type: 'withdraw', amount, at: new Date() })
    return this
  }

  // Getter — accessed as a property, not a method call
  get balance() { return this.#balance }

  get history() { return [...this.#transactionLog] }  // copy — don't expose internals

  // Private method
  #validate(amount) { return amount > 0 && amount <= this.#balance }

  // Static method — called on the class, not an instance
  static getTotalAccounts() { return BankAccount.#totalAccounts }

  toString() { return `${this.owner}: ${this.currency}${this.#balance}` }
}

const acc = new BankAccount('Alice', 1000)
acc.deposit(500).withdraw(200)  // method chaining via return this
acc.balance        // 1300 — getter
acc.#balance       // SyntaxError — private field inaccessible outside class
BankAccount.getTotalAccounts()  // 1

Inheritance — extends and super

class Animal {
  #name
  #sound

  constructor(name, sound) {
    this.#name  = name
    this.#sound = sound
  }

  speak()        { return `${this.#name} says ${this.#sound}` }
  get name()     { return this.#name }

  toString()     { return `[Animal: ${this.#name}]` }
}

class Dog extends Animal {
  #tricks = []

  constructor(name) {
    super(name, 'Woof')  // MUST call super() before using 'this'
  }

  learn(trick) {
    this.#tricks.push(trick)
    return this  // chaining
  }

  // Override parent method
  speak() {
    const base = super.speak()  // call the parent's version
    return `${base}! Knows: ${this.#tricks.join(', ') || 'nothing yet'}`
  }
}

const rex = new Dog('Rex')
rex.learn('sit').learn('shake')
rex.speak()  // 'Rex says Woof! Knows: sit, shake'

// Prototype chain:
// rex → Dog.prototype → Animal.prototype → Object.prototype → null
rex instanceof Dog     // true
rex instanceof Animal  // true — instanceof checks the whole chain

Public vs private — what private actually means

class Counter {
  #count = 0       // private field — hard privacy, enforced by the engine

  increment() { this.#count++ }
  get value()  { return this.#count }

  // Private fields are ONLY accessible within the class body
  // Not accessible from subclasses either
}

class LoggedCounter extends Counter {
  increment() {
    super.increment()
    // this.#count  // SyntaxError — # fields are NOT inherited
  }
}

const c = new Counter()
c.increment()
c.value    // 1
c.#count   // SyntaxError — you cannot access private fields from outside

// Before private fields (#), developers used _ convention (not enforced) or WeakMap:
// The _ prefix is a promise, not a lock. Anyone can still access _count.
// Private fields are a lock — the engine refuses access at the syntax level.

Static members — class-level, not instance-level

class MathUtils {
  // Static properties and methods belong to the CLASS, not instances
  static PI = 3.14159265358979

  static add(a, b)      { return a + b }
  static multiply(a, b) { return a * b }
  static square(n)      { return n * n }
}

MathUtils.PI          // 3.14159...
MathUtils.add(2, 3)   // 5

const m = new MathUtils()
m.PI                  // undefined — static is on the class, not the instance
m.add(2, 3)           // TypeError: m.add is not a function

// Static block — runs once when the class is defined
class Config {
  static env
  static apiUrl

  static {
    Config.env    = process.env.NODE_ENV ?? 'development'
    Config.apiUrl = Config.env === 'production'
      ? 'https://api.example.com'
      : 'http://localhost:3000'
  }
}
// Runs once at class definition time — like a static constructor

Class fields vs constructor assignment

class Example {
  // Class field — initialized BEFORE the constructor body runs
  // Each instance gets its OWN copy
  data = []        // each instance gets a fresh []
  count = 0

  // Arrow function class field — bound to the instance
  // Useful for event handlers that need 'this'
  handleClick = (event) => {
    console.log(this.count)  // 'this' is always the instance
  }

  constructor() {
    // Class fields are already set by here
    // this.data already exists and is []
  }
}

// Without class fields — the old way
class OldExample {
  constructor() {
    this.data  = []   // same effect as class field
    this.count = 0
    this.handleClick = (event) => {  // creates a new function PER INSTANCE
      console.log(this.count)        // vs prototype method — shared one function
    }
  }
}

// Key difference:
// Prototype method: ONE function shared by all instances (memory efficient)
// Arrow class field: NEW function per instance (but permanently bound 'this')

Abstract classes — the pattern, not a keyword

// JavaScript has no abstract keyword — simulate it
class Shape {
  constructor(color) {
    if (new.target === Shape) {
      throw new Error('Shape is abstract — instantiate a subclass')
    }
    this.color = color
  }

  // "Abstract" method — subclasses must implement
  area() {
    throw new Error(`${this.constructor.name} must implement area()`)
  }

  describe() {
    return `A ${this.color} ${this.constructor.name} with area ${this.area().toFixed(2)}`
  }
}

class Circle extends Shape {
  constructor(color, radius) {
    super(color)
    this.radius = radius
  }
  area() { return Math.PI * this.radius ** 2 }
}

new Shape('red')           // Error: Shape is abstract
new Circle('blue', 5).describe()  // 'A blue Circle with area 78.54'

Mixins — multiple inheritance workaround

// JavaScript classes can only extend ONE class
// Mixins compose behavior from multiple sources

const Serializable = (Base) => class extends Base {
  serialize()   { return JSON.stringify(this) }
  static deserialize(json) { return Object.assign(new this(), JSON.parse(json)) }
}

const Timestamped = (Base) => class extends Base {
  constructor(...args) {
    super(...args)
    this.createdAt = new Date()
    this.updatedAt = new Date()
  }
  touch() { this.updatedAt = new Date() }
}

const Validatable = (Base) => class extends Base {
  validate() { return Object.keys(this).every(k => this[k] !== null) }
}

// Compose mixins — order matters (right to left)
class User extends Serializable(Timestamped(Validatable(class {}))) {
  constructor(name, email) {
    super()
    this.name  = name
    this.email = email
  }
}

const u = new User('Alice', 'alice@example.com')
u.serialize()   // '{"name":"Alice","email":"...",...}'
u.createdAt     // Date object
u.validate()    // true

Common Misconceptions

⚠️

Many devs think classes are a fundamentally new feature that replaces prototypes — but actually ES6 classes are syntactic sugar over constructor functions and prototype chains. typeof class === 'function', class methods go on ClassName.prototype, and instances are linked via [[Prototype]] exactly as before. Classes add cleaner syntax and new features (private fields, static blocks) but the object model is unchanged.

⚠️

Many devs think private fields with # are just a convention like _ prefix — but actually # private fields are enforced by the JavaScript engine at the syntax level. Accessing a # field from outside the class body is a SyntaxError that prevents the code from even parsing. The _ convention is a gentleman's agreement; # is a hard lock that not even eval() can bypass.

⚠️

Many devs think super() in a subclass constructor is optional — but actually in a class that extends another, calling super() before any use of this is mandatory. The engine throws a ReferenceError if you access this before super(). This is because the parent constructor is responsible for setting up the object — without it, this doesn't exist yet.

⚠️

Many devs think arrow function class fields and prototype methods are equivalent — but actually they have a critical memory difference. Prototype methods are defined once on the prototype and shared by all instances. Arrow function class fields create a new function for every single instance. For a class instantiated hundreds of times, this is significant. The trade-off is: prototype methods lose this in callbacks; arrow fields keep it but cost more memory.

⚠️

Many devs think static properties are shared and mutable in a dangerous way — but static properties are simply properties on the class function object itself. They behave like any other object property. The risk is that subclasses inherit static properties through the prototype chain of the class itself (not instances), so SubClass.staticProp works if SuperClass defines it — which can be surprising.

⚠️

Many devs think class declarations are hoisted like function declarations — but actually class declarations are in the temporal dead zone like let and const. You cannot use a class before its declaration line. Class expressions are also not hoisted. This is different from function declarations, which are fully hoisted and callable anywhere in their scope.

Where You'll See This in Real Code

React class components are the canonical production use of JavaScript classes — class MyComponent extends React.Component inherits lifecycle methods (componentDidMount, setState, render) through the prototype chain. React's class component API was the primary reason millions of JavaScript developers encountered extends and super for the first time, and the migration to hooks was partly motivated by the complexity of managing this binding in class event handlers.

TypeScript's class support is a direct superset of JavaScript classes — TypeScript adds access modifiers (public, protected, private) that are erased at compile time, while JavaScript's # private fields survive to runtime. Understanding that TypeScript private is compile-time only and # private is runtime-enforced is essential for writing secure class-based code in TypeScript.

Node.js's EventEmitter is a class designed to be extended — class MyEmitter extends EventEmitter gives your class emit(), on(), and removeListener() for free through prototype inheritance. This is the textbook use case for class inheritance in Node.js: inheriting a complete, battle-tested capability set rather than copying methods.

Mongoose models are JavaScript classes under the hood — const UserModel = mongoose.model('User', schema) creates a class where every document is an instance. Model methods go on the prototype, statics go on the class, and instance methods have this bound to the document. Understanding classes makes Mongoose's API entirely predictable.

The error subclassing pattern is universal in production JavaScript — class AppError extends Error { constructor(message, code) { super(message); this.code = code; this.name = 'AppError' } }. This is why name must be set manually (Error doesn't set it from the class name) and why super(message) is required to correctly set the message property and stack trace.

Private class fields are now used to implement true encapsulation in browser APIs — the HTML spec uses the same conceptual model for internal state. Libraries like Temporal (the new date/time API) use private fields internally in their polyfills to prevent accidental access to internal state, setting the standard for how platform-quality JavaScript APIs should be written.

Interview Cheat Sheet

  • class: syntactic sugar over constructor function + prototype — typeof class === 'function'
  • constructor(): runs on new, sets own properties; must call super() before this in subclasses
  • Methods: go on ClassName.prototype — shared by all instances (not copied)
  • Class fields: initialized per-instance before constructor body; each instance gets own copy
  • Arrow class fields: bound to instance (good for callbacks), but new function per instance (memory cost)
  • Private (#): engine-enforced, syntax error outside class body, NOT inherited by subclasses
  • Static: on the class itself, not instances; subclasses inherit statics via class prototype chain
  • extends + super: single inheritance only; use mixins for multiple behaviors
  • new.target: the class being constructed — use to simulate abstract classes
  • Class declarations: NOT hoisted (TDZ) — unlike function declarations
💡

How to Answer in an Interview

  • 1."Classes are sugar over prototypes" then prove it with typeof and .prototype — foundational
  • 2.# vs _ private distinction shows you know the language evolved — and why it matters
  • 3.Arrow class fields vs prototype methods memory trade-off is an excellent senior question
  • 4.new.target trick for abstract classes shows you think in patterns, not just syntax
  • 5.The React class component → hooks migration as a real-world consequence of class this binding complexity
📖 Deep Dive Articles
Arrow Functions vs Regular Functions in JavaScript: 6 Key Differences9 min readModern JavaScript: ES6+ Features Every Developer Must Know13 min readJavaScript Error Handling: A Complete Guide to Errors, try/catch, and Async Failures11 min readJavaScript Prototypes Explained: The Object Model Behind Every Class and Method10 min readJavaScript `this` Keyword Explained: Five Rules, Zero Guessing10 min read

Practice Questions

No questions tagged to this topic yet.

Tag questions in Admin → Questions by setting the "Topic Page" field to javascript-class-interview-questions.

Related Topics

JavaScript "this" Keyword Interview Questions
Intermediate·6–10 Qs
JavaScript Prototype & Prototypal Inheritance Interview Questions
Advanced·6–10 Qs
🎯

Can you answer these under pressure?

Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.

Practice Free →Try Output Quiz