Classes & Prototypes
A class is a template for creating objects that share the same behavior. It gives JavaScript a cleaner syntax for constructors, methods, inheritance, and private state.
Classes are built on top of JavaScript's prototype system. They do not replace prototypes - they make prototypes easier to work with.
Basic class syntax
class User {
constructor(name, role) {
this.name = name;
this.role = role;
}
describe() {
return `${this.name} is a ${this.role}`;
}
}
const ada = new User("Ada", "developer");
console.log(ada.name);
console.log(ada.describe());
Result:
Ada
Ada is a developer
Use new to create an instance. The constructor method runs automatically and initializes the new object.
Constructors
The constructor is where you set up instance data:
class ShoppingCart {
constructor(owner) {
this.owner = owner;
this.items = [];
}
addItem(name, price) {
this.items.push({ name, price });
}
total() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
const cart = new ShoppingCart("Ada");
cart.addItem("Book", 20);
cart.addItem("Pen", 3);
console.log(cart.owner);
console.log(cart.total());
Result:
Ada
23
If you do not write a constructor, JavaScript provides an empty one for you.
Methods
Methods written inside the class body are shared by all instances:
class Counter {
constructor() {
this.value = 0;
}
increment() {
this.value += 1;
return this.value;
}
reset() {
this.value = 0;
}
}
const counter = new Counter();
console.log(counter.increment());
console.log(counter.increment());
counter.reset();
console.log(counter.value);
Result:
1
2
0
Methods usually use this to read or change the current instance. If you need a refresher on this, see the
Objects chapter.
Public fields
Public fields are properties declared directly in the class body:
class Task {
completed = false;
constructor(title) {
this.title = title;
}
complete() {
this.completed = true;
}
}
const task = new Task("Write documentation");
console.log(task.completed);
task.complete();
console.log(task.completed);
Result:
false
true
Public fields are created on each instance. They are useful for defaults, but do not put shared methods in fields unless
you need a special behavior such as binding this.
Private fields and methods
Private class members start with #. They can only be accessed inside the class that declares them:
class BankAccount {
#balance = 0;
constructor(owner) {
this.owner = owner;
}
#formatBalance() {
return `${this.owner}: ${this.#balance}`;
}
deposit(amount) {
if (amount <= 0) {
throw new RangeError("Deposit must be positive");
}
this.#balance += amount;
}
getStatement() {
return this.#formatBalance();
}
static hasAccountState(value) {
return #balance in value;
}
}
const account = new BankAccount("Ada");
account.deposit(50);
console.log(account.getStatement());
console.log(BankAccount.hasAccountState(account));
console.log(BankAccount.hasAccountState({ owner: "Ada" }));
Result:
Ada: 50
true
false
The expression #balance in value is a brand check. It checks whether an object was created with that private field.
It only works inside the class where the private field is declared.
You cannot read account.#balance from outside the class. That is a syntax error, not just undefined.
Static members
Static fields and methods belong to the class itself, not to instances:
class IdGenerator {
static prefix = "user";
static next = 1;
static create() {
const id = `${this.prefix}-${this.next}`;
this.next += 1;
return id;
}
}
console.log(IdGenerator.create());
console.log(IdGenerator.create());
Result:
user-1
user-2
Instances do not receive static members:
const generator = new IdGenerator();
console.log(typeof IdGenerator.create);
console.log(generator.create);
Result:
function
undefined
Use static methods for helpers that belong to the class concept, such as Date.now() or Array.isArray().
Static blocks
A static block runs once when the class is evaluated. It is useful when static setup needs more than one expression:
class FeatureFlags {
static values = new Map();
static {
this.values.set("search", true);
this.values.set("sharing", false);
}
static isEnabled(name) {
return this.values.get(name) === true;
}
}
console.log(FeatureFlags.isEnabled("search"));
console.log(FeatureFlags.isEnabled("sharing"));
Result:
true
false
Static blocks can access private static fields too.
Getters and setters
Getters and setters look like properties, but run code when you read or assign them:
class Temperature {
#celsius;
constructor(celsius) {
this.celsius = celsius;
}
get celsius() {
return this.#celsius;
}
set celsius(value) {
if (value < -273.15) {
throw new RangeError("Temperature is below absolute zero");
}
this.#celsius = value;
}
get fahrenheit() {
return this.#celsius * 9 / 5 + 32;
}
}
const temperature = new Temperature(20);
console.log(temperature.celsius);
console.log(temperature.fahrenheit);
temperature.celsius = 25;
console.log(temperature.fahrenheit);
Result:
20
68
77
Use setters carefully. Assignment that looks simple can now throw an error or perform validation.
Extending a class
Use extends to create a subclass:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
return `${this.name} barks`;
}
describe() {
return `${super.speak()} and is a ${this.breed}`;
}
}
const dog = new Dog("Mochi", "shiba inu");
console.log(dog.speak());
console.log(dog.describe());
Result:
Mochi barks
Mochi makes a sound and is a shiba inu
In a subclass constructor, call super() before using this. super() runs the parent constructor. Inside methods,
super.methodName() calls a method from the parent class.
Custom error classes are a common place to use extends Error. See the
Error Handling chapter for examples.
instanceof
The instanceof operator checks whether an object has a constructor's prototype in its prototype chain:
class Animal {}
class Dog extends Animal {}
const dog = new Dog();
console.log(dog instanceof Dog);
console.log(dog instanceof Animal);
console.log(dog instanceof Object);
console.log(dog instanceof Array);
Result:
true
true
true
false
instanceof is useful for class instances and built-in objects. For plain data from JSON or another JavaScript realm,
checking the shape of the object is often safer.
Classes and prototypes
Class methods live on the class prototype, not directly on each instance:
class TodoList {
constructor() {
this.items = [];
}
add(item) {
this.items.push(item);
}
}
const list = new TodoList();
console.log(Object.hasOwn(list, "items"));
console.log(Object.hasOwn(list, "add"));
console.log(Object.getPrototypeOf(list) === TodoList.prototype);
console.log(TodoList.prototype.add === Object.getPrototypeOf(list).add);
Result:
true
false
true
true
The items property is stored on the instance. The add method is stored once on TodoList.prototype and shared by
all instances.
The prototype chain
Inheritance links prototypes together:
class Animal {}
class Dog extends Animal {}
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype);
console.log(Object.getPrototypeOf(Animal.prototype) === Object.prototype);
console.log(Object.getPrototypeOf(Object.prototype));
Result:
true
true
null
When you access dog.speak, JavaScript looks on the dog object first, then on Dog.prototype, then on
Animal.prototype, and continues up the chain.
this pitfalls with callbacks
Class methods are not automatically bound to their instance. If you pass a method as a callback, it can lose this:
class ButtonCounter {
count = 0;
increment() {
this.count += 1;
return this.count;
}
}
const counter = new ButtonCounter();
const callback = counter.increment;
try {
console.log(callback());
} catch (error) {
console.log(error instanceof TypeError);
}
Result:
true
The method was called without an object before the dot, so this is undefined.
Fix with bind
class ButtonCounter {
count = 0;
increment() {
this.count += 1;
return this.count;
}
}
const counter = new ButtonCounter();
const callback = counter.increment.bind(counter);
console.log(callback());
console.log(callback());
Result:
1
2
Fix with an arrow class field
class ButtonCounter {
count = 0;
increment = () => {
this.count += 1;
return this.count;
};
}
const counter = new ButtonCounter();
const callback = counter.increment;
console.log(callback());
console.log(callback());
Result:
1
2
Arrow class fields capture this from the instance setup. That makes them convenient for event handlers, but each
instance gets its own function instead of sharing one prototype method.
Hoisting and the temporal dead zone
Class declarations create a binding before the code runs, but you cannot use the class before the declaration is evaluated. It is in the temporal dead zone:
try {
new User("Ada");
} catch (error) {
console.log(error instanceof ReferenceError);
}
class User {
constructor(name) {
this.name = name;
}
}
Result:
true
This is different from function declarations, which can be called before they appear in the file.
Class bodies are strict mode
Code inside a class body always runs in strict mode, even if the rest of the file does not:
class StrictExample {
method() {
accidentalGlobal = 1;
}
}
try {
new StrictExample().method();
} catch (error) {
console.log(error instanceof ReferenceError);
}
Result:
true
Strict mode also explains why an unbound class method receives this as undefined instead of the global object.
When to prefer plain objects or functions
Classes are useful when you create many similar objects that share behavior:
- UI widgets with instance state
- Domain objects with methods and validation
- Custom errors
- Adapters around external APIs
Prefer plain objects when you only need data:
const user = {
id: 1,
name: "Ada",
role: "admin",
};
console.log(user.name);
Result:
Ada
Prefer functions or closures when behavior is small and private state can stay local:
function createCounter() {
let value = 0;
return {
increment() {
value += 1;
return value;
},
get value() {
return value;
},
};
}
const counter = createCounter();
console.log(counter.increment());
console.log(counter.value);
Result:
1
1
Do not use classes just because they are available. Choose the simplest structure that communicates the idea clearly.
Summary
- Classes create objects with shared behavior using
constructor, methods, fields, andnew. - Public fields are per-instance properties; private
#fieldsand#methodsare only accessible inside the class. - Static fields, methods, and blocks belong to the class itself.
- Getters and setters provide property-like access with validation or computed values.
extendsandsuperbuild inheritance chains;instanceofchecks the prototype chain.- Class methods live on the prototype. Instance data lives on each object.
- Passing a method as a callback can lose
this; usebindor an arrow class field when needed. - Classes are in the temporal dead zone before their declaration and class bodies always run in strict mode.
- Prefer plain objects or functions when you only need data or simple behavior.
Next up: ES Modules - split JavaScript into reusable files with imports and exports.