2. Classes
Table of Contents:
Key Concepts
The methods of objects created by factory functions are recreated for each instance, wasting memory.
Classes provide a blueprint for creating objects with a shared interface.
The
instanceofoperator can be used to check if a given object is derived from a given class.The
constructor()method is invoked when thenew Class()syntax is used.Inside the
constructor(), the value ofthisis the new instance object.
Own properties are properties defined directly on an object.
The Prototype Chain is the mechanism through which
Intro: Reviewing OOP, Encapsulation, Factory Functions, and Interfaces
In the last lecture, we learned about encapsulation - bundling data and methods that act on that data into an object. We learned how to use closures to hide internal data and restrict access to them.
With encapsulation, we can re-use factory functions like makeFriendsManager to create multiple objects that look alike: each friends manager instance has a username and getFriends() and addFriends() methods.
const makeFriendsManager = (username) => {
const friends = [];
const friendsManager = {
username, // we can leave username "public" if we don't care about how it is used or reassigned.
addFriend(newFriend) {
if (typeof newFriend !== 'string') {
console.log('new friends must be strings');
return;
}
friends.push(newFriend);
},
printFriends() {
this.friends.forEach((friend) => {
console.log(`${this.username} is friends with ${friend}`);
});
},
// add a getter method to get a copy of the friends array
}
return friendsManager;
}
// instances of the factory function have their own friends
const reuben = makeFriendsManager('reuben');
reuben.addFriend('carmen')
reuben.addFriend('ann')
reuben.addFriend('motun')
reuben.printFriends();
// reuben is friends with carmen
// reuben is friends with ann
// reuben is friends with motun
const maya = makeFriendsManager('maya');
maya.addFriend('ben')
maya.addFriend('gonzalo')
reuben.printFriends();
// maya is friends with ben
// maya is friends with gonzaloWe often refer to encapsulated objects as "interfaces" for the data that they manage.
In summary, separation of concerns is achieved in OOP by identifying the features of an application and encapsulating the data and methods for those features in objects to create interfaces for each feature.
Factory Functions Waste Memory
The instances reuben and maya definitely have the same behavior. But do they share that behavior? That is, are the methods reuben.addFriend and maya.addFriend really the same function in memory?
Printing out the objects themselves shows us two identical-seeming objects:
console.log(reuben);
// {
// getFriends: [Function: getFriends],
// addFriend: [Function: addFriend]
// }
console.log(maya);
// {
// getFriends: [Function: getFriends],
// addFriend: [Function: addFriend]
// }But looking at the methods themselves, we can see that they are not referencing the same function in memory.
console.log(reuben.addFriend === maya.addFriend);Each time the factory function is invoked, a brand new object is made and the methods are recreated as well. This is a waste of memory!
To address this memory issue, in 2016 classes were introduced to JavaScript. Let's see how they work.
Classes

A class is similar to a factory function in that it can be used to create objects that all have the same interface (the same set of properties and methods).
Class Definition and new
newIn JavaScript, it starts with the class keyword, an uppercase name, and curly braces. Like this:
// class definitions
class Person {
}
// creating class instances with the `new` keyword
const ben = new Person();
const carmen = new Person();
// Instances are objects derived from a particular class
console.log(ben); // User {}
console.log(carmen); // User {}With a class definition, we can create new instances of that class ben and carmen using the new keyword.
Instanceof
We can use the instanceof operator (kind of like the typeof operator) to see if an object is derived from the given class.
console.log(ben instanceof Person); // true
console.log(ben instanceof Array); // false
console.log([] instanceof Person); // false
console.log([] instanceof Array); // trueSetting Properties With A Constructor
We can add properties to the objects created by a class in two ways:
Define public class fields within the class body.
Define a
constructor()that accepts values when the instance is initialized.
class Person {
// Public class fields are defined here. All instances will start with this property.
friends = [];
// The constructor is invoked whenever a new instance is created.
// This constructor accepts two parameters: name and age
constructor(name, age) {
// The `this` keyword references the new instance object being created
this.name = name;
this.age = age;
}
}
const ben = new Person('ben', 30);
const gonzalo = new Person('gonzalo', 36);
console.log(ben); // Person { name: 'ben', age: 30, friends: [] }
console.log(gonzalo); // Person { name: 'gonzalo', age: 36, friends: [] }As you can see, each instance of Person has the properties friends, name, and age. However, only name and age were set by the constructor while the friends property was hardcoded to []. Here's how it works:
constructoris a "reserved" method name. When thenew Class()syntax is used, JavaScript will look to see if the class has aconstructor()and will automatically execute it.Inside of a
constructor(), thethiskeyword references the new instance object being created.
Public class fields are added to every instance of a class.
thisis not needed to define a public class field.
All of the properties in this example are "public" and the objects are mutable.
ben.age++;
ben.friends.push(gonzalo.name);
console.log(ben); // Person { name: 'ben', age: 31, friends: [ 'gonzalo' ] }Optional Constructor Parameters & Overwriting Public Class Fields
Let's say we want to have the option to add some initial friends when initializing a new Person. There are many ways to do this but the common pattern is to simply create a public field with a hardcoded default value and then overwrite it in the constructor if a value is provided.
class Person {
friends = []; // the default value
constructor(name, age, friends) {
this.name = name;
this.age = age;
if (friends) this.friends = friends;
}
}
const ben = new Person('ben', 30);
console.log(ben); // Person { name: 'ben', age: 30, friends: [] }
const gonzalo = new Person('gonzalo', 36, ['ben', 'carmen']);
console.log(gonzalo); // Person { name: 'gonzalo', age: 36, friends: ['ben', 'carmen'] }Defining Instance Methods
Here's where classes start to shine. We can add methods that all instances share by defining them inside the class body without commas between them. Inside a method, the this keyword will refer to whichever instance is invoking the method.
class Person {
friends = [];
constructor(name, age) {
this.name = name;
this.age = age;
}
addFriend(newFriend) {
// When used in a method, this references the object invoking the method
this.friends.push(newFriend);
}
greet() {
console.log(`Hi, I'm ${this.name}. I am ${this.age} years old and I have ${this.friends.length} friends.`);
}
}
const alan = new Person('Alan Turing', 30);
alan.greet(); // Hi, I'm alan. I am 30 years old and I have 0 friends.
const ada = new Person('Ada Lovelace', 36)
ada.addFriend('Alan');
ada.addFriend('Nikola');
ada.greet(); // Hi, I'm Ada Lovelace. I am 36 years old and I have 2 friends."Own Properties" and Prototypes
In JavaScript, a property of an object is considered "own properties" if that property is defined directly on the object. For instances of a given class, only the public class fields and properties set in the constructor are "own properties". For the Person class, these would be friends, name, and age.
You can see which properties are "own properties" held by an object using the Object.getOwnPropertyNames() static method:
console.log(Object.getOwnPropertyNames(ada)); // [ 'friends', 'name', 'age' ]So, where are the methods addFriend and greet stored if not in the instances? And how can those instances still invoke those methods?
In JavaScript when we create a class, the methods of the class are stored as own properties of the Class.prototype object. For example, we can see the properties owned by the Person.prototype:
// Person.prototype holds the methods available to all Person instances
console.log(Object.getOwnPropertyNames(Person.prototype)); // [ 'constructor', 'addFriend', 'greet' ]Every Person instance has access to that Person.prototype object. It is described as "the prototype of a Person instance".
We can get the prototype of an instance using the Object.getPrototypeOf() static method:
console.log(Object.getPrototypeOf(ada) === Person.prototype); // true
console.log(Object.getOwnPropertyNames(Object.getPrototypeOf(ada))); // [ 'constructor', 'addFriend', 'greet' ]As you can see, the prototype of the ada instance is the object Person.prototype
Array Prototype
We can see the same thing is true for arrays. The "own properties" of an array are the indexes of the array and the length property. Array methods are stored in the prototype object Array.prototype.
const arr = ['a', 'b', 'c'];
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
console.log(Object.getOwnPropertyNames(arr)); // ['0', '1', '2', 'length']
console.log(Object.getOwnPropertyNames(Array.prototype));
/*
[
'length', 'constructor', 'at',
'concat', 'copyWithin', 'fill',
'find', 'findIndex', 'findLast',
'findLastIndex', 'lastIndexOf', 'pop',
'push', 'reverse', 'shift',
'unshift', 'slice', 'sort',
'splice', 'includes', 'indexOf',
'join', 'keys', 'entries',
'values', 'forEach', 'filter',
'flat', 'flatMap', 'map',
'every', 'some', 'reduce',
'reduceRight', 'toLocaleString', 'toString',
'toReversed', 'toSorted', 'toSpliced',
'with'
]
*/This is often why you will see methods like arr.push() written as Array.prototype.push.
Why This Matters
All of this shows us the main benefit of classes compared to factory functions: the methods are truly shared between instances because they are owned by their prototype, not the instance.
console.log(ada.addFriend === alan.addFriend); // true
console.log(ada.addFriend === Person.prototype.addFriend); // true
console.log(alan.addFriend === Person.prototype.addFriend); // trueQuiz!
Can you spot the mistake(s) with the code below?
const Animal = {
this.owners = [];
constructor: (species, sound) => {
this.species = species;
this.sound = sound;
},
makeSound() {
console.log(sound)
}
}
const dog = Animal('canine', 'woof');Challenge
Create a class called FoodItem. Every instance of FoodItem should have the following properties and methods
name— the name of the itemprice- the price of the item in US dollarsweight- the weight of the itemgetPricePerPound()- returns the price / pound of the item
For example, I should be able to use this FoodItem class like so
const apple = new FoodItem('apple', 1, 0.5);
console.log(apple);
// FoodItem { name: 'apple', price: 1, weight: 0.5 }
console.log(apple.getPricePerPound());
// 2Now, create a second class called ShoppingCart. Every instance of ShoppingCart should have the following properties and methods:
items— an array that starts empty. It should holdFoodIteminstances.addItem(FoodItem)— takes in aFoodIteminstance and adds it to theitemsarray.getTotalPrice()- calculates the total price of allFoodItemsin theitemsarray
For example, I should be able to use this ShoppingCart class like so
const myCart = new ShoppingCart();
console.log(myCart); // ShoppingCart { items: [] }
myCart.addItem(new FoodItem('apple', 1, 0.5)) // name, price, weight
myCart.addItem(new FoodItem('bread', 5, 1))
myCart.addItem(new FoodItem('cheese', 7, 2))
console.log(myCart); // ShoppingCart { items: Array(3) }
console.log(myCart.getTotalPrice()); // 13Summary
A class defines a type of object with shared methods and properties
It has a constructor function for defining the default properties that every instance of that class (objects of that type) will have.
All instances of that class inherit the class' methods.
Classes are defined using the
classkeywordInstances of a class are created using the
newkeyword and the class constructor.When used in a constructor function,
thispoints to the newly created objectWhen used in a method,
thispoints to the object invoking the method
class Animal {
owners = [];
constructor (species, sound) {
this.species = species;
this.sound = sound;
}
makeSound() {
console.log(this.sound)
}
}
const dog = new Animal('canine', 'woof');
dog.makeSound(); // 'woof'
const cat = new Animal('feline', 'meow');
cat.makeSound(); // 'meow'Last updated