ES6 classes are syntactic sugar over prototypes. Learn constructor, inheritance, private fields, static methods, and common patterns.
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.
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
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
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.
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 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')
// 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'
// 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() // trueMany 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.
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.
No questions tagged to this topic yet.
Tag questions in Admin → Questions by setting the "Topic Page" field to javascript-class-interview-questions.
Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.