Technology · JavaScript
IntermediateJavaScript Classes
Build objects with class syntax covering constructors, inheritance, static members, and private fields.
TL;DR
- 01Define object blueprints with class and constructor syntax.
- 02Inherit shared behavior using extends and super calls.
- 03Hide internal state with private fields marked by hash.
Class Basics
- Use
classandconstructorto define a blueprint for creating objects with shared methods.class User { constructor(name, email) { this.name = name; this.email = email; } } - Create new instances with the
newkeyword, which runs the constructor automatically.const user = new User('Ana', 'ana@example.com'); console.log(user.name); // "Ana" - Define instance methods inside the class body without the
functionkeyword.class User { constructor(name) { this.name = name; } greet() { return `Hi, ${this.name}`; } } - Methods live on the prototype, so every instance shares one copy instead of duplicating code.
const a = new User('Ana'); const b = new User('Leo'); console.log(a.greet === b.greet); // true - Class declarations run in strict mode automatically, which catches more silent bugs.
class Demo { constructor() { undeclaredVar = 1; // throws ReferenceError in strict mode } } - Classes are not hoisted the way function declarations are, so define them before use.
// new Greeter() here would throw a ReferenceError class Greeter {}
Static Methods and Properties
- Mark a method
staticto attach it to the class itself instead of each instance.class MathHelper { static square(n) { return n * n; } } MathHelper.square(4); // 16 - Static methods cannot access instance data through
thisbecause no instance exists.class Counter { static count = 0; constructor() { Counter.count++; } } - Use static properties to track data shared across all instances, like a running total.
new Counter(); new Counter(); console.log(Counter.count); // 2 - Build factory methods as static functions that return configured instances.
class User { static fromJSON(json) { const data = JSON.parse(json); return new User(data.name, data.email); } } - Static blocks let you run setup logic once when the class is first defined.
class Config { static settings; static { Config.settings = loadDefaults(); } }
Getters, Setters, and Private Fields
- Use
getto define a property that computes its value each time it's read.class Circle { constructor(radius) { this.radius = radius; } get area() { return Math.PI * this.radius ** 2; } } - Use
setto run logic, like validation, whenever a property is assigned.class Circle { set radius(value) { if (value <= 0) throw new RangeError('Radius must be positive'); this._radius = value; } } - Mark fields private with a leading
#so they can't be read or changed outside the class.class BankAccount { #balance = 0; deposit(amount) { this.#balance += amount; } get balance() { return this.#balance; } } - Accessing a private field from outside the class throws a SyntaxError, not just undefined.
const acc = new BankAccount(); acc.#balance; // SyntaxError: Private field must be declared in an enclosing class - Private methods work the same way, hiding internal logic from the public API.
class Order { #calculateTax(amount) { return amount * 0.08; } getTotal(amount) { return amount + this.#calculateTax(amount); } }
Inheritance with Extends and Super
- Use
extendsto create a subclass that inherits methods and properties from a parent.class Animal { constructor(name) { this.name = name; } speak() { return `${this.name} makes a sound`; } } class Dog extends Animal {} - Call
super()inside a subclass constructor to run the parent constructor first.class Dog extends Animal { constructor(name, breed) { super(name); this.breed = breed; } } - Override a parent method by redefining it with the same name in the subclass.
class Dog extends Animal { speak() { return `${this.name} barks`; } } - Call
super.methodName()to reuse parent logic instead of duplicating it.class Dog extends Animal { speak() { return `${super.speak()} loudly`; } } - Use
instanceofto check whether an object inherits from a given class.const rex = new Dog('Rex', 'Lab'); console.log(rex instanceof Animal); // true
Classes vs Prototypes
- Classes are syntactic sugar over JavaScript's existing prototype-based inheritance model.
class Point { constructor(x, y) { this.x = x; this.y = y; } } // Roughly equivalent to a constructor function + prototype assignment - A class method becomes a non-enumerable property on the constructor's prototype object.
class Point { distanceTo(other) { return Math.hypot(this.x - other.x, this.y - other.y); } } console.log(typeof Point.prototype.distanceTo); // "function" - The
typeofa class is still"function", confirming classes are functions under the hood.console.log(typeof Point); // "function" - Unlike old-style constructor functions, classes throw an error if called without
new.function OldStyle() {} OldStyle(); // works (but usually a bug) class NewStyle {} NewStyle(); // TypeError: Class constructor cannot be invoked without 'new'
Tips
- 01Use private fields with a hash prefix to stop outside code from reading or changing internal state directly.
- 02Call super() before using this in a subclass constructor, since the parent must initialize the instance first.
- 03Prefer static methods for utility functions that relate to a class but don't need a specific instance.
Warnings
- 01Forgetting to call super() in a subclass constructor throws a ReferenceError before this can be accessed.
- 02Arrow function class fields capture this permanently, which can surprise developers expecting normal method binding rules.
- 03Class declarations are not hoisted like functions, so using a class before its definition throws an error.
FAQ
- A class field declares a property directly on the class body, and it runs before the constructor body executes. A constructor assignment sets the property inside the constructor function instead. Both end up creating the same instance property, but fields are often shorter for simple defaults.
- Yes, classes compile down to the same prototype-based inheritance JavaScript always used. Methods defined in a class body are added to the prototype, not to each instance. Classes simply give that pattern cleaner, more familiar syntax.
- An underscore prefix like _name is just a convention; outside code can still access it. A true private field written as #name is enforced by the engine. Accessing it from outside the class throws an error, so private fields offer real encapsulation, not just a hint.
- Use a static method when the logic doesn't depend on a specific instance, like a factory function or a helper. Static methods are called on the class itself, such as MyClass.create(). Instance methods need this to refer to specific object data.
- No, a class cannot have a field and an accessor pair with the same name; that throws a SyntaxError. Pick one approach: a plain field for simple storage, or a getter/setter pair for computed values. Use accessors when you need validation logic too.