JavaScript Classes
Build objects with class syntax covering constructors, inheritance, static members, and private fields.
TL;DR
- 01Define object blueprints with
classandconstructorsyntax. - 02Inherit shared behavior using
extendsandsuper()calls. - 03Hide internal state with private fields marked by
#.
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.
Class Basics
class + constructorDefine a blueprint for creating objects with shared methods.
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}new keywordCreate instances with new, which runs the constructor automatically.
const user = new User('Ana', 'ana@example.com');
console.log(user.name); // "Ana"Instance methodsDefine instance methods inside the class body without the function keyword.
class User {
constructor(name) {
this.name = name;
}
greet() {
return `Hi, ${this.name}`;
}
}Shared prototypeMethods live on the prototype, so every instance shares one copy instead of duplicating.
const a = new User('Ana');
const b = new User('Leo');
console.log(a.greet === b.greet); // trueStrict modeClass declarations run in strict mode automatically, catching more silent bugs.
class Demo {
constructor() {
undeclaredVar = 1; // throws ReferenceError in strict mode
}
}No hoistingClasses 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
static methodMark a method static to attach it to the class itself instead of each instance.
class MathHelper {
static square(n) {
return n * n;
}
}
MathHelper.square(4); // 16No instance thisStatic methods cannot access instance data through this because no instance exists.
class Counter {
static count = 0;
constructor() {
Counter.count++;
}
}Shared dataUse static properties to track data shared across all instances, like a running total.
new Counter();
new Counter();
console.log(Counter.count); // 2Factory methodsBuild 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 blocksStatic 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
getDefine 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;
}
}setRun 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;
}
}Private fields (#)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;
}
}Enforced privacyAccessing 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 classPrivate methodsPrivate 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
extendsCreate 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 {}super()Call super() inside a subclass constructor to run the parent constructor first.
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
}Overriding methodsOverride a parent method by redefining it with the same name in the subclass.
class Dog extends Animal {
speak() {
return `${this.name} barks`;
}
}super.method()Call super.methodName() to reuse parent logic instead of duplicating it.
class Dog extends Animal {
speak() {
return `${super.speak()} loudly`;
}
}instanceofCheck whether an object inherits from a given class.
const rex = new Dog('Rex', 'Lab');
console.log(rex instanceof Animal); // trueClasses vs Prototypes
Syntactic sugarClasses 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 assignmentMethods on prototypeA 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"typeof a classThe typeof a class is still "function", confirming classes are functions under the hood.
console.log(typeof Point); // "function"Requires newUnlike 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'In Practice
A SavingsAccount subclass extends a base Account class, using super(), a private field, and a getter to apply interest safely.
- 01Account keeps #balance private, exposing it only through a read-only balance getter.
- 02SavingsAccount extends Account and calls super() to initialize the shared owner and balance fields.
- 03applyInterest() reads this.balance through the inherited getter, then calls the inherited deposit() method.
- 04instanceof confirms SavingsAccount still inherits from Account despite adding its own behavior.
class Account {
#balance;
constructor(owner, balance = 0) {
this.owner = owner;
this.#balance = balance;
}
deposit(amount) {
this.#balance += amount;
return this.#balance;
}
get balance() {
return this.#balance;
}
}
class SavingsAccount extends Account {
constructor(owner, balance, rate) {
super(owner, balance);
this.rate = rate;
}
applyInterest() {
const interest = this.balance * this.rate;
return this.deposit(interest);
}
}
const savings = new SavingsAccount('Priya', 1000, 0.05);
savings.applyInterest();
console.log(savings.balance); // 1050
console.log(savings instanceof Account); // trueFAQ
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.