By the end of this lesson students should be able to:
- Demonstrate a use case that explains prototypal inheritance
- Demonstrate what kind of flexibility prototypal inheritance gives programmers
In this lesson we are going to learn about a type of inheritance called Prototypal Inheritance. We are going to see how with prototypes we can share functionality between related objects. We are going to see how with Object.create we can easily set up prototypes and how we can extend prototypes to easily add functionality.
- Inheritance
- Prototype
- Prototypal Inheritance
- Prototype Chain
Object.create()
Object.getPrototypeOf()
In javascript we can create Objects to group together related data and functions. For example we might have a Steve
object that looks like this:
var Steve = {
firstName: "Steve",
lastName: "Jones",
fullName: function(){
return this.firstName + " " + this.lastName;
}
}
But creating objects like this would soon be cumbersome if we had a lot of different people. We'd have to write that fullName
function over and over again for each person which would not only be error prone but super boring. Rather than create a unique set of properties for every person, we can create an abstract object that describes a person in general:
var Person = {
firstName: "",
lastName: "",
fullName: function(){
return this.firstName + " " + this.lastName;
}
}
Notice how the firstName
and lastName
are now empty strings while the fullName
function stays exactly the same as it was before. Now to create a Steve
object all we have to do is create a new object based on our Person
object and then set the first and last name. We use the function Object.create()
to do just that.
var Steve = Object.create(Person);
Steve.firstName = "Steve";
Steve.lastName = "Jones";
And now if we call Steve.fullName()
It'll just work without us having to re-write it. This isn't super useful by itself but if we want to create hundreds or thousands of users we can follow this same pattern and never have to worry about re-writing the fullName
function. Let's create one more person:
var Molly = Object.create(Person);
Molly.firstName = "Molly";
Molly.lastName = "Heath";
And now Molly.fullName()
and Steve.fullName()
both work as we'd expect.
In computer programming, we call this kind of code reuse Prototypal Inheritance. The Steve
and Molly
objects can be described as inheriting from the Person
object. When we used Object.create(Person)
, it set up Molly
and Steve
to inherit from Person
. Practically, this means that any functions or properties we defined on Person
are available to anything that inherits from it. So even thought we didn't specify a fullName
function on Steve
or Molly
we can access the function as though we had.
When we then set a property directly on Steve
like firstName
and lastName
, it creates those properties directly on Steve
and doesn't modify Person
. This is what allows us to create many different objects that inherit from Person
each with their own unique first and last names but sharing the same fullName
function. If we check person we can see that it remains unchanged from when we originally defined it.
> Person
[object Object] {
firstName: "",
fullName: function (){
return this.firstName + " " + this.lastName;
},
lastName: ""
}
In javaScript, every object, like our Steve
object, has what's called a prototype. An object's prototype is the place that it inherits code from. With this information it's pretty easy to reason that Person
is Steve
and Molly
s prototype. This is because, Person
is where Steve
inherits code from. We can check what Steve
and Molly
s prototypes are by passing them to the function Object.getPrototypeOf()
.
> Object.getPrototypeOf(Steve) === Person;
true
> Object.getPrototypeOf(Molly) === Person;
true
With Prototypal Inheritance it is super easy to extend prototypes to add additional functionality. Steve
, Molly
, and Person
are just regular old JavaScript objects in every sense. And since they are regular old JavaScript objects we can add and remove properties just like we would any other JavaScript object. Let's say we now wanted to give every Object that inherits from Person
a default location
of "Denver". So we want to be able to say Steve.location
and Molly.location
and have them return "Denver" by default. Since Steve
and every other Object that inherits form Person
simply checks itself for a location
property, and if it can't find one, will check it's prototype for a location
property. We simply have to specify the location
property on Person
and it will immediately be available to Steve
, Molly
and any other object that inherits from Person
.
Person.location = "Denver";
Now both, Steve.location
and Molly.location
will be "Denver".
> Steve.location;
"Denver"
> Molly.location;
"Denver"
We can easily change Molly
s location while not affecting Steve
s
Molly.location = "New York";
> Molly.location
"New York"
> Steve.location
"Denver"
> Person.location
"Denver"
Now Molly
has a location of "New York" while Steve
still has the default location of "Denver". Another way to think of this, is that the Molly
Object has it's own "location" property, so when we call Molly.location
we get it directly from the Molly
Object. The Steve
Object on the other hand doesn't have a location
property directly on it. So when we call Steve.location
and the property isn't found, JavaScript will check Steve
s prototype, and since Steve
's prototype is Person
and Person
has a location
property, JavaScript will return Person.location
when we call Steve.location
. This checking of the the direct object and then the prototype the essence of Prototypal Inheritance.
##Dopplegangers
Say we wanted to create an evil twin of Steve that is the same in every way, except he's evil. The first thing we can do is use Object.create
passing it in Steve
to create an Object with Steve
as it's prototype.
var evilSteve = Object.create(Steve);
All Object.create does is take an object as it's parameter and set that object as the prototype of a new empty object, and then return that new object. So evilSteve
right now is just an empty object, with Steve
as it's prototype.
> Object.getPrototypeOf(evilSteve) === Steve
true
We want to override the fullName
function so we can tell that evilSteve
is actually evil. We want to use the old fullName
function but we just want to prepend the word "Evil" to it's return value. To do this, we can simply set a fullName
property on evilSteve
to be a new function that calls the fullName
function of evilSteve
s prototype.
evilSteve.fullName = function(){
return "Evil " + Object.getPrototypeOf(this).fullName()
}
Now if we call evilSteve.fullName()
we'll get "Evil Steve Jones" and if we call Steve.fullName()
we'll still get regular old "Steve Jones".
Since evilSteve
inherits from Steve
and Steve
inherits from Person
, we can say that evilSteve
also inherits from Person
. That means that evilSteve
also has a default location of "Denver".
> evilSteve.location
"Denver"
Another way of saying this same thing, is that evilSteve
has Steve
as its prototype and Steve
has Person
as its prototype. This flow of prototypes from evilSteve
through to Person
is called the Prototype Chain. Every single object in Javascript has a Prototype Chain and every prototype chain ends in null
. So if you have an object like Molly
or evilSteve
or even an empty object {}
, you can pass it to Object.getPrototypeOf()
and see what it's prototype is. Then you can send the return value to Object.getPrototypeOf()
and continue up the prototype chain until you eventually hit null.
> var proto = Object.getPrototypeOf({});
> proto === Object.prototype;
true
> Object.getPrototypeOf(proto) === null
true
JavaScript has no real notion of classes or instances like in Ruby. In javaScript there are only Objects and every Object has exactly one direct prototype. Prototypes themselves are always also just Objects and so they too have a prototype. In ruby we talk about creating instances of a class and of classes extending other classes. In JavaScript we are simply creating objects, the inheritance comes from the prototype chain. Any Object can have any other Object as it's prototype. Prototypal and Classical inheritance are very different ways of achieving a similar goal of code reuse.