Prototypes
Where did we Leave Off?
At a cliffhanger!
(Actually… doing some magic tricks)
Pulling Properties Out of Thin Air!
Let's check out this code. →
// an empty object
const hat = {};
// printing out a properties
console.log(hat.toString);
// calling a method
console.log(hat.toString());
- Have we defined any properties on the object,
hat
, yet? → - What do we expect the output to be? →
[function: toString]
[object object]
So Where Did Those Properties Come From?
const hat = {};
console.log(hat.toString); // a function
console.log(hat.toString()); // returns object
"Inherited" Properties
All objects have a link to another object that's called its [[prototype]]
.
- note that
[[prototype]]
means the concept, prototype not the actual syntax (confusingly, there are properties and objects in JavaScript that are namedprototype
but are not exactly the concept[[prototype]]
) - objects are basically just a collection of properties
- when an objects gets a request for a property that it doesn't have, the object's prototype is searched for that property
[[prototype]]
objects have [[prototype]]s as well!- searching goes on up the chain of prototypes until
- the property is found
- an object with a
null
prototype is reached / the last object in the chain:Object.prototype
Object.prototype
The top level prototype is Object.prototype:
- all objects in JavaScript are descended from
Object
- all objects inherit methods and properties from
Object.prototype
Object.prototype
's [[prototype]] isnull
Let's do some exploring. →
- use
Object.getPrototypeOf(obj)
- this gives back the [[prototype]] of the passed in object
obj
…
console.log(
Object.getPrototypeOf({}) == Object.prototype);
#true
console.log(
Object.getPrototypeOf(Object.prototype));
#null
Object.prototype Continued Some More
What do you think the [[prototype]] of Array.prototype is?→
console.log(
Object.getPrototypeOf(Array.prototype) == Object.prototype);
#true
Object.prototype
is at the top of the prototype chain (it's the last object checked for properties)- it provides a bunch of methods and properties to all JavaScript objects
toString()
hasOwnProperty()
(we've seen this before!)
Using Object.create
Object.create
- creates a new object with the specified [[prototype]] object and properties
// our "template" object
const protoWerewolf = {
description: 'hairy',
howl: function(thing) {
console.log('The werewolf howls at the ' + thing + '.');
}
};
// make a new werewolf with Object.create
const sadWerewolf = Object.create(protoWerewolf);
sadWerewolf.mood = 'sullen';
sadWerewolf.howl('moon');
(It turns out, for inheritance, it's common to use Object.create(MyObj.prototype) … we'll see why later)
Constructors
Another way to create an object with a particular prototype is to use a constructor.
- a constructor is basically just a function with the
new
keyword in front of it- it's a convention to make the first letter of a constructor uppercase
- this helps distinguish between regular functions and constructors
- an instance is an object created by using
new
- a constructor's
this
object is bound to a fresh, empty object - this is the object that's returned from invoking the constructor with
new
(unless the constructor explicitly returns a different object)
Constructors Continued
In the code below, both sadWerewolf
and partyWerewolf
are instances of Werewolf
. Note that:
- a property is added to the constructor's
this
object this
is the object that's returned after callingnew Werewolf
function Werewolf(mood) {
this.mood = mood;
}
const sadWerewolf = new Werewolf('sad');
const partyWerewolf = new Werewolf('partying');
console.log(partyWerewolf.mood);
You can think of the above constructor as doing the following when invoked with new…
function Werewolf(mood) {
// this = {}
this.mood = mood;
// return this
}
Let's try adding some more properties to this
.→
Constructors, Prototype
All constructors have a property named prototype
.
- the default value of a constructor's prototype is a plain, empty object that derives
from Object.prototype
- every instance created with the constructor will have that object as its actual prototype
- note that there's a difference between the constructor's prototype property that's used to set an instance's prototype versus the constructor's actual prototype… can you guess what that is? →
Function.prototype
- for example, we could use a constructor's prototype to add a howl method on every instance of Werewolf
Werewolf.prototype.howl = function(thing) {
console.log('The werewolf howls at the ' + thing + '.');
}
sadWerewolf.howl('moon');
partyWerewolf.howl('bowl of chips');
Something Happened! Just one Prototype
When we added a property to the constructor's prototype, something unusual happened! How were the instances of that constructor affected? →
The instances immediately had access to the new property, even though they were instantiated before the prototype was set.
- all instances share that prototype object
- so… when a property is looked up on any of those instances and isn't found
- it looks at that shared prototype object
- it's typical for a prototype object to only contain methods
Searching for a Property
When a property is requested from an object, where are the places that the property is searched for?
- the object itself
- the object's prototype
- the object's prototype's prototype
- and so on up the prototype chain up until
Object.prototype
Overriding Properties
If you add a property directly to an object, it is added to the object itself, not the object's prototype. What's the output of the following code? →
Werewolf.prototype.clothing = 'tattered shirt';
console.log(partyWerewolf.clothing);
partyWerewolf.clothing = 'backwards cap';
console.log(partyWerewolf.clothing);
console.log(sadWerewolf.clothing);
tattered shirt
backwards cap
tattered shirt
Overriding Properties Continued
Again, when you add a property to an object, that property is added to the object itself…
- (not the prototype)
- this happens regardless of whether or not there's already a property with the same name in the prototype
- if there is a property with the same name in the prototype, it is masked or overridden by the new property
- note that the prototype itself is not changed
Where Did Those Properties Come From?
Let's break down all of the properties of our partyWerewolf
object and determine where they came from →
partyWerewolf properties
=====
from partyWerewolf object
-----
clothing: backwards cap
mood: partying
from Werewolf.prototype
-----
clothing: tattered shirt (masked)
howl: (function)
from Object
-----
toString: (function)
etc.
Common Pattern for Inheritance
A common pattern for implementing inheritance is to: →
- use a fresh object that has the prototype set to the parent constructor's prototype property (WAT?)
- as the child constructor's prototype property
- … which can be done with
Object.create
Using our parent constructor, Werewolf
function Werewolf(mood) {
this.mood = mood;
}
Werewolf.prototype.howl = function(thing) {
console.log('The werewolf howls at the ' + thing + '.');
}
Create a constructor for a space werewolf (!!!) by setting its prototype to a new object who's prototype is Werewolf.prototype
function SpaceWerewolf() {}
SpaceWerewolf.prototype = Object.create(Werewolf.prototype);
This isn't quite complete, though. →
Common Pattern for Inheritance
A common pattern for implementing inheritance is to: →
- use a fresh object that has the prototype set to the parent constructor's prototype property (WAT?)
- as the child constructor's prototype property
- … which can be done with
Object.create
Using our parent constructor, Werewolf
function Werewolf(mood) {
this.mood = mood;
}
Werewolf.prototype.howl = function(thing) {
console.log('The werewolf howls at the ' + thing + '.');
}
Create a constructor for a space werewolf (!!!) by setting its prototype to a new object who's prototype is Werewolf.prototype
function SpaceWerewolf() {}
SpaceWerewolf.prototype = Object.create(Werewolf.prototype);
This isn't quite complete, though. →
Inheritance Continued
In the previous implementation, there's actually some stuff missing when we create a SpaceWerewolf
. What's missing from the previous implementation that results in incomplete inheritance? →
- the prototype only contains methods
- what about properties set from the constructor (like
mood
)?
const w = new SpaceWerewolf();
console.log(mood)
Calling Super
Hm. The constructor, Werewolf
, sets the property, mood
…
- if only we can execute the parent constructor (you know… like call
super
in Java). - but we can, how? →
- use
call
function SpaceWerewolf(mood) {
Werewolf.call(this, mood);
}
One Last Detail, Constructor Property
All object's have a property named constructor
. constructor
is the function that was used to create the instance's prototype.
const a = [];
console.log(a.constructor); // [Function: Array]
So we should probably set that on our child constructor's prototype property explicitly so that all objects created from SpaceWerewolf
have that as its constructor.
SpaceWerewolf.prototype.constructor = SpaceWerewolf;
All Together
function Werewolf(mood) {
this.mood = mood;
}
Werewolf.prototype.howl = function(thing) {
console.log('The werewolf howls at the ' + thing + '.');
}
function SpaceWerewolf(mood) {
Werewolf.call(this, mood);
}
SpaceWerewolf.prototype = Object.create(Werewolf.prototype);
SpaceWerewolf.prototype.constructor = SpaceWerewolf;
const w = new SpaceWerewolf('in space');
console.log(w.mood);
console.log(w.constructor);
Prototype: An Example
Check out the following example… →
function Monster() {
this.scary = true;
}
Monster.prototype.boo = function() { console.log('Boo!');}
function Werewolf(mood) {
Monster.call(this);
this.mood = mood;
}
Werewolf.prototype = Object.create(Monster.prototype);
Werewolf.prototype.constructor = Werewolf;
Werewolf.prototype.howl = function(thing) {
console.log('The werewolf howls at the ' + thing + '.');
}
Example Continued
What would the output be if the following code were run… →
const sadWerewolf = new Werewolf('sad');
const partyWerewolf = new Werewolf('partying');
partyWerewolf.scary = false;
console.log(sadWerewolf.scary);
console.log(partyWerewolf.scary);
partyWerewolf.boo();
true
false
Boo!
Some notes on the example:
- to inherit properties from Monster…
- we set our Werewolf constructor's prototype to a fresh object with
Monster.prototype
as the prototype - we called the "super" constructor
- we set our Werewolf constructor's prototype to a fresh object with
const sadWerewolf = new Werewolf('sad');
- …which is why scary was found in the prototype chain for
sadWerewolf
Own Property
What if we only want the properties that were explicitly set on our object, rather than including inherited ones. →
We could use the hasOwnProperty
method that every object inherits from Object.prototype
!
console.log('party\n-----');
for (const p in partyWerewolf) {
if (partyWerewolf.hasOwnProperty(p)) {
console.log(p + ': ' + partyWerewolf[p]);
}
}
console.log('\n');
console.log('sad\n-----');
for (const p in sadWerewolf) {
if (sadWerewolf.hasOwnProperty(p)) {
console.log(p + ': ' + sadWerewolf[p]);
}
}
What Instance do I Have?
If you have an object, and you'd like to know what constructor it came from, you can use the instanceof
operator.
- instance on left
- constructor on right
What do you think the following code will print out? →
console.log(myCar instanceof Car);
console.log(myCar instanceof Bike);
true
false
(in actuality instance of checks if an object has in its prototype chain the prototype property of a constructor)
Example ES6 Class
These two bits of code both produce a function called HttpRequest
! →
ES6 class:
class HttpRequest {
}
ES5 constructor:
function HttpRequest() {
}
Both result in the same output when used in the following manner:
const req = new HttpRequest();
console.log(HttpRequest);
console.log(typeof req.constructor);
console.log(req.constructor.name);
Constructors
ES6 style classes allow for a constructor to be defined as follows:
- within the class definition, create a function called constructor
- no function keyword is required
- the constructor has access to
this
which represents the instance that is created
class HttpRequest {
constructor(method, url) {
this.method = method;
this.url = url;
}
}
The above code is mostly the same as this ES5 function that can be used as a constructor:
function HttpRequest(method, url) {
this.method = method;
this.url = url;
}
We'll see later that subclass constructors must call super before using this
.
Methods in ES5
In ES5, to add a method to the prototype, we'd have to do something like this:→
function HttpRequest(method, url) {
this.method = method;
this.url = url;
}
HttpRequest.prototype.makeRequest = function() {
return this.method + ' ' + this.url + ' HTTP/1.1';
}
Methods in ES6
In ES6, we can define methods directly in the class definition, and they will show up in the instances' prototype →
class HttpRequest {
constructor(method, url) {
this.method = method;
this.url = url;
}
makeRequest() {
return this.method + ' ' + this.url + ' HTTP/1.1';
}
}
- note that there are no commas between method and constructor definitions
- again, you do not have to use the keyword,
function
- methods, of course, can reference
this
, and if the method is called within the context of an instance, thenthis
refers to the instance
ES6 Methods Continued
Note that creating these methods in ES6 style classes is actually just adding to the prototype! →
const req = new HttpRequest('GET', 'http://foo.bar/baz');
console.log(req.makeRequest());
console.log(Object.getPrototypeOf(req).makeRequest);
Inheritance
Use extends
to inherit from a class! (read: set up a prototype chain)
class Element {
constructor(name) {
this.name = name;
}
}
class ImgElement extends Element {
// make sure to call super before using this
// within subclass
constructor(url) {
super('img');
this.url = url;
}
}
const img = new ImgElement('http://foo.bar/baz.gif');
console.log(img.name);
console.log(img.url);
Calling Super Constructor
In the previous example, super
was used to call the base class constructor. →
super
must be called in your subclass constructor if…- you use
this
within your constructor - (it's essentially initializing
this
properties the way that the superclass would super
must be called before using this within a subclass
High Level Summary
Every object in JavaScript links to another object called its [[prototype]]. →
- when a property cannot be found in the original object, it goes up the prototype chain
- objects can be given prototypes in 3 ways:
Object.create
- constructor functions
- ES6 Classes
- (there's also something called
__proto__
that allows direct access to a [[prototype]], but its use is discouraged)