Prototypes

CSCI-UA.0480-008

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

rabbit out of a hat

"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 named prototype 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]] is null


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);

console.log(
	Object.getPrototypeOf(Object.prototype)); 

Object.prototype Continued

Object.prototype isn't always an object's direct [[prototype]]; instead, most objects have a [[prototype]] of another object

  • functions derive from Function.prototype
  • arrays derive from Array.prototype


Let's see what happens when we call getPrototypeOf.


function f(x) { return x;}

console.log(
	Object.getPrototypeOf(f) == Function.prototype);

console.log(
	Object.getPrototypeOf([1, 2, 3]) == Array.prototype);

Object.prototype Continued Some More

What do you think the [[prototype]] of Array.prototype is?


console.log(
	Object.getPrototypeOf(Array.prototype) == Object.prototype);
  • 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 calling new 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.

  1. all instances share that prototype object
  2. so… when a property is looked up on any of those instances and isn't found
  3. it looks at that shared prototype object
  4. 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.

Overriding Properties

Why would overriding properties be useful?

  • it allows for convenient way of dealing with objects that have exceptional properties (without having to create an entirely new constructor)
  • while still allowing a default property to be set for all instances
  • let's override the inherited toString() method
    • the string representation of the werewolf object should be [mood] werewolf

console.log(partyWerewolf);
Werewolf.prototype.toString = function() {
	return this.mood + ' werewolf';
};
console.log(partyWerewolf + '');

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
  • const sadWerewolf = new Werewolf('sad');
  • …which is why scary was found in the prototype chain for sadWerewolf

Enumerating Properties Revisited

How do we list every property in an object?


for (const prop in obj) {
	console.log(prop)
}

Let's list every property and value in partyWerewolf and sadWerewolf.


for (const p in partyWerewolf) {
	console.log(p + ': ' + partyWerewolf[p]);
}
for (const p in sadWerewolf) {
	console.log(p + ': ' + sadWerewolf[p]);
}

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)

About prototype and this

There are two ways to set properties automatically when using a constructor. What are they?


function Thing() {
	this.prop1 = 'some value';
}

Thing.prototype.prop2 = 'another value';
  • generally, you would use the prototype method if you only want one version of that object to exist
  • see the SO article.

About Creating Objects

There are a few ways to create objects… what are they?

  • object literal - {}
  • Object.create
  • new


Object.create seems to be gaining some traction.

A Few Things to Remember

What object is at the top of the prototype chain?

Object.prototype is at the top of the prototype chain

What is Object.prototype's prototype?

Its prototype is null.

What are some properties that are inherited from Object.prototype?

toString, hasPropertyOf

And Some More Questions

What does the hasOwnProperty method do?

return true if property was set on actual object rather than inherited

What does the instanceof operator do?

It determines whether the operand on the left is an instance of the operand on the right.

ES6 Classes

In ES6, there's yet another way to create a prototype chain of objects, and that's using a familiar construct, classes.

  • they're syntactic sugar for creating constructor functions and a prototype object with methods
  • (in actuality, everything is still prototypes and constructor functions)

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, then this 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:
    1. Object.create
    2. constructor functions
    3. ES6 Classes
    4. (there's also something called __proto__ that allows direct access to a [[prototype]], but its use is discouraged)