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

  • 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.
 

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 constructorconstructor 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

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, then thisrefers 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)
 
posted @ 2023-02-04 03:31  M1stF0rest  阅读(26)  评论(0编辑  收藏  举报