class, prototype, and Object-Oriented Programming
Organizing code structures around Objects rather than functions is called Object-Oriented Programming (or OOP).
As we've seen already, Objects can have keys which are Strings, and values which can be any data type: Strings, Numbers, Arrays, Objects... even functions!
const person = {
name: "Bob",
location: "Los Angeles",
age: 56,
hobbies: ["working", "partying"],
cat: {
name: "mr fuzzles",
hobbies: ["being inert", "nudging things off tables"]
},
party: function throwParty() {
console.log("dance, dance, dance!");
}
};
There are a number of powerful additions to this basic understanding of Objects that allow us to more meaningfully group the data that represents a thing to the actions (called "methods") that operate on or are associated with those other pieces of data.
Methods and this
this
In the example above, party
is a function
. If a function
is attached to an Object, we refer to that function
as a method. While it's perfectly valid to notate the party
method as above, we can also use a shorter syntax:
const person = {
// other properties here
party() {
console.log("dance, dance, dance");
}
}
And we can call this method by invoking person.party()
. But what does this give us (besides a bit of organizational and semantic clarity)?
One of the big benefits of attaching methods to Objects is access to the calling context (or parent Object) of that method through the this
keyword. See if you can get the following to work:
const person = {
name: "Bob",
// other properties and methods
greet(){
console.log(`Hi, my name is ${this.name}`);
}
};
this
is a tough concept in JavaScript, but the thing to remember: this
refers to the calling context of a function
. Which, as a matter of fact, is always an Object of some sort. Try this regular function
:
function logDefaultContext(){
console.log(`what's this? -> ${this}`);
}
You should find that the above function
, which doesn't appear to be attached to any Object, does have a default calling context that's built into the browser. What's the value of that context?
Exercise 1
Here in my car
We can also change an Object's properties by referencing them with this
:
const car = {
type: "Honda Civic",
position: 1,
move() {
let prev = this.position;
this.position = this.position + 1;
console.log(`${this.type} is moving from ${prev} to ${this.position}`);
}
};
Invoke
car
'smove
method and see what happens.Invoke it a few more times. Then check its
position
property.Add a
speed
property (an integer) to car.When a car moves, adjust its position by adding its speed. HINT:
const car = {
type: "Honda Civic",
position: 1,
speed: 8,
move() {
let prev = this.position;
this.position = this.position + this.speed;
console.log(`${this.type} is moving from ${prev} to ${this.position}`);
}
};
Constructor function
s
function
sThe car example above works well as long as we're dealing with a single type of car. But what if we'd like to share properties and methods of cars amongst a number of more specific types of cars? To put it another way: how can we treact "car" as a category of things, and than create specific instances of that category later on? The answer is to construct an instance of a car Object. One way that we can do that is with something called a constructor function
, which might look like this:
// constructor names are capitalized by convention
function Car(type, speed){
// to what does "this" refer?
this.position = 1;
this.type = type;
this.speed = speed;
}
This constructor gives us the ability to create a specific instance of this more general Car
idea. To create that instance, we need to use a keyword that we saw previously when we set up our client side router: new
. Try the following:
const civic = new Car("Honda Civic", 8);
const camry = new Car("Toyota Camry", 7);
What are the data types of civic
and camry
? Hopefully, they're Objects that have similar properties (speed
, type
, and position
).
prototype
and inheritance
prototype
and inheritanceBut what of our move()
method? These newly-constructed cars have a consistent set of properties, which is nice, but they don't yet share a consistent set of behaviors. We could, if we wanted to, define a move
function on the constructor. But it's more ideomatic for each Car
to inherit behaviors.
Most OOP languages have a concept of inheritance. JavaScript is a bit unique among programming languages in that Objects inherit behaviors from something called a prototype
. Every Object has at least one prototype
, which is itself an Object. Every instance of constructed Object has a prototype that is inherited from its constructor. To put it another way: if you look at the __proto__
property of your civic
Object in your developer tools, you should see that __proto__
(short for prototype
) is listed as Car
. Car
, too, has its own __proto__
, which is listed as Object
. And that Object
prototype
has a number of methods attached to it that can be implemented by any Object in JavaScript.
This is what we refer to as a prototype
chain! Every Object can (and does) inherit behavior from the prototype
chain, even without explicit instantiation in a constructor.
This is all well and good, but how do we implement new, shared behavior for our instances of Car
? The answer is that we modify the Car
's attached prototype
Object directly:
Car.prototype.move = function move(){
// what is "this"?
let prev = this.position;
this.position = this.position + this.speed;
console.log(`${this.type} is moving from ${prev} to ${this.position}`);
}
Now you should notice the following:
civic.move()
andcamry.move()
work without modifying the constructor or re-instantiating our individual cars.the
this
inmove
still refers to the correct instance! This is whythis
is referred to as afunction
's calling context: it matches the Object that calls the function, rather than the Object to which thatfunction
is directly attached (which is, in this case,Car.prototype
)
NOTE: Try creating an Array. Notice that any Array that you create has a
prototype
chain, too. What does that tell us about Arrays?
class
class
For decades, Object-Oriented Programming in JavaScript was built around constructor function
s and direct prototype
modification. This worked, but was very cumbersome to write and maintain when compared to other programming languages. To make OOP a bit easier for web developers, JavaScript now has a class
keyword that allows us to re-write our Car
constructor + prototype
mangling as follows
class Car {
// this constructor should look familiar
constructor(type, speed){
this.position = 1;
this.type = type;
this.speed = speed;
}
// this attaches move to the Car prototype
move() {
let prev = this.position;
this.position = this.position + this.speed;
console.log(`${this.type} is moving from ${prev} to ${this.position}`);
}
}
Now all of our data and logic is organized in one spot once again! But now, perhaps, you're imagining deeper prototype
chains, with class
es inheriting behaviors from other class
es. Now you can more easily implement something like this by using the extend
keyword. Let's try to create a class of Dragster
that inherits some behavior from the more general Car
class, like so:
class Dragster extends Car {
constructor(speed){
// super calls the constructor from Car
super("dragster", speed)
}
pitStop(){
console.log(`Making a pit stop at ${this.position}`);
}
}
const dragster = new Dragster(100);
dragster.move(); // still works!
dragster.pitStop(); // has access to same calling context
Try a few more examples with other categories of things that we might represent abstractly with Objects and class
es. Maybe Users, Actions, Components, or Forms... the possibilities are endless!
Portfolio Project 1
Store
and uni-directional data flow
Store
and uni-directional data flowUp to this point, we've been using a Plain Ol' JavaScript Object (sometimes called a "POJO") to hold all of our application state. This is an important pattern that we should continue to use! Unfortunately, the rules that govern how that state changes is pretty lax, and is scattered throughout the application. Let's refactor that state into a class
that manages both application data and how that application data is modified.
Instead of
export
-ing a number of different pieces of our state tree fromstore/index.js
, let'sexport
a singleclass
calledStore
by default. Something like:export default class Store {};
Next, we'll need to create a
constructor
that bundles up all of the pieces of state that we were previously exporting into an internalstate
property, e.g.:export default class Store { constructor(){ this.state = { Home: Home, Blog: Blog, // etc etc } } }
Now we should be able to replace our
import * as State
line in our projects's mainindex.js
file with a singleimport Store from './store'
. Then we can create a new store with:const store = new Store();
We could, at this point, access
store.state
directly. But it's better if we restrict direct access to thestate
if we can. This will make our implementation ofstate
easier to change later if we want, decoupling our state from the use of that state in our application. To do that, we're going to implement a pattern that we've seen before when dealing withevent
s called the Listener pattern. One way to think about this pattern is to say that our application will listen for changes in state. Let's create an Array of listeners managed by our Store like so:export default class Store { constructor(){ this.listeners = []; this.state = { // same state stuff }; } addStateListener(listener){ // listener should be a function this.listeners.push(listener); } }
Thinking about how listeners work, it makes sense that render is the primary listener of application state, i.e. when state updates, we want to re-render! So add the following to the bottom of your main
index.js
file before therouter
is configured or activated:store.addStateListener(render); // router stuff here
At this point, we have a state to update, and we have a way of adding functions that should be called when that state updates, but we don't yet have a way of updating that state. The way that we're going to update state is with a special method called
dispatch
. The idea here is that we're going to dispatch an action that will modify our application state and call our listeners with our newly-updated state:export default class Store { constructor { this.listeners = []; this.state = { // state stuff }; } addStateListener(listener){ this.listeners.push(listener); } dispatch(reducer){ // a reducer is a pure function // it takes in state and returns a new state this.state = reducer(this.state); // call each listener with updated state this.listeners.forEach(listener => listener(this.state)); } }
Now, every change that we make to state can be done through
store
'sdispatch
method! The only requirement is that we pass in a validreducer
, where areducer
is a purefunction
that takes in a state and returns a new state. SohandleRoute
might be refactored into something like this:function handleRoute(params) { store.dispatch(state => assign(state, { active: params.page })); };
You should now be able to represent every change in your application as a sreducer
function
passed to store.dispatch
as an argument. See if you can make that work across you portfolio project!
Last updated
Was this helpful?