LESSON 20 · INTERMEDIATE4 min read

Modern ES6+ Classes: Constructors, #private Fields & Subclassing

Master modern ECMAScript class syntax. Understand constructor semantics, true private fields (#), static members, super(), and prototype inheritance.

ES2024 / Node.js 20+Free to readModule 5 of 10
YOUR NEXT SMALL WINS

By the end, you’ll be able to…

  • Understand ES6 class syntax as declarative sugar over prototype delegation.
  • Implement hard private state using hash private identifiers (#privateField).
  • Manage subclassing hierarchies with extends, super(), and polymorphic overrides.

01Classes: Declarative Prototype Sugar

Introduced in ES2015, the class syntax provides a clean, declarative syntax for creating objects and managing inheritance. However, it is vital to understand that JavaScript did not introduce a new object-oriented inheritance model: under the hood, classes still compile directly to constructor functions and prototype delegation.

A class definition encapsulates the constructor, prototype methods, static methods, and instance fields into a single unified construct. Classes execute strictly in strict mode by default and cannot be called without the new keyword (attempting ClassName() without new throws a TypeError).

Before ECMAScript 2015, achieving true data privacy in JavaScript was only possible using closures inside constructor functions. This had a significant downside: every object instance duplicated method closures in heap memory, preventing memory sharing via the prototype chain.

The introduction of private instance fields with the hash prefix (#field) in modern ECMAScript solved this permanently. Private fields are stored in a dedicated internal slot on the instance, managed directly by the V8 engine. They are syntactically and reflectively invisible from outside the class body—not even Object.getOwnPropertyNames() or Proxy traps can read them.

example.js⚡JavaScript
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

02True Private Fields with Hash (#)

For decades, JavaScript developers relied on convention (such as prefixing property names with an underscore _privateKey) or closures to simulate private variables. In modern ECMAScript, true encapsulation arrived with Private Class Fields, declared with a leading hash (#) symbol.

Private fields cannot be accessed, read, or modified from outside the class body. They are not accessible via Object.keys(), Object.getOwnPropertyNames(), or even reflective proxies. Attempting to access an unexposed private field from outside throws a hard syntax error or TypeError at runtime.

Under the hood, class inheritance via 'extends' wires up two distinct prototype chains simultaneously: instance inheritance and static inheritance.

SubClass.prototype delegates to SuperClass.prototype (so child instances inherit parent methods), and SubClass constructor function delegates directly to SuperClass constructor function (so static utility methods like User.findById are inherited by Admin.findById automatically). This dual delegation is a hallmark of clean object-oriented architecture.

example.js⚡JavaScript
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

03Subclassing and the Super Keyword

The extends keyword sets up prototype delegation between both the constructor functions (for static methods) and their prototypes (for instance methods). When a derived child class defines a constructor, it must call super() before accessing this.

super() invokes the parent class constructor and initializes the this binding. Inside overridden methods, super.methodName() allows invoking the parent's original implementation, enabling clean polymorphic extensions.

example.js⚡JavaScript
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

04Why This Matters in Production

Modern frameworks and enterprise libraries structure complex subsystems (ORM models, HTTP clients, UI components) using modern class hierarchies. Utilizing private fields (#) ensures that internal implementation details remain un-leaked, preventing downstream consumers from coupling directly to internal variables.

Keep class inheritance shallow: deep inheritance hierarchies create tight coupling and fragile base-class problems. Favor composition and behavior delegation over 5-level-deep class hierarchies.

LET IT CLICK

The ideas worth keeping.

  • Classes provide clean syntax over prototype delegation and constructor functions.
  • Use #privateField for hard privacy and encapsulation without closures.
  • Derived classes must invoke super() before accessing 'this' in their constructors.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

Make it yours.

Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.

01

Exercise 1: Encapsulated Counter Class

Build a class SafeCounter with a private field #count, a getter count, and methods increment() and reset().

your_turn_1.js⚡JavaScript
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.jsJavaScript
class SafeCounter {  #count = 0;  get count() { return this.#count; }  increment() { this.#count++; return this.#count; }  reset() { this.#count = 0; }}

The #count private field prevents external tampering, while the getter provides controlled read-only access.

02

Exercise 2: Polymorphic Shape Area Hierarchy

Create an abstract-like Shape base class and a Circle subclass extending Shape that calculates area with Math.PI * radius ** 2.

your_turn_2.js⚡JavaScript
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.jsJavaScript
class Shape {  getArea() { throw new Error('getArea() must be implemented by subclass'); }}class Circle extends Shape {  constructor(radius) {    super();    this.radius = radius;  }  getArea() {    return Math.PI * this.radius ** 2;  }}

Circle overrides getArea() polymorphically, fulfilling the interface established by the base Shape class.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01What happens if a child class constructor forgets to call super() before referencing 'this'?
02How do modern ECMAScript private fields (#field) enforce encapsulation?
03Under the hood, what is an ES6 class in JavaScript?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

Exercise 1: Encapsulated Counter Class

Build a class SafeCounter with a private field #count, a getter count, and methods increment() and reset().

EXPECTED OUTPUT
[Process exited with code 0 - No output]
main.jsJavaScript
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this lesson, when you want a second angle or the full details.

MODULE 05 · PRACTICAL SELF-CHECK

Pause. Put the pieces together.

You’ve reached the end of Objects, Prototypes, this Binding & Classes. Before moving on, use this task to check what you can do independently.

Implement a polymorphic component hierarchy using private fields (#) and behavior delegation.

Try it in your own environment and write down the result. This is a self-check; reading completion does not evaluate your code.

Review this module

One lesson closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock lesson completion.