Ad – 728×90
🏗️ OOP

JavaScript Classes – Object-Oriented Programming in JS

ES6 introduced the class keyword as a clean, readable syntax for creating objects and handling inheritance in JavaScript. Under the hood classes are still built on prototypes, but the syntax makes object-oriented patterns much easier to write and understand. This lesson covers everything from basic class declarations to static members, getters/setters, and public class fields.

⏱️ 22 min read 🎯 Intermediate 📅 Updated 2026

Class Declaration vs Class Expression

You can define a class in two ways: a class declaration uses the class keyword followed by a name, while a class expression assigns the class to a variable. Both create the same result.

JavaScript
// Class declaration
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  greet() {
    return `Hi, I'm ${this.name} and I'm ${this.age} years old.`;
  }
}

// Class expression (anonymous)
const Animal = class {
  constructor(species) {
    this.species = species;
  }
};

// Class expression (named – name is only visible inside the class body)
const Vehicle = class VehicleClass {
  constructor(make) {
    this.make = make;
  }
};

const p = new Person('Alice', 30);
console.log(p.greet());

const dog = new Animal('Canis lupus familiaris');
console.log(dog.species);
▶ Output
Hi, I'm Alice and I'm 30 years old.
Canis lupus familiaris
⚠️
Classes Are NOT Hoisted

Unlike function declarations, class declarations are not hoisted. Trying to use a class before it is declared throws a ReferenceError. Always declare your classes before using them.

The constructor() Method

The constructor is a special method that runs automatically when you use new. It initialises instance properties and can accept any number of parameters. A class can only have one constructor; having more than one throws a SyntaxError.

JavaScript
class Rectangle {
  constructor(width, height) {
    // Validate inputs inside the constructor
    if (width <= 0 || height <= 0) {
      throw new Error('Dimensions must be positive numbers');
    }
    this.width = width;
    this.height = height;
  }

  area() {
    return this.width * this.height;
  }

  perimeter() {
    return 2 * (this.width + this.height);
  }

  toString() {
    return `Rectangle(${this.width} × ${this.height})`;
  }
}

const rect = new Rectangle(5, 3);
console.log(rect.area());       // 15
console.log(rect.perimeter());  // 16
console.log(rect.toString());   // Rectangle(5 × 3)
console.log(typeof rect);       // object

// What typeof tells you about a class itself:
console.log(typeof Rectangle);  // function  (classes ARE functions under the hood)
▶ Output
15
16
Rectangle(5 × 3)
object
function

Public Class Fields

Class fields let you declare and initialise instance properties directly in the class body — without putting them in the constructor. They run before the constructor body and make the shape of an instance immediately obvious.

JavaScript
class Counter {
  // Public class fields (initialised to default values)
  count = 0;
  step = 1;
  label = 'counter';

  constructor(label = 'counter', step = 1) {
    this.label = label;
    this.step = step;
  }

  increment() { this.count += this.step; }
  decrement() { this.count -= this.step; }
  reset()     { this.count = 0; }

  toString() {
    return `[${this.label}] count = ${this.count}`;
  }
}

const c = new Counter('clicks', 2);
c.increment();
c.increment();
c.increment();
console.log(c.toString()); // [clicks] count = 6

// Fields are own properties, not on the prototype
console.log(Object.hasOwn(c, 'count'));  // true
console.log(Object.hasOwn(c, 'increment')); // false (method is on prototype)
▶ Output
[clicks] count = 6
true
false

Static Methods and Properties

Static members belong to the class itself, not to instances. You call them on the class directly. They are commonly used for utility functions, factory methods, and shared configuration.

JavaScript
class MathUtils {
  // Static property
  static PI = 3.14159265358979;
  static instanceCount = 0;

  constructor() {
    MathUtils.instanceCount++;
  }

  // Static methods – no access to 'this' (instance)
  static circleArea(r) {
    return MathUtils.PI * r * r;
  }

  static clamp(value, min, max) {
    return Math.min(Math.max(value, min), max);
  }

  // Factory method – a common static pattern
  static fromDegrees(degrees) {
    return (degrees * MathUtils.PI) / 180;
  }
}

console.log(MathUtils.circleArea(5));      // 78.539...
console.log(MathUtils.clamp(150, 0, 100)); // 100
console.log(MathUtils.fromDegrees(180));   // 3.14159...

new MathUtils();
new MathUtils();
console.log(MathUtils.instanceCount);      // 2
▶ Output
78.53981633974483
100
3.14159265358979
2
💡
When to Use Static Methods

Use static methods for operations that are logically related to the class but don't need access to instance data — things like validation helpers, factory methods, or utility calculations. If the function needs this, it should be an instance method.

Getters and Setters

Getters (get) and setters (set) let you define computed properties and add validation when reading or writing a value. They look like properties from the outside but run functions internally.

JavaScript
class Temperature {
  #celsius = 0; // private field (covered in Encapsulation lesson)

  constructor(celsius) {
    this.celsius = celsius; // goes through the setter
  }

  get celsius() {
    return this.#celsius;
  }

  set celsius(value) {
    if (typeof value !== 'number') throw new TypeError('Temperature must be a number');
    if (value < -273.15) throw new RangeError('Below absolute zero!');
    this.#celsius = value;
  }

  get fahrenheit() {
    return this.#celsius * 9/5 + 32;
  }

  set fahrenheit(value) {
    this.celsius = (value - 32) * 5/9; // delegates to celsius setter
  }

  get kelvin() {
    return this.#celsius + 273.15;
  }

  toString() {
    return `${this.#celsius}°C / ${this.fahrenheit}°F / ${this.kelvin}K`;
  }
}

const temp = new Temperature(100);
console.log(temp.toString());    // 100°C / 212°F / 373.15K

temp.fahrenheit = 32;
console.log(temp.celsius);       // 0
console.log(temp.toString());    // 0°C / 32°F / 273.15K
▶ Output
100°C / 212°F / 373.15K
0
0°C / 32°F / 273.15K

Class vs Object Literal

FeatureClassObject Literal
Multiple instancesYes – use newNo – single object
InheritanceYes – extendsManual prototype wiring
Private fieldsYes – #fieldNo native support
Static membersYes – staticAdd directly to object
Getters/SettersYesYes (get/set keywords)
Use caseBlueprints for many objectsOne-off singleton config
typeof"function" (class itself)"object"

Class vs Function Constructor

Before ES6, objects were created with function constructors and new. Classes are syntactic sugar over this pattern — they compile down to nearly the same prototype-based code.

JavaScript
// Pre-ES6: Function constructor pattern
function PersonOld(name, age) {
  this.name = name;
  this.age = age;
}
PersonOld.prototype.greet = function() {
  return `Hi, I'm ${this.name}`;
};
PersonOld.create = function(name, age) { // static-like
  return new PersonOld(name, age);
};

// ES6+ Class – same result, cleaner syntax
class PersonNew {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  greet() {
    return `Hi, I'm ${this.name}`;
  }
  static create(name, age) {
    return new PersonNew(name, age);
  }
}

// Both produce the same prototype chain
const a = new PersonOld('Alice', 30);
const b = new PersonNew('Bob', 25);
console.log(a.greet()); // Hi, I'm Alice
console.log(b.greet()); // Hi, I'm Bob

// Key difference: classes CANNOT be called without new
// PersonNew('Eve', 20); // TypeError: Cannot call a class as a function

Practical Example – BankAccount Class

Let's bring it all together with a realistic BankAccount class that uses fields, a constructor, instance methods, a static method, and getters.

JavaScript
class BankAccount {
  // Public class field
  currency = 'USD';
  // Transaction history
  #transactions = [];
  static #nextId = 1000;

  constructor(owner, initialBalance = 0) {
    this.owner = owner;
    this.#transactions.push({ type: 'open', amount: initialBalance });
    this.id = `ACC-${BankAccount.#nextId++}`;
  }

  get balance() {
    return this.#transactions.reduce((sum, t) => {
      return t.type === 'credit' ? sum + t.amount
           : t.type === 'debit'  ? sum - t.amount
           : t.type === 'open'   ? sum + t.amount
           : sum;
    }, 0);
  }

  deposit(amount) {
    if (amount <= 0) throw new Error('Deposit must be positive');
    this.#transactions.push({ type: 'credit', amount, date: new Date() });
    return this;
  }

  withdraw(amount) {
    if (amount <= 0) throw new Error('Amount must be positive');
    if (amount > this.balance) throw new Error('Insufficient funds');
    this.#transactions.push({ type: 'debit', amount, date: new Date() });
    return this;
  }

  getStatement() {
    return this.#transactions.map(t =>
      `${t.type.padEnd(6)}: ${t.amount} ${this.currency}`
    ).join('\n');
  }

  static transfer(from, to, amount) {
    from.withdraw(amount);
    to.deposit(amount);
    console.log(`Transferred ${amount} ${from.currency} from ${from.id} to ${to.id}`);
  }
}

const alice = new BankAccount('Alice', 500);
const bob   = new BankAccount('Bob', 100);

alice.deposit(200).deposit(50); // chaining
alice.withdraw(100);
BankAccount.transfer(alice, bob, 75);

console.log('Alice balance:', alice.balance); // 575
console.log('Bob balance:',   bob.balance);   // 175
console.log(alice.getStatement());
▶ Output
Transferred 75 USD from ACC-1000 to ACC-1001
Alice balance: 575
Bob balance: 175
open  : 500 USD
credit: 200 USD
credit: 50 USD
debit : 100 USD
debit : 75 USD
ℹ️
Method Chaining

Notice deposit and withdraw return this. This enables fluent method chaining: alice.deposit(200).deposit(50).withdraw(100). It's a common pattern in well-designed class APIs.

Ad – 336×280

Exercise

Create a ShoppingCart class with:

  • A items array field initialised to []
  • An addItem(name, price, qty) method
  • A removeItem(name) method
  • A total getter that returns the sum of price * qty
  • A static merge(...carts) method that combines multiple carts

Challenge

Extend the BankAccount example above to add:

  • A transactionLimit field capping individual withdrawals
  • A freeze() / unfreeze() method pair using a private boolean
  • A history getter that returns a copy (not a reference) of the transactions array
  • Ensure withdraw throws if the account is frozen

Summary

  • class is syntactic sugar over prototype-based inheritance.
  • The constructor() method initialises instance properties and runs when new is called.
  • Public class fields declare instance properties directly in the class body.
  • Static methods/properties belong to the class, not instances — call them as ClassName.method().
  • Getters (get) and setters (set) let you compute values and validate writes.
  • Classes are not hoisted — declare them before use.
  • typeof ClassName returns "function" because classes are functions.

Common Interview Questions

  • What is the difference between a class declaration and a class expression?
  • What does typeof MyClass return and why?
  • Are classes hoisted in JavaScript?
  • What is the difference between a static method and an instance method?
  • When would you use a getter instead of a regular method?

FAQ

Can a class have multiple constructors?

No. A class body can only contain one method named constructor. Attempting to define two throws a SyntaxError. To handle different argument shapes, use default parameters or conditional logic inside the single constructor.

Are class methods enumerable?

No — methods defined in a class body are non-enumerable by default, which means they won't appear in for...in loops. This is different from manually assigning to a prototype where properties are enumerable.

Can you add new methods to a class after it's defined?

Yes, by adding to ClassName.prototype: MyClass.prototype.newMethod = function() {...}. However, this is generally discouraged because it makes the code harder to follow. For production code, define all methods in the class body.

What is the difference between class fields and constructor assignments?

Class fields are always initialised first (before the constructor body runs) and are syntactically concise. Constructor assignments give you more flexibility (conditionals, parameters), but both create own properties on the instance. For simple defaults, class fields are preferred.

Do static fields get inherited?

Yes. When a class extends another, the subclass inherits static methods and properties through the prototype chain of the class objects themselves. SubClass.staticMethod() will find the method on the parent class if not overridden.