Skip to main content

ES6 Classes: Syntax and Features

Published: December 18, 2025 Updated: August 30, 2026 Larry Qu 6 min read

ES6 classes provide cleaner syntax for object-oriented programming in JavaScript. Under the hood they still use the prototype system, but the class syntax is more intuitive, supports private fields, and makes inheritance straightforward. This article covers everything from basic class structure to private encapsulation and inheritance patterns.

How Classes Relate to Prototypes

JavaScript classes are syntactic sugar over prototype-based inheritance — they don’t introduce a new object model. A class declaration creates a constructor function and sets up the prototype chain automatically. Understanding this helps when you debug class behavior or work with older code.

// Class declaration
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    return `Hello, I'm ${this.name}`;
  }
}

// Under the hood, this is equivalent to:
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.greet = function() {
  return `Hello, I'm ${this.name}`;
};

const person = new Person('Alice', 30);
console.log(person.greet()); // "Hello, I'm Alice"

One important difference: class bodies always run in strict mode, and class declarations are not hoisted the same way function declarations are.

Class Expressions

Classes can also be created as expressions — useful when you need to generate classes dynamically or pass them as values:

// Anonymous class expression
const Person = class {
  constructor(name) { this.name = name; }
};

// Named class expression — the name is only accessible inside the class body
const Animal = class AnimalClass {
  constructor(name) { this.name = name; }
  whoAmI() { return AnimalClass.name; } // works inside
};

The Constructor

The constructor method runs once when you call new ClassName(...). It initializes instance properties. If you don’t define one, the engine creates an implicit empty constructor:

class User {
  constructor(username, email) {
    this.username = username;
    this.email = email;
    this.createdAt = new Date();
    this.isActive = true;
  }
}

const user = new User('alice', '[email protected]');
console.log(user.createdAt); // current date

Instance Methods

Methods defined in the class body are added to the prototype — they’re shared across all instances, not copied per instance:

class Calculator {
  add(a, b) { return a + b; }
  subtract(a, b) { return a - b; }
  multiply(a, b) { return a * b; }
  divide(a, b) {
    if (b === 0) throw new Error('Division by zero');
    return a / b;
  }
}

const calc = new Calculator();
console.log(calc.add(5, 3));      // 8
console.log(calc.multiply(4, 7)); // 28

Getters and Setters

Getters and setters let you define computed properties and intercept property assignment. They look like property access from the outside but run code:

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  // Getter — accessed as person.fullName (no parentheses)
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  // Setter — called when you do person.fullName = '...'
  set fullName(name) {
    const parts = name.split(' ');
    this.firstName = parts[0];
    this.lastName = parts.slice(1).join(' ');
  }
}

const person = new Person('John', 'Doe');
console.log(person.fullName); // "John Doe"

person.fullName = 'Jane Smith';
console.log(person.firstName); // "Jane"
console.log(person.lastName);  // "Smith"

Getters are also useful for validating access to private fields.

Private Fields

Private fields (prefixed with #) are a native language feature that prevents access from outside the class. Unlike convention-based naming (like _privateField), #field throws a SyntaxError on external access:

class BankAccount {
  #balance;           // Private field declaration
  #transactionLog = [];

  constructor(initialBalance) {
    if (initialBalance < 0) throw new Error('Initial balance cannot be negative');
    this.#balance = initialBalance;
  }

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

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

  get balance() { return this.#balance; }
  get history() { return [...this.#transactionLog]; } // return a copy
}

const account = new BankAccount(1000);
account.deposit(500);
console.log(account.balance); // 1500
// account.#balance;           // SyntaxError — cannot access outside class

Private methods work the same way: #validateAmount(n) { ... }.

Static Methods and Properties

Static members belong to the class itself, not to instances. They’re the right place for utility functions, constants, and factory methods that don’t need this to refer to an instance:

class MathUtils {
  static PI = 3.14159265358979;

  static circleArea(radius) {
    return MathUtils.PI * radius ** 2;
  }

  static degreesToRadians(degrees) {
    return degrees * (MathUtils.PI / 180);
  }
}

console.log(MathUtils.circleArea(5)); // 78.539...
console.log(MathUtils.degreesToRadians(90)); // 1.5707...
// new MathUtils().circleArea(5); // Works but is confusing — use class directly

A static counter pattern tracks instances created:

class Counter {
  static #count = 0;

  constructor() {
    Counter.#count++;
    this.id = Counter.#count;
  }

  static get totalCreated() { return Counter.#count; }
}

new Counter(); new Counter(); new Counter();
console.log(Counter.totalCreated); // 3

Inheritance with extends and super

extends sets up the prototype chain so the child class inherits from the parent. super() must be called in the child constructor before using this:

class Shape {
  constructor(color) {
    this.color = color;
  }

  describe() {
    return `A ${this.color} shape`;
  }
}

class Circle extends Shape {
  constructor(color, radius) {
    super(color);   // must call before this.radius = ...
    this.radius = radius;
  }

  getArea() {
    return Math.PI * this.radius ** 2;
  }

  // Override parent method and extend it
  describe() {
    return `${super.describe()} — circle with radius ${this.radius}`;
  }
}

const c = new Circle('red', 5);
console.log(c.describe()); // "A red shape — circle with radius 5"
console.log(c.getArea());  // 78.539...
console.log(c instanceof Circle); // true
console.log(c instanceof Shape);  // true

Practical Example: User Management System

A realistic example combining private fields, getters, static factory methods, and inheritance:

class User {
  #passwordHash;

  constructor(id, username, email) {
    this.id = id;
    this.username = username;
    this.email = email;
    this.createdAt = new Date();
    this.#passwordHash = null;
  }

  setPassword(hash) { this.#passwordHash = hash; }
  verifyPassword(hash) { return this.#passwordHash === hash; }

  getProfile() {
    return { id: this.id, username: this.username, email: this.email };
  }
}

class Admin extends User {
  constructor(id, username, email, permissions = []) {
    super(id, username, email);
    this.permissions = [...permissions];
  }

  hasPermission(perm) { return this.permissions.includes(perm); }
  grantPermission(perm) { if (!this.hasPermission(perm)) this.permissions.push(perm); }

  getProfile() {
    return { ...super.getProfile(), role: 'admin', permissions: this.permissions };
  }

  // Static factory for convenience
  static create(username, email) {
    return new Admin(Date.now(), username, email, ['read', 'write']);
  }
}

const admin = Admin.create('superuser', '[email protected]');
admin.grantPermission('delete');
console.log(admin.hasPermission('delete')); // true
console.log(admin.getProfile());

Class vs Object Literal: When to Use Each

Classes are the right choice when you need multiple instances with shared methods, inheritance, or private state. For single objects without instances, plain object literals are simpler:

// ✅ Class — multiple instances, inheritance, private state
class UserSession { /* ... */ }
const session1 = new UserSession();
const session2 = new UserSession();

// ✅ Object literal — singleton configuration, no instances
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
};

Summary

ES6 class features and when to use each:

  • constructor — initialize instance state; always called by new
  • Instance methods — shared via prototype, accessed on instances
  • get / set — computed property access and assignment interception
  • #privateField — true encapsulation, SyntaxError on external access
  • static — class-level utilities, constants, factory methods, singletons
  • extends / super — inherit behavior and extend it; always call super() before this in child constructors

Resources

Comments

👍 Was this article helpful?