Intro to JavaScript Objects & Classes
Click here to access recording
Lesson Setup
For this lesson, we're going to code along using an HTML, CSS & JS REPL from repl.it -- you can name it "JavaScript Objects Practice".`
What Are Objects?
- Objects are the most common data structure in Object Oriented Programming (OOP)
- Very simply, objects are a collection of zero or more properties
-
So what's a property? A property consists of a key: value pair, where the:
- key is a string (JS will coerce the type automatically), and the
- value is any JS expression (code that evaluates to a single value or thing), including other objects (yes, functions too)
-
In computer science, collections of key/value pairs are commonly referred to as dictionaries - a good visualization of what an object is
Why Objects?
- In OOP, we often model the goal of our application using real-world objects
- We use Objects in our code to describe
Nounslike we use functions to describeVerbsand arrays to belists of nouns
Lesson Objectives
After this lesson, students will be able to:
- Explain the difference between arrays and objects
- Store key-value pairs inside an object
- Access values by key-name
- Add Object Properties
- Change Object Properties
- Explain why we use an object instead of an array
- Manipulate objects and arrays declared as
const - List common errors made with objects
- Use object properties with conditionals
- Use an array inside an object
- Iterate over an array that is within an object
- Use an object within an object
- Use an object within an object within an object
- Use an array within an object within an object within an object
- Use an array of objects
- Combine objects, arrays, and functions
- Explain why we need classes
- Create a class to define the blueprint for creating objects
- Add methods to a class
- Set properties on an instance of a class
- What is
this? Why do we need it? - Make an instance of each class customizable
- Create methods to alter the properties of an instance
- Describe the use case for classes
- Describe encapsulation in OOP
- Define a class
- Instantiate a class
- Include and use a constructor method in a class
- Define prototype (instance) methods in a class
- Recognize constructor functions (predecessor to classes)
- Define static (class) methods
- Use extends to create a subclass
- Use super within a subclass
Use an array inside an object
Let's model an adventurer who has belongings (a list)
const adventurer = {
name: "Timothy",
hitpoints: 10,
belongings: ["sword", "potion", "Tums"]
}Access all values in the player.belongings array:
console.log(adventurer.belongings)Access a specific item in the belongings array:
console.log(adventurer.belongings[0])Iterate over an array that is within an object
for (let i=0; i < adventurer.belongings.length; i++) {
console.log(adventurer.belongings[i]);
}Use an object within an object
Our adventurer now has a companion! Our companion, a bat, is an object with its own properties.
Add the companion object to the adventurer object:
const adventurer = {
name: "Timothy",
hitpoints: 10,
belongings: ["sword", "potion", "Tums"],
companion: {
name: "Velma",
type: "Bat"
}
}Access the companion object:
console.log(adventurer.companion)=> { name: "Velma", type: "Bat" }
Access the companion's name:
console.log(adventurer.companion.name)=> "Velma"
Access the companion's type:
console.log(adventurer.companion.type)=> "Bat"
Use an object within an object within an object
Velma the bat also has a companion, a magical parasite called Tim.
Let's add Tim to our data:
const adventurer = {
name: Timothy,
hitpoints: 10,
belongings: ["sword", "potion", "Tums"],
companion: {
name: "Velma",
type: "Bat",
companion: {
name: "Tim",
type: "Parasite"
}
}
}What would you write to:
- console.log Tim's type
Use an array within an object within an object within an object
Tim has a bag of holding and can carry an infinite number of belongings.
Let's add an array of belongings to Tim:
const adventurer = {
name: 'Timothy',
hitpoints: 10,
belongings: ["sword", "potion", "Tums"],
companion: {
name: "Velma",
type: "Bat",
companion: {
name: "Tim",
type: "Parasite",
belongings: ["SCUBA tank", "Rogan josh", "health insurance"]
}
}
}What would your write to:
- console.log "health insurance"
Use an array of objects
A common pattern you will start to see everywhere (especially in Unit 2 and onwards) is an array of objects.
An array of objects can look like this:
const movies = [ { title: "Tokyo Story" }, { title: "Paul Blart: Mall Cop" }, { title: "L'Avventura" } ];These objects have no names, they are just anonymous objects packed into an array.
You could reference them with indexes as usual:
console.log(movies[0]);You could reference the properties by first asking for the index, then the property:
console.log(movies[0].title);You could loop over the array and just print all of the titles:
for (let i=0; i < movies.length; i++) {
console.log(movies[i].title);
}Combine objects, arrays, and functions
You can create a property for an object that is an array
const foo = {
someArray:[1,2,3]
};
foo.someArray[0]; //1You can create a property for an object that is an object
const foo = {
someObject: {
someProperty: 'oh hai!'
}
};
foo.someObject.someProperty; //oh hai!You can create a property for an object that is a function (method)
const foo = {
someMethod:()=>{
console.log('oh hai');
}
};
foo.someMethod();//logs 'oh hai!'You can store an object in an array
const foo = [{someProperty:'weee'}, 2, 3];
console.log(foo[0].someProperty);You can store an array in an array
const foo = [
["0,0", "0,1", "0,2"],
["1,0", "1,1", "1,2"],
["2,0", "2,1", "2,2"]
];
foo[1][2]; //1,2You can store a function in an array
const foo = [
1,
"hi",
()=>{
console.log('fun');
}
];
foo[2]();Explain why we need classes
As we can see we need to repetitively create new objects with the same attributes alot. Imagine we wanted to create a bunch of characters.
We'd need at least:
- name
- health
- power
- stamina
Imagine if we had 500 players would this be okay.....
- What if the structure changed
- What if we needed to upgrade the players
const player = {
name: 'Josh the great',
health: 1000,
power: 1000,
stamina: 1000
}
const bigBadBoss = {
name: 'Magnardo the Merciless',
health: 1000000000000000000,
power: 10000000000000000,
stamina: Infinity
}What about a function?????
const createEnemy = (nameIs, healthIs, powerIs, staminaIs) => {
const newEnemy = {
name: nameIs,
health: healthIs,
power: powerIs,
stamina: staminaIs
}
return newEnemy
}
const createPlayer = (nameIs, healthIs, powerIs, staminaIs) => {
const newPlayer = {
name: nameIs,
health: healthIs,
power: powerIs,
stamina: staminaIs
}
return newPlayer
}Great! A Function that returns objects. How can we create another one? How about copy pasting, then changing all the details? Typing it all from scratch? What if someone makes a typo with a key?
There is a better way! We can create a class, which will be a blueprint or template for similar objects. Not only can we add data, we can also include functionality.
Set Up
- in
repl.it
Create a class to define the blueprint for creating objects
When creating a class, it's custom to capitalize the first letter of the variable, so we know it's a class. This follows customs in other programming languages.
class Character {
}Now we can "instantiate" or create new objects using this class. We do this by adding the new keyword before calling the class name like a function.
const me = new Character();
const you = new Character();
console.log(me);
console.log(you);
console.log(typeof me);
console.log(typeof you);Instance, Instantiate, Instantiation, Encapsulation
Here's a bit more OOP vocab for you:
- instance: An object created by a class
- instantiate: We instantiate a class to create an object
- instantiation: The process of creating an object
- 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.
- Here comes a graphic depicting this principle...
Add methods to a class
Right now, our object doesn't do anything. Let's have it do some stuff;
class Character {
greet () {
console.log('hi!');
}
}
const me = new Character();
const you = new Character();
me.greet();
you.greet();These methods can, of course, take parameters:
class Character {
greet (otherCharacter) {
console.log('hi ' + otherCharacter + '!');
}
}
const me = new Character();
const you = new Character();
me.greet('you');
you.greet('me');We only had to update our code in one place, and then every instance of the class now has the updated functionality. Sweet!
If we have multiple methods, don't put commas between them:
class Character {
greet (otherCharacter) {
console.log('hi ' + otherCharacter + '!');
}
smite () {
console.log('I smite thee you vile person');
}
}
const me = new Character();
const you = new Character();
me.greet('bob');
me.walk();
you.greet('bob');
you.walk();Set properties on an instance of a class
If we log the instances of our class, we'll see they don't have any properties:
class Character {
greet (otherCharacter) {
console.log('hi ' + otherCharacter + '!');
}
smite () {
console.log('I smite thee you vile person');
}
}
const me = new Character();
const you = new Character();
console.log(me);
console.log(you);Let's add some properties with a constructor function. This is a function that gets called once, each time an object is created:
class Character {
constructor () {
this.legs = 2;
this.arms = 2;
this.eyes = 'hazel';
this.hair = 'gray';
}
greet (otherCharacter) {
console.log('hi ' + otherCharacter + '!');
}
smite () {
console.log('I smite thee you vile person');
}
}
const me = new Character();
console.log(me);constructor is a special function. Try misspelling constructor (ie constr) and see if you still get the same results.
Make an instance of each class customizable
Our world is very boring and weird if all of our people are exactly the same! We need a way to customize each object: Our constructor function can take params which we can use to alter the properties of the object instantiated. This allows us to customize each instance:
class Character {
constructor (name, age, eyes, hair) {
this.legs = 2;
this.arms = 2;
this.name = name;
this.age = age;
this.eyes = eyes;
this.hair = hair;
}
greet (otherCharacter) {
console.log('hi ' + otherCharacter + '!');
}
smite () {
console.log('I smite thee you vile person');
}
}
const me = new Character('Cathy the Miraculous', 29, 'brown', 'locs of dark brown');
console.log(me);Create default values
Sometimes, you want to create default values that can be overwritten.
There are two ways to write it, writing it in the constructor with an = is the newer way. Using || is the older way and does work. In both cases, values with that have default parameters should go at the end (why?).
class Character {
constructor (name, age, eyes, hair, lovesCats = false, lovesDogs) {
this.legs = 2;
this.arms = 2;
this.name = name;
this.age = age;
this.eyes = eyes;
this.hair = hair;
this.lovesCats = lovesCats;
this.lovesDogs = lovesDogs || false;
}
greet (otherCharacter) {
console.log('hi ' + otherCharacter + '!');
}
smite () {
console.log('I smite thee you vile person');
}
}
const you = new Character('Cathy the Miraculous', 29, 'brown', 'locs of dark brown', true, true);
const me = new Character('Arthur the Wavy', 32, 'brown', 'his waves be spinnin keep of the g for greatness');
console.log(me);
console.log(you);Create methods to alter the properties of an instance
We can of course, alter the properties of an instance, after it is created:
me.hair = 'supernova red';
console.log(me);But it's a nice practice to define a method that will alter that:
class Character {
constructor (name, age, eyes, hair, lovesCats = true, lovesDogs) {
this.legs = 2;
this.arms = 2;
this.name = name;
this.age = age;
this.eyes = eyes;
this.hair = hair;
this.lovesCats = lovesCats;
this.lovesDogs = lovesDogs || true;
}
greet (otherCharacter) {
console.log('hi ' + otherCharacter + '!');
}
setHair (hairColor) {
this.hair = hairColor;
}
smite () {
console.log('I smite thee you vile person');
}
}
const me = new Character('Arthur', 32, 'brown', 'his waves be spinnin keep of the g for greatness');
console.log(you);
you.setHair('red');
console.log(you);- This way, everything is done with methods
- Other developers can quickly scan the class definition to determine what you'd like them to be able to do with the class
Objects interacting with other objects
We can pass an object to another object to have them interact
class Character {
constructor (name, age, eyes, hair, lovesCats = false, lovesDogs) {
this.legs = 2;
this.arms = 2;
this.name = name;
this.age = age;
this.eyes = eyes;
this.hair = hair;
this.lovesCats = lovesCats;
this.lovesDogs = lovesDogs || false;
}
greet (otherCharacter) {
console.log('hi ' + otherCharacter + '!');
}
classyGreeting (otherClassyCharacter) {
console.log('Greetings ' + otherClassyCharacter.name + '!');
}
setHair (hairColor) {
this.hair = hairColor;
}
smite () {
console.log('I smite thee you vile person');
}
}
const you = new Character('Cathy the Miraculous', 29, 'brown', 'locs of dark brown', true, true);
const me = new Character('Arthur the Wavy', 32, 'brown', 'his waves be spinnin keep of the g for greatness');
me.classyGreeting(you);
you.classyGreeting(me);Continued Lesson Objectives
- Make a class inherit attributes from a "parent class"
- Create static properties for a class
- Create a factory
Make a class inherit attributes from a "parent class"
Sometimes we want to have a "parent" class that will have some basic attributes that will be inherited by "child" classes. Here is our parent class. But what if we have a super hero amongst us that has all our human attributes and more?
class Character {
constructor (name, age, eyes, hair, lovesCats = true, lovesDogs) {
this.legs = 2;
this.arms = 2;
this.name = name;
this.age = age;
this.eyes = eyes;
this.hair = hair;
this.lovesCats = lovesCats;
this.lovesDogs = lovesDogs || true;
}
greet (otherCharacter) {
console.log('hi ' + otherCharacter + '!');
}
classyGreeting (otherClassyCharacter) {
console.log('Howdy ' + otherClassyCharacter.name + '!');
}
setHair (hairColor) {
this.hair = hairColor;
}
smite () {
console.log('i smited thee.');
}
}
const hobbit = new Character('Mr Baggins', 33, 'brown', 'black')
console.log(hobbit);We could just copy paste the above and add what we need, but that's not sticking to the principal of DRY. Instead we can extend our Character class for our Hobbit
We can now add additional functionality:
class Hobbit extends Character {
steal () {
console.log('lets get away!');
}
}
const frodo = new Hobbit('Frodo', 30, 'brown', 'black')
console.log(frodo);
frodo.smite();
frodo.steal();And we can override previous functionality:
class Hobbit extends Character {
steal () {
console.log('lets get away!');
}
greet (otherCharacter) {
console.log(`Hello ${otherCharacter}`);
}
}
const frodo = new Hobbit('Frodo', 30, 'brown', 'black')
frodo.greet('Bob');We can even reference the parent class' method and extend its original functionality:
class Hobbit extends Character {
steal () {
console.log('lets get away!');
}
greet (otherCharacter) {
console.log('Greetings ' + otherCharacter);
}
smite () {
super.smite();
this.steal();
}
}
const frodo = new Hobbit('Frodo', 30, 'brown', 'black')
frodo.smite();This is most useful on the constructor:
class Hobbit extends Character {
constructor (name, age, eyes, hair) {
super(name, age, eyes, hair);
this.skills = ["thievery", "speed", "willpower"];
}
steal () {
console.log('lets get away!');
}
greet (otherCharacter) {
console.log('Greetings ' + otherCharacter);
}
smite () {
super.smite();
this.steal();
}
}
const frodo = new Hobbit('Frodo', 30, 'brown', 'black')
console.log(frodo);super is another special keyword/function. Try mispelling it - and you'll see it won't have the same functionality.
Create a factory
- Sometimes we need to have a factory object that will generate other objects
- The purpose of the factory is so it can control the creation process in some way
-
This is usually a single object that exists throughout the program that performs a set of functions
- also called a singleton
Let's start with a car
class Car {
constructor (maker, serialNumber) {
this.serialNumber = serialNumber;
this.maker = maker
}
drive () {
console.log('Vroom Vroom');
}
}
const newCar = new Car('Mazda', 12345);
console.log(newCar);Now let's make a factory class that will make cars for us.
class Factory {
constructor (company) {
this.company = company;
this.cars = [];
}
generateCar () {
const newCar = new Car(this.company, this.cars.length);
this.cars.push(newCar);
}
findCar (index) {
return this.cars[index];
}
}
const tesla = new Factory('Tesla');
tesla.generateCar();
tesla.generateCar();
tesla.generateCar();
tesla.generateCar();
console.log(tesla);
console.log(tesla.findCar(0));Now we can easily create another new factory that produces its own cars
const porche = new Factory('Porche');
porche.generateCar();
porche.generateCar();
console.log(porche);
console.log(porche.findCar(0));Extra
Create static properties for a class
Sometimes you want to define properties that pertain to the class as a whole, not the instance. This allows us to limit, somewhat, what the user of class can do.
class Person {
static eyeColors () {
return ['blue', 'green', 'brown'];
}
// rest of class definition here...
}
// more code...
const superman = new SuperHero('Clark Kent', 30, Person.eyeColors()[0], 'black');