JavaScript Classes Table
| Piece | What it does | Field note |
|---|---|---|
class User { | Class declaration | Class bodies are strict mode by default - no sloppiness inside |
constructor(props) | Runs on new | Returning an object from it replaces this - rarely wanted |
method() {} | Prototype method | Lives on the prototype, shared across instances - not per-instance |
#private = 1 | True private field | The hash is the syntax - enforced by the engine, not convention |
static create() | Class-level method | Called on the class, not instances - factories live here |
get value() / set value(v) | Accessor pair | Property syntax with method logic behind it |
extends Base | Inheritance | this is uninitialized until super() runs - the classic first-line error |
super.method() | Parent call | Required in constructors before touching this |
instanceof Base | Type check | Walks the prototype chain; fails across realms |
ES classes gave JavaScript a clean object-oriented surface: declaration, constructor, methods, inheritance, private state. Under the surface it is still prototypes - but the syntax hides the machinery well enough that you only need to know it when debugging. The table below is the working nine: every piece of a real class, with the rule attached.
Bottom line: the two bugs everyone hits are both ordering errors. In a subclass, this does not exist until super() runs - touching this on the constructor's first line is the ReferenceError every JavaScript developer meets once. And the extends/super pair must appear together: extends without a super() call produces a constructor that cannot construct.
The honest part: class bodies are strict mode always, methods live on the prototype (shared, memory-efficient, invisible on instances), and true privacy finally arrived with #fields - enforced by the engine, unlike the underscore convention. The deeper design question mirrors the Map-versus-Object rule: classes for shapes with behavior, plain objects for data that gets serialized.
How to use
- Find the piece you need in the table; each row carries the rule that prevents its classic bug.
- Order the constructor correctly in subclasses: super() first, then any this access - no exceptions.
- Choose privacy honestly: #fields for state the compiler should enforce, underscore for conventions the teampolices.
Frequently asked questions
Why does 'this' not work in my subclass constructor?
Because in a derived class, this is uninitialized until super() executes - the language refuses to let you touch it first, throwing 'Must call super constructor before accessing this'. The fix is mechanical: super(...) must be the first statement, with whatever arguments the parent constructor needs. The error looks aggressive but it is protecting you from a half-built object: without the parent's initialization, this has no fields to read.
What is the difference between a method and a field in a class?
Methods written normally (describe() {}) live ON THE PROTOTYPE - one copy shared by every instance, invisible in Object.keys of the instance. Fields written as assignments (count = 0) live PER-INSTANCE - every object carries its own copy. The practical consequence: prototype methods are memory-efficient and cannot be enumerated over instances, while fields are data. Mixing them up shows up when iterating instances expecting to find methods, or when comparing two instances that share methods but diverge in fields.
How do private #fields actually work?
The hash makes privacy an engine guarantee, not a naming convention: this.#secret is inaccessible from outside the class body, period - no workaround, no prototype trick. Two gotchas worth knowing: the field must be DECLARED in the class body (even as #secret;) before assignment in some engine paths, and a missing hash on access throws a TypeError that looks like an undefined-property bug. Underscore-prefixed properties remain a team convention - anyone can still read them.
When should I use a class versus a plain object?
Classes earn their keep when state and behavior travel together with invariants to protect - a Validator that cannot be used before init, a Connection with a close() contract. Plain objects win for data that crosses boundaries: anything that will be JSON-serialized, passed between modules, or stored - serialization of class instances loses methods and needs revival logic. The corollary of the Map-versus-Object rule: shape with behavior is a class; bag of values is an object literal.