Class Syntax & Methods Interview Questions & Answers

26 questions Updated 2026-06-18

JavaScript class interview questions — class syntax, constructors, methods, fields, classes vs constructor functions, hoisting and common gotchas.

Read the in-depth guideJavaScript Class Syntax & Methods Explained — Fields, Getters, and the Prototype Truth

A class is a template for creating objects — syntactic sugar over constructor functions and prototypes that gives a cleaner, more familiar OOP syntax.

class User {
  constructor(name) { this.name = name }
  greet() { return `Hi, ${this.name}` }
}
const u = new User('Ada')
u.greet()  // 'Hi, Ada'

Under the hood, User is a function, greet lives on User.prototype, and new User() does the usual prototype wiring. Classes add conveniences (strict mode, new-only, non-enumerable methods) but use the same prototypal machinery.

Classes are functions with extra rules:

  • Must be called with new (throw otherwise).
  • Always strict mode inside the body.
  • Methods are non-enumerable (won't show in for...in).
  • Not hoisted for use (temporal dead zone).
  • Built-in extends/super for inheritance.
class C {}
C()        // TypeError: Class constructor cannot be invoked without 'new'
function F() {}
F()        // fine (no error)

They're the same prototype mechanism with safer defaults and nicer syntax. The new-enforcement alone eliminates the classic "forgot new" bug.

A special method that runs when an instance is created with new, used to initialize instance properties. A class can have at most one.

class Point {
  constructor(x, y) {
    this.x = x
    this.y = y
  }
}

If you omit it, JavaScript supplies a default empty one (or one that calls super(...args) in a subclass). The constructor is where per-instance state on this is set up.

Regular methods are defined on the class's prototype, so all instances share one copy.

class User { greet() {} }
User.prototype.greet           // the method
const a = new User(), b = new User()
a.greet === b.greet            // true — shared

This is memory-efficient. Class fields and arrow-function fields, by contrast, are created per instance (on this), not on the prototype — an important distinction.

Class fields (public instance fields) let you declare instance properties directly in the class body, initialized per instance before the constructor body runs.

class Counter {
  count = 0               // instance field
  step = 1
  constructor(step) { if (step) this.step = step }
}

Fields are added to each instance (on this), not the prototype. They're handy for default values and for arrow-function methods that bind this. Each instance gets its own copy.

A method lives on the prototype (shared) and gets its this from the call site. An arrow-function field is created per instance and captures this lexically (bound to the instance) — useful for callbacks.

class Btn {
  label = 'Click'
  handleA() { return this.label }       // prototype method — `this` can be lost
  handleB = () => this.label             // instance arrow — `this` always bound
}
const b = new Btn()
const a = b.handleA; a()   // this is undefined
const c = b.handleB; c()   // 'Click'

Arrow fields cost memory (one per instance) but solve the "lost this" problem for event handlers without manual bind.

A class defined as an expression (assigned to a variable or passed around), optionally named. Mirrors function declarations vs expressions.

const User = class { greet() {} }          // anonymous class expression
const Animal = class Named {                // named (name visible inside only)
  whoAmI() { return Named.name }
}

Class expressions are useful for conditionally creating classes, mixins (a function returning a class), and IIFE-style patterns. Like function expressions, the name in a named class expression is scoped to the class body.

Class declarations are hoisted but remain in the temporal dead zone, so you can't use them before their declaration — unlike function declarations.

new Foo()   // ReferenceError: Cannot access 'Foo' before initialization
class Foo {}

So practically, classes behave as "not hoisted." Always declare a class before instantiating or extending it. This is one behavioral difference from converting a constructor function to a class.

The class body supports several method forms:

class Example {
  regular() {}                 // prototype method
  static helper() {}           // on the class itself
  get value() {}               // getter
  set value(v) {}              // setter
  async load() {}              // async method
  *generate() {}               // generator method
  [Symbol.iterator]() {}       // computed/symbol method
}

All of these (except fields) go on the prototype (or the class for static). The class body is its own syntax — no commas between members, always strict.

get/set define accessor properties on the prototype that run on read/write, so a property can be computed or validated.

class Temperature {
  #celsius = 0
  get celsius() { return this.#celsius }
  set celsius(v) {
    if (typeof v !== 'number') throw new TypeError('number required')
    this.#celsius = v
  }
  get fahrenheit() { return this.#celsius * 1.8 + 32 }
}

Accessors let you expose a clean property API while validating or deriving behind the scenes. A getter without a setter makes a read-only property.

A method on the class itself, not on instances — called as ClassName.method(). this inside it is the class.

class MathUtil {
  static square(n) { return n * n }
}
MathUtil.square(4)   // 16
new MathUtil().square // undefined — not on instances

Statics are for utilities and factory methods that don't need instance state (Array.from, Object.keys, User.fromJSON). They're inherited by subclasses via the class's own prototype chain.

In a method called as instance.method(), this is the instance. Because class bodies are strict, extracting the method gives this === undefined (a TypeError), not the global object.

class Counter {
  count = 0
  inc() { this.count++ }
}
const c = new Counter()
const fn = c.inc
fn()   // TypeError: Cannot read properties of undefined

Fixes: call it on the instance (() => c.inc()), bind it, or use an arrow-function field. This is the React class-component this problem.

Use a class when you need many instances with shared behavior, inheritance, or encapsulated state. Use an object literal for a single, fixed configuration or namespace.

const config = { apiUrl: '/api', timeout: 5000 }   // one-off -> literal
class User { constructor(name) { this.name = name } }  // many instances -> class

Don't reach for a class just to group functions — a module or plain object is simpler. Classes shine when you're modeling entities that come in multiples.

Use instanceof, which checks whether the class's prototype is in the object's prototype chain (so it sees inheritance).

class Animal {}
class Dog extends Animal {}
const d = new Dog()
d instanceof Dog     // true
d instanceof Animal  // true (ancestor)

For cross-realm safety or duck typing, instanceof can be unreliable; Symbol.hasInstance can customize it, and feature checks (typeof obj.bark === 'function') are an alternative.

Override toString (and optionally Symbol.toPrimitive) to control string conversion in concatenation, template literals, and logging.

class Money {
  constructor(cents) { this.cents = cents }
  toString() { return `$${(this.cents / 100).toFixed(2)}` }
}
`${new Money(150)}`   // '$1.50'

Without it, instances stringify to [object Object]. For richer debug output in Node, you can also implement [Symbol.for('nodejs.util.inspect.custom')].

The spec runs all class code in strict mode automatically — no 'use strict' needed. This surfaces bugs (e.g. this is undefined in detached methods instead of the global object) and disallows sloppy-mode footguns.

class C {
  m() {
    undeclared = 1   // ReferenceError (strict)
    return this      // undefined when called without a receiver
  }
}

Strict mode is part of what makes classes safer than constructor functions: many mistakes throw loudly instead of failing silently.

Yes — use [expression] for dynamic method names, including well-known symbols.

const methodName = 'greet'
class User {
  [methodName]() { return 'hi' }            // dynamic name
  [Symbol.iterator]() { /* ... */ }          // symbol method
}
new User().greet()   // 'hi'

Computed names enable symbol-keyed methods (making a class iterable, thenable, etc.) and metaprogramming where method names come from data.

They're largely equivalent, but timing and inheritance differ. A class field initializes after super() but before the rest of a subclass constructor body; constructor assignment happens wherever you write it.

class A {
  x = 1               // field
  constructor() { this.y = 2 }  // constructor assignment
}

A subtle gotcha: in a subclass, parent constructor logic runs before the child's fields are initialized, so a parent method called during construction won't see the child's field values yet.

Yes — prefix with # for truly private fields and methods, accessible only inside the class.

class Account {
  #balance = 0
  #log(msg) { /* private method */ }
  deposit(n) { this.#balance += n; this.#log('deposit') }
  get balance() { return this.#balance }
}
new Account().#balance   // SyntaxError — private outside class

Unlike the old _underscore convention (just a hint), #private is enforced by the language. (Covered in depth in the static & private members topic.)

Use super.method() inside the subclass — it invokes the parent's version with the current instance as this.

class Animal { speak() { return 'sound' } }
class Dog extends Animal {
  speak() { return super.speak() + ': woof' }  // extend, don't replace
}
new Dog().speak()   // 'sound: woof'

super works in any method (not just the constructor) and is the clean replacement for the manual Parent.prototype.method.call(this) pattern.

Like constructor functions, returning an object overrides the instance; returning a primitive is ignored. But in a derived class you can only return an object or undefined.

class A { constructor() { return { custom: true } } }
new A()   // { custom: true }

class B extends A { constructor() { super(); return 5 } } // 5 ignored -> instance

This enables caching/singleton tricks but is rarely needed and easy to misuse. Most constructors should return nothing.

Define a Symbol.toStringTag getter to control what Object.prototype.toString.call(instance) reports.

class Collection {
  get [Symbol.toStringTag]() { return 'Collection' }
}
Object.prototype.toString.call(new Collection())
// '[object Collection]'

This affects the default toString tag and some debugging output. It's a niche feature, mostly used by built-ins (Map, Promise) and libraries that want meaningful type tags.

Yes — extends accepts any expression that evaluates to a constructor (or null). This enables mixin factories and dynamic base classes.

const Serializable = (Base) => class extends Base {
  toJSON() { return { ...this } }
}
class Model extends Serializable(Object) {}

Because extends <expr> is evaluated, you can compute the superclass — the basis of the mixin-via-class-factory pattern. extends null creates a class with no Object.prototype (advanced/rare).

Return this from methods so calls chain on the same instance.

class Builder {
  parts = []
  add(p) { this.parts.push(p); return this }
  build() { return this.parts.join('') }
}
new Builder().add('a').add('b').build()   // 'ab'

Returning the instance is the builder/fluent pattern, common in query builders and configuration APIs. Each method does its work and hands back this.

Passing instance.method as a callback detaches it from the instance; when the caller invokes it plainly, strict-mode this is undefined.

class Timer {
  seconds = 0
  tick() { this.seconds++ }   // `this` lost if passed directly
  start() {
    setInterval(this.tick, 1000)        // this is undefined
    setInterval(() => this.tick(), 1000) // arrow keeps this
  }
}

Fix with an arrow wrapper, .bind(this), or an arrow-function class field. This is the single most common class this bug.

Avoid classes when there's no real instance state or behavior to model — a module of functions or a plain object is simpler and more idiomatic in JS.

// class just to namespace stateless functions
class StringUtils { static reverse(s) { return [...s].reverse().join('') } }

// plain functions / module
export const reverse = (s) => [...s].reverse().join('')

Functional and composition-based code often reads better in JavaScript than class hierarchies. Use classes for genuine entities with state + behavior + (sometimes) inheritance — not as a default container.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.