JavaScript Classes
Learning Objectivesβ
Students will be able to: |
---|
Describe the use case for classes |
Describe encapsulation in OOP |
Define a class |
Instantiate a class |
Understand this at a basic level |
Include and use a constructor method in a class |
Define prototype (instance) methods in a class |
Define static (class) methods |
Define static (class) properties |
Use extends to implement inheritance (create a subclass) |
Use super within a subclass |
Road Mapβ
- Setup
- The use case of classes
- Defining and instantiating a class and
this
- Defining prototype methods in a class
- πͺ You Do - Define another class
- Defining static methods and properties
- Inheritance
- Essential Questions
- Further Study
1. Setupβ
- Create a new HTML/CSS/JS-based Repl in replit.com
- Name it "JS Classes"
2. The use case of classesβ
What are classes?β
Classes are used to create objects!
In object-oriented programming (OOP), we use objects to model our application's purpose.
Think of classes as the blueprints used to create objects. These new objects are instances of that class.
Why use classes?β
We've already been creating objects using object ___________ notation.
literal
So why do we need classes to create objects then?
Continuing from our example above - imagine we are building an application for a car dealership, and we want to track their inventory of cars and modify it over time. We can't possibly know the make, model, and color of every car that the car dealership will ever sell when we build the application.
Without classes, we would have to add a new object literal in the app's code every time the dealership buys a new car. Not a great use of anybody's time! To get around this, we can construct a Car
class that defines what a car should be. In this example, a car has a make, a model, and a color.
Encapsulation in OOPβ
Encapsulation is a key principle of Object Oriented Programming.
Encapsulation is the concept of bundling data (properties/attributes) and related behavior (methods) within an object.
Let's build on this idea more with the cars we've constructed above - all of those car objects have three attributes:
make
model
color
These attributes are all data about a car! They describe what a car is and define its properties! So, what would an object with these properties look like?
const hybridCar = {
make: "Toyota",
model: "Prius",
color: "black",
};
β What other attributes might a car have?β
What about the behavior of a car? What can a car do? What can you do to a car? Let's start with the basics - a car needs to start, right? A start()
method will do nicely then.
As you add behaviors you may also decide to create new attributes for a car - a start()
method won't help us much unless we're also tracking if a car is currently running, so we should also create an isRunning
attribute to support that behavior. Let's add those onto our object from above:
const hybridCar = {
make: "Toyota",
model: "Prius",
color: "black",
isRunning: false,
start: function () {
hybridCar.isRunning = true;
console.log("Running!");
},
};
β What other methods might a car have? Would new attributes need to be added to support those methods? If so, what are they?β
β Review Questions - OOPβ
(1) What does the acronym OOP stand for?
Object-Oriented Programming
(2) What are Classes used for in OOP?
To create objects of a certain type.
(3) Describe the OOP principle known as encapsulation.
The bundling of data (properties/attributes) and related behavior (methods) within an object.
(4) Assume we are making a cohort
object - what could we encapsulate inside of it?
cohort
object - what could we encapsulate inside of it?Many things! Here's an example.
Attributes
- subject
- students
- instructors
Methods
- addStudent
- pickRandomStudent
3. Defining and instantiating a class and this
β
Let's write some code! We use the class keyword to define a class in JavaScript:
class Car {
// Code to define the class's properties and methods
}
If this looks similar to defining a function to you, thatβs because classes are special functions with a couple of differences.
β What's different about the above class
definition compared to a function? Do you notice anything different about the naming convention?β
Although we haven't added anything to the class, we can still create an object (instance) from it if we want to (although it wouldn't do us much good).
Instantiating a class (creating an object)β
First, here's a bit more OOP vocabulary in regards to creating objects using a class:
- Instance: An object created by a class.
- Instantiate: We instantiate a class to create an object.
- Instantiation: The process of creating an object.
In JS, we create objects using the new
keyword when invoking (instantiating) the class.
Let's create a myCar
object:
const myCar = new Car();
The constructor
methodβ
When a class is instantiated, a special constructor method defined in the class will automatically be called.
The purpose of the constructor
method is to initialize the data properties of the new object being created (represented by the this
keyword - we'll get to this momentarily).
class Car {
constructor(make, model) {
this.make = make;
this.model = model;
// return is not needed
// because the new object is returned by default
}
}
Now, we can pass the arguments that the constructor
is counting on when instantiating the class:
const myCar = new Car("Ford", "Bronco");
console.log(myCar); // { make: "Ford", model: "Bronco" }
Very nice, but what in the world is this
?
What is this
?β
this
is a keyword in JavaScript available for use inside of functions/methods (like our constructor
above). The this
keyword is a part of a function's execution context, which includes the code and everything that aids in its execution.
The mechanism provided by this
is necessary for all object-oriented programming languages to:
- Provide access to an object's properties and methods from other methods within that object
- Implement code reuse
this
has its value automatically set by the JavaScript engine when a function is invoked. This setting of a value is also known as binding. However, even though we can change the value of this
, doing so is not common, so we'll focus on learning the rules that JavaScript uses to set this
automatically. Some of these rules are explained below.
Object instantiation - behind the scenesβ
When we invoke the class prefaced with the new
keyword behind the scenes:
- JavaScript creates a shiny new empty object and assigns it to the
this
keyword when calling theconstructor
method. - The
constructor
method is called with the arguments we provided when invoking the class. Remember, theconstructor
method is where we create/initialize properties on the new object assigned tothis
. - After executing the
constructor
, the class automatically returns the shiny new object.
Although the constructor
method is notable because it's called automatically, there's nothing special about how it's defined; other methods are defined the same way.
Don't worry if this is hard to grasp at first; this
is a difficult concept - we'll return to it later in this lecture when we can provide a more concrete example. For now, what's most important to remember is that this
in the constructor
function refers to the shiny new object being made whenever it is run! In this context, this
is necessary because we can't know the object's name ahead of time.
π You Do - Add another property, and instantiating a classβ
-
Modify the
Car
class by adding a property namedcolor
. Don't forget to add a new parameter to theconstructor
method. -
Test it by instantiating another object of your choice. It should resemble the one below - you should add this line to your code as well; we'll use it later in this lesson.
const mySubaru = new Car("Subaru", "Crosstrek", "blue");
Not all properties need a parameter in the constructorβ
We can create any number of properties for a new car object within the constructor
.
For example, let's make sure a car isn't running when we build it:
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
this.isRunning = false; // default to false
}
}
Now, any car we create will not be running when it pulls off the assembly line, and we don't need to pass any additional parameters to the constructor!
4. Defining prototype methods in a classβ
Two types of methods can be added to a class - Prototype methods and Static methods.
Prototype methods are available on an instance of the class (object) - this is why they are called instance methods in other OOP languages. forEach
is an example of a prototype method:
const nums = [1, 2, 3, 4, 5];
nums.forEach((num) => console.log(num));
Prototype methods are common, but another less common type of method can be defined.
Static methods are called on the class itself and are not available on instances. These are typically used to implement behavior that doesn't pertain to a specific instance.
Array.isArray is an example of a static method:
const nums = [1, 2, 3, 4, 5];
console.log(Array.isArray(nums)); //-> true
Defining prototype methods in a classβ
Let's add a prototype method called start
to our Car
class:
class Car {
// the constructor will always be called
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
this.isRunning = false;
}
start() {
this.isRunning = true;
console.log("Running!");
}
}
Notice that methods are not separated by a comma or any other character.
Also, notice our friend this
is back! We're using it outside of the constructor this time, so let's briefly talk about why.
Recall when we built a hybridCar
object above:
const hybridCar = {
make: "Toyota",
model: "Prius",
color: "black",
isRunning: false,
start: function () {
hybridCar.isRunning = true;
console.log("Running!");
},
};
It had a start
method, just like any car created using our Car
class will, but instead of having to use this.isRunning
, we were able to use hybridCar.isRunning
. Why is this?
Think about how we instantiate a Car
. We've done this together a couple of times, and you've done it on your own - what must change each time we create a new car?
The name of the shiny new object that gets created!
const anythingWeWant = new Car(...)
anythingWeWant
can be, well, anything we want!
Because we don't know the name of the object being created ahead of time, we cannot specify its name within the start
method. Even if we knew the object's name, it wouldn't be very useful because our start
method would only work with that single object!
In this case, this
is useful because it abstracts away the specific object being made and says this
will reference the current execution context - so when we say run the anythingWeWant.start()
method, anythingWeWant.isRunning
will be set to true
!
As a final note - we could have also used this.isRunning
instead of hybridCar.isRunning
inside of the hybridCar.start()
method as well - the end result would have been identical.
π You Do - Add another instance methodβ
-
Define a
stop
method in theCar
class. -
The
stop
method should set theisRunning
property tofalse
and log"Stopped!"
to the console.
Overriding methodsβ
Thanks to another OOP principle called inheritance, subclasses inherit methods from their parent classes.
JavaScript implements inheritance differently from traditional OOP languages like Java or Python in that JavaScript's implementation is prototype-based.
We won't go into prototypes during this lesson because, thanks to the new class syntax we're currently learning about, knowing about prototypes isn't as important anymore. Check out the Further Study section for additional info.
In JavaScript, the Object
class is at the top of the class hierarchy, and thus nearly all objects inherit its methods, such as toString:
console.log(mySubaru.toString()); // something like: [object Object]
However, it's possible to override inherited methods by redefining that method.
For example, we can override/replace Object
's implementation of toString
by defining it in the Car
class:
class Car {
// the constructor will always be called
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
this.isRunning = false;
}
start() {
this.isRunning = true;
console.log("Running!");
}
toString() {
return `This car is a ${this.color} ${this.make} ${this.model}.`;
}
}
and now when we call the toString()
method on mySubaru
:
console.log(mySubaru.toString()); // "This car is a blue Subaru Crosstrek"
So far, you've learned how to define a class that creates objects with properties and add prototype methods. This represents about 80% of what there is to know about classes - congrats! Sounds like a great time for you to try your hand at this!
5. πͺ Practice Exercise - Define another classβ
- Pick something like our car example above that would make sense to instantiate. You can pick anything you'd like, but we'll use
Airplane
as an example. Remember that the class name should be uppercase and singular. - Code the class'
constructor
method so that it creates at least two properties. - Add at least two methods to the class. At least one of the methods you write should interact with one or multiple of the properties you created above. As you write your methods you might think of more properties you could add to the constructor - if you do, add them!
- Create a couple of new instances of the class you've built.
6. Defining static methods and propertiesβ
Again, static methods and static properties, are accessible on a class itself - not on its instances.
Static methods and propertiesβ
Static methods implement behavior that does not pertain to a particular instance. These are callable on the class itself - not on its instances.
Static methods are typically used to implement behavior that does not pertain to a particular instance. For example, we could design the Car
class so that it tracks every car it creates. We could then write static methods that return how many cars have been created, search for them by their make, and more.
Hereβs how to define a basic static method:
class Car {
// the constructor will always be called
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
this.isRunning = false;
}
start() {
this.isRunning = true;
console.log("Running!");
}
toString() {
return `This car is a ${this.color} ${this.make} ${this.model}.`;
}
static about() {
console.log("I'm the Car class!");
}
}
The only difference is the static
keyword that prefaces the method's name.
Try it out!
Car.about(); // I'm the Car class!
However, attempting to call about
on the mySubaru
instance leads to an error:
mySubaru.about(); // Uncaught TypeError: mySubaru.about is not a function
7. Inheritanceβ
In OOP, inheritance is when a new class is derived from an existing class to specialize it by:
- Adding additional properties
- Adding additional methods
- Overriding existing methods
The newly derived class is called a derived or subclass.
The original class is called a base or superclass.
A subclass automatically inherits all of the superclass's properties and methods - whether you want them all or not.
Above, the Insect
superclass has the BumbleBee
and Grasshopper
subclasses derived from it. Class diagrams are typically drawn in a specific way as described by the Unified Modeling Language's class diagram specifications, but we've simplified that here since those details aren't important to your understanding of this concept.
We're going to add a subclass called ElectricCar
that will be derived from the Car
superclass! Here's a diagram showing the relationship we will create.
Using the extends
keyword to create a subclassβ
We use the extends
keyword to define a subclass.
class ElectricCar extends Car {
constructor(make, model, color, batteryCharge) {
super(make, model, color);
this.batteryCharge = batteryCharge;
}
}
In a derived class, the super keyword represents the parent superclass and must be called before the this
keyword can be used in the constructor. It can also be used to access properties of the superclass.
Additional properties like batteryCharge
above can be initialized in the constructor
. Instances of ElectricCar
will have a batteryCharge
property, while instances of Car
will not. Let's build an electric car!
const myVolvo = new ElectricCar("Volvo", "EX30", "Gray", 100); // Fully charged!
Determining which classes in a hierarchy to attach properties and methods to is difficult work, full of pitfalls and gotchas. This is one of the biggest hurdles to overcome when you're getting started with inheritance - it won't always be as clear-cut as it is above!
Overriding methodsβ
One last thing before we wrap up. Remember how we overrode the toString()
method earlier? What if we also wanted to override the start()
method provided by the Car
class in the ElectricCar
class? Check it:
class ElectricCar extends Car {
constructor(make, model, color, batteryCharge) {
super(make, model, color);
this.batteryCharge = batteryCharge;
}
start() {
if (this.batteryCharge > 0) {
this.isRunning = true;
console.log("Your electric car is running!");
} else {
this.isRunning = false;
console.log("Time to recharge!");
}
}
}
Very cool. Start up your electric car, quickly deplete the battery, and try to restart it.
myVolvo.start(); // "Your electric car is running!"
myVolvo.batteryCharge = 0;
myVolvo.start(); // "Time to recharge!"
8. β Essential Questionsβ
(1) What are classes used for?
Classes are used to create objects.
(2) What is the name of the special method in a class that is automatically called when we instantiate a class?
constructor
(3) What is the main purpose of the above method?
To initialize the properties on the shiny new object.
(4) True or False: Prototype methods are called on a class.
False, prototype methods are called on instances (objects) of a class.
(5) What keyword in JavaScript is used to implement inheritance (create a subclass)?
extends
9. Further studyβ
Example DOM Node Class Hierarchyβ
In complex systems, it's not uncommon to have several layers of inheritance.
For example, let's take the suggested Airplane
class from the Practice Exercise earlier. As you thought through that exercise you may have included properties that overlap with the existing Car
model, like make
or model
- airplanes can have a make or model too, right? Maybe that would warrant creating a new class (perhaps Vehicle
) to be a superclass of both the Car
and Airplane
classes.
With this example, you can start to see how class inheritance may play out in a larger application or system - below you can see a simplified class hierarchy for DOM elements:
Yes, that is actually oversimplified - the DOM is quite complex!
Subclass JavaScript's built-in classesβ
It's possible to use extends
to subclass JavaScript's built-in classes.
For example, it would be nice to have a last
property on arrays that returns the last element of the array:
class MyArray extends Array {
get last() {
return this[this.length - 1];
}
}
You then would need to instantiate the MyArray
class to create the specialized array:
const nums = new MyArray(1, 2, 3, 4, 5);
console.log(nums.last); //-> 5
All other array properties and methods are available due to inheritance!
Prototypal inheritance and the prototype chainβ
Learn more about prototypal inheritance and the prototype chain here in the docs.
Constructor functions - B.C. (Before Classes π)β
Before classes arrived via ES2015, we used constructor functions to do the same thing as classes.
No doubt, new code today will be written using the class
keyword.
However, older code would have had to use constructor functions, so let's look at how the Car
class can be written as a constructor function instead:
function Car(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
this.isRunning = false; // default to false
}
Car.prototype.start = function () {
this.isRunning = true;
console.log("Running!");
};
// other prototype (instance) methods defined like above would go here
// Instantiation is identical
const car = new Car("Toyota", "Camry", "Green");
Static methods would be created directly on the constructor function:
Car.about = function () {
console.log("I'm the Car class!");
};
Referencesβ
-
Old school Prototypal Inheritance example