JavaScript: Classes and Object-Oriented Programming#
Learn how to use classes in JavaScript and how to write your own.
To download the demo for this lecture, run:
$ dmget wb-classes --demo
Intro#
Why Classes?#
Let’s say we’re building a virtual pet game.
We have objects that store information about pets and functions that operate on those objects.
const ezra = {
name: 'Ezra',
species: 'cat',
hunger: 100,
};
// Feed a pet and reduce their hunger
function feed(pet) {}
// Set the pet's mood to a new value
function setMood(pet, newMood) {}
Potential downsides to this code
Structuring code like this can work, but it’s not ideal for long-term projects. For instance, it’s not immediately apparent that these functions only work on objects with specific properties (name, species, hunger).
It’s also unclear that the objects and functions are designed to be closely related to each other. Even though the object and functions exist in the same file, it’s hard to tell that feed and setMood only work on our particularly-formatted pet objects.
But what if someone created a new pet with different properties?
const ezra = {
name: 'Ezra',
species: 'cat',
hunger: 100,
};
const auden = {
name: 'Auden',
type: 'cat',
hp: 60,
};
We wouldn’t be able to use the same functions with both objects because they have different properties.
Wait, why is this bad?
The ezra and auden objects were probably meant to have the same properties so that
the same functions would work on them. There’s variation—for example, hp
instead of hunger
—making it difficult to reuse code on both objects.
Compared to plain objects, classes can store information in a more structured (but less flexible) way.
Classes can also “have their own smarts.” They can contain functionality, not just data.
Classes vs. objects#
Feature |
Object |
Class |
---|---|---|
Store data |
✔️ |
✔️ |
Flexible |
✔️ |
Some |
Structured |
Some |
✔️ |
Comes with functions |
❌ |
✔️ |
Classes you know#
You’ve already been using classes!
Goals#
Learn the basics of how to use classes in JavaScript
Then, learn how to write your own class!
One more thing before we start:
To give you a preview of what we’re learning next, we’ll be running code in the terminal with
node
Don’t worry if you’re not sure how it works right away—we’ll go over it in more detail soon!
Classes and Instances#
Classes vs. instances#
A class is a type of thing, like
Array
orString
It’s like a blueprint for making instances
It’s a plan for house—but not a house itself
An instance is an individual occurrence of a class, like
[1, 2, 3]
or"hello"
You can have multiple houses based on one blueprint
Multiple instances can exist of the same class
The terms instance and object can be used interchangeably
Classes and instances in JavaScript#
const fruits = new Array('berry', 'apple', 'cherry');
console.log(fruits.length); // 3
fruits.sort();
Classes can be used to create instances with the
new
operatorInstances are created with uniform properties—every array has a
length
andsort
method
The most basic class#
Let’s start with the most basic class to talk about how they work.
class Cat {}
class Cat
is a class declaration.
Class names are written in UpperCamelCase
.
$ node Welcome to Node.js vXX.X.X. Type ".help" for more information. > class Cat{} undefined
Create an instance by calling the class with the new
operator:
const felix = new Cat();
console.log(felix); // Cat {}
felix.name = 'Felix';
console.log(felix); // Cat { name: 'Felix' }
Can have more than one instance of a Cat
:
const ezra = new Cat();
console.log(ezra); // Cat {}
Cat()
is a constructor function.
Cat
objects still work like any other object:
felix.name = 'Felix';
felix.hunger = 100;
for (const attr in felix) {
console.log(attr);
}
Making Your Own Class#
A class body#
Our Cat
class is pretty boring—let’s add to its class body.
A custom constructor#
We want cats to have the same properties every time we create one.
Rather than attaching instance attributes by hand, we can initialize them inside a special function called a constructor.
class Cat {
constructor(name, hunger = 100) {
this.name = name;
this.hunger = hunger;
this.mood = 'happy';
}
}
Inside constructor
, this
refers to the instance being created.
When we create an instance of a class, JavaScript will call the class’s constructor.
const ezra = new Cat('Ezra');
ezra.name; // 'Ezra'
ezra.hunger; // 100
const auden = new Cat('Auden', 60);
auden.name; // 'Auden'
auden.hunger; // 60
Methods#
Instance methods are functions that belong to an instance.
By defining methods on a class, we can give every instance access to the same functions.
class Cat { // ...
greet() {
console.log(`Meow, I'm ${this.name} the cat`);
}
}
Once we define a method, every instance of the class will have access to it.
ezra.greet();
auden.greet();
The power of this
#
Inside methods, we can use this
to access instance properties.
class Cat { // ...
graduate() {
this.name = `Dr. ${this.name}`;
this.greet();
}
}
ezra.graduate();
ezra.name; // 'Dr. Ezra'
Instance properties == data and functions!
Prototypes#
Every object (including functions) gets a prototype
property.

We can use the prototype to store attributes and methods that should be shared by all instances of a class.
If we add a property to Cat.prototype
, then each cat has that property:
const ezra = new Cat('Ezra');
const auden = new Cat('Auden');
Cat.prototype.species = 'Felis catus';
ezra.species; // 'Felis catus'
auden.species; // 'Felis catus'
Instance properties will override and take priority over prototype properties.
ezra.species; // 'Felis catus'
ezra.species = 'Party animal';
ezra.species; // 'Party animal'
Understanding this concept is key to designing classes with object-oriented principals.
Classes are “special functions”
In JavaScript, classes are actually just a special kind of function. This doesn’t really affect how we use them, but it’s good to know. In fact, you might see classes written like this in older code:
function Cat(name, hunger) {
this.name = name;
this.hunger = hunger;
this.mood = 'happy';
}
Cat.prototype.greet = function() {
console.log(`Meow, I'm ${this.name} the cat`);
};
const ezra = new Cat('Ezra', 100);
Object-Oriented Programming#
What’s so great about OO?#
Data lives close to its functionality
Encapsulation
Don’t need to know info a method uses internally
Abstraction
Easy to make different, interchangeable types of animals
Polymorphism
We’ve taken advantage of encapsulation and abstraction but not polymorphism… yet!
Making different types of animals#
Say we want different kinds of animals. Let’s make a more generic Animal
class.
class Animal {
constructor(name, species, hunger = 100) {
this.name = name;
this.species = species;
this.hunger = hunger;
}
greet() {
console.log(`Hey, I'm ${this.name} the ${this.species}`);
}
}
const rover = new Animal('Rover', 'dog');
const ezra = new Animal('Ezra', 'cat');
rover.greet();
ezra.greet();
Animal
is more flexible and generic… but it’s also more boring.
Let’s add more flavor to differentiate different species:
class Animal { // ...
greet() {
let greeting;
if (this.species === 'cat') {
greeting = 'Meow';
} else if (this.species === 'dog') {
greeting = 'Woof';
} else {
greeting = 'Hello';
}
console.log(`${greeting}, I'm ${this.name} the ${this.species}`);
}
}
What are the downsides of this approach?
To handle more species, we’d have to add tons of conditions.
There has to be a better way!
Inheritance#
Inheriting from Animal
#
We can make our Cat
class inherit from Animal
with extends
.
class Cat extends Animal {}
This is like copying and pasting all the contents of Animal
into Cat
.
Vocab: Superclass and Subclass#
- Superclass
A parent class.
Animal
is a superclass ofCat
- Subclass
A child class.
Cat
is a subclass ofAnimal
Subclasses automatically get their parents’ properties and can add their own.
class Cat extends Animal {
purr() {
console.log('Purrrrrrr');
}
}
const ezra = new Cat('Ezra', 'cat');
ezra.greet(); // Hi, I'm Ezra the cat
ezra.purr(); // Purrrrrrr
This still isn’t great though: we have to pass in a species when we create a Cat
and the cats no longer say 'Meow'
.
Overriding properties#
Subclasses can override their parents’ properties.
Let’s override the greet
method so cats say 'Meow'
:
class Cat extends Animal {
greet() {
return `Meow, I'm ${this.name} the ${this.species}`;
}
}
const ezra = new Cat('Ezra', 'cat');
ezra.hunger; // 100 (inherited from Animal)
ezra.greet(); // Meow, I'm Ezra the cat
When you ask for an attribute/method on an object, for example ezra.hunger
:
First, it looks for an attribute called
hunger
on the instance.Next, it looks for an attribute called
hunger
on the class’s prototype.Finally, it looks for an attribute called
hunger
on the superclass, if any.
Reusing code from the superclass#
This greet
method is still almost identical to the greet
method in Animal
.
We can eliminate this repetition by refactoring greet
in Animal
…
class Animal { // ...
greet(greeting = 'Hi') {
console.log(`${greeting}, I'm ${this.name} the ${this.species}`);
}
}
...and then use super
to call the superclass’s method:
class Cat extends Animal { // ...
greet() {
super.greet('Meow');
}
}
const ezra = new Cat('Ezra', 'cat');
ezra.greet();
Overriding the constructor#
We’d also like to not have to pass in a species when creating a Cat
instance, because
the species will always be 'cat'
.
To do that, we can override the parent’s constructor.
When overriding a constructor, you must begin by calling the superclass’s constructor
with super()
.
class Cat extends Animal {
constructor(name, hunger = 100) {
super(name, 'cat', hunger);
}
}
Now we no longer need to pass in a species for Cat
!
const ezra = new Cat('Ezra');
ezra.greet();
Subclasses can also add their own attributes inside the constructor, after the call to
super()
:
class Cat extends Animal {
constructor(name, hunger=100) {
super(name, 'cat', hunger);
this.breed = 'tabby';
}
}
const ezra = new Cat('Ezra');
ezra.breed; // 'tabby'
Recap#
Three benefits of OOP#
- Abstraction
Hiding details we don’t need
- Encapsulation
Keeping everything “together”
- Polymorphism
Different types, same interface
Design considerations#
Polymorphism is the ability to treat different types the same way and without conditionals.
When animals don’t have a uniform way to greet, we have to resort to conditionals:
for (const someAnimal of familyAnimals) {
if (someAnimal.species === 'dog') {
someAnimal.bark();
} else if (someAnimal.species === 'cat') {
someAnimal.meow();
} else if (someAnimal.species === 'bird') {
someAnimal.squawk();
} // ...etc.
}
This is much better:
for (const someAnimal of familyAnimals) {
someAnimal.greet();
}
When overriding methods, it’s common for constructors to take different args:
class Animal {
constructor(name, species, hunger = 100) {
// ...
}
}
class Cat extends Animal{
constructor(name, hunger = 100) {
// ...
}
}
const partyAnimal = new Animal('Sanic', 'party animal');
const ezra = new Cat('Ezra');
...but not for other methods:
class Animal {
greet(greeting) {
// ...
}
}
class Cat extends Animal{
greet() {
super().greet('Meow')
}
}
for (const animal of animals) {
animal.greet(); // uh-oh...
}
This is often solved with default arguments:
class Animal {
greet(greeting = 'Hi') {
// ...
}
}
class Cat extends Animal{
greet() {
super().greet('Meow')
}
}
for (const animal of animals) {
animal.greet(); // yay!
}
Looking Ahead#
Coming up#
Designing class hierarchies
More on Node.js and the
node
command
Further study#
Functional programming, another paradigm for designing programs commonly used in JavaScript