Skip to content

Ultimate JavaScript Mastery: Prototypes

Inheritance

Inheritance is a mechanism for sharing code.

Common code is put into a super class and inherit to reuse code into a subclass.

There are two types of inheritance: Classical vs. Prototypical Inheritance.

Prototypes and Prototypical Inheritance

Every object in Javascript (except one, the common object ancestor), has a prototype or parent. An object 'inherits' all the members of the its prototype.

Prototypes themselves have a prototype and are thus chained together. An object 'inherits' all the members members of the entire prototype chain.

Every object created in Javascript inherits from a common object ancestor. This is the last prototype in an object's prototype chain. This common ancestor does not itself have a prototype.

When accessing a object's member, first the object is checked. If the member is not found on the object, then its prototype is checked. If not found there, the prototype's prototype is checked and so on through the prototype chain.

Use Object.getPrototypeOf(obj) to get the prototype of any object or Object.setPrototypeOf(obj). The __proto__ property is the deprecated way getting and setting an object's prototype.

let x = {}
let y = {}

console.log(Object.getPrototypeOf(x));
console.log(Object.getPrototypeOf(x) === Object.getPrototypeOf(y)); // true

Multilevel Inheritance

Objects created by a given constructor will have the same prototype.

function Person(name) {
  this.name = name
}

person1 = new Person('Bob Smith');
person2 = new Person('Jane Doe');

console.log(getPrototypeOf(person1) === getPrototypeOf(person2));

Property Descriptors

Properties have descriptors that define if the object is ritable, enumerable, or configurable.

Use Object.getOwnPropertyDescriptor(obj, propertyName) to get a property's descriptor object.

let person = { name: 'James' };
let nameDescriptor = Object.getOwnPropertyDescriptor(person, 'name');
console.log(nameDescriptor);
// {value: 'James', writable: true, enumerable: true, configurable: true}

let objectBase = Object.getPrototypeOf(person);
let toStringDescriptor = Object.getOwnPropertyDescriptor(objectBase, 'toString');
console.log(toStringDescriptor);
// {writable: true, enumerable: false, configurable: true, value: ƒ}

The descriptor can be set with Object.defineProperty(obj, propertyName, descriptor). The default for writable, enumerable, and configurable are all true.

let person = { name: 'James' };

Object.defineProperty(person, 'name', {
  writable: false, // the property is now read only
  enumerable: false, // the property won't show up in Object.keys or in for... in loops
  configurable: false, // the property won't be able to be deleted
  // get: <<getter function>>,
  // set: <<setter function>>,
});

// Writable
person.name = 'Bob'; // not being writable doesn't throw an error
console.log(person.name); // James

// Enumerable
console.log(Object.keys(person)); // []
for ( let key in person) console.log(key, person[key]); // nothing will be logged
console.log(person); // { name: 'James' } - name still shows up here

// Configurable
delete person.name;
console.log(person); // { name: 'James' } - still has the name property

Constructor Prototypes

Set a constructor function's prototype property to define what object another object should inherit from.

function Creature(name) {
  this.name = name
}

function Person(name) {
  this.prototype = Creature
  this.move = function
}

function Fish(name) {
  this.prototype = Creature

}

function Bird(name) {
  this.prototype = Creature
}

Prototype vs Instance Members

function Person(name) {
  this.name = name
}

Person.prototype.firstName = function() { return this.name.split(' ')[0] }
Person.prototype.lastName = function() { return this.name.split(' ')[1] }

const p1 = new Person('James Couball');
const p2 = new Person('Bob Smith');

// Methods defined in the prototype can be called
console.log(p1.firstName()); // James
console.log(p1.lastName()); // Couball

// The prototypes are the same for p1 and p2:
console.log(Object.getPrototypeOf(p1) === Object.getPrototypeOf(p2)); // true

// A method in the prototype can override an ancestor method
console.log(p1.toString()); // [object Object]
Person.prototype.toString = function() { return `A person named ${this.name}` }
console.log(p1.toString()); // A person named James Couball

Iterating Instance and Prototype Members

Object.keys only returns instance members. This does not include members defined by this object's prototype chain.

for... in loop returns all members defined in the instance and by the prototype chain.

The hasOwnProperty(propertyName) (defined in the root object) returns true if the propertyName is an instance member.

function Person(name) {
  this.name = name
}

Person.prototype.firstName = function() { return this.name.split(' ')[0] }
Person.prototype.lastName = function() { return this.name.split(' ')[1] }

const p1 = new Person('James Couball');

console.log(p1.hasOwnProperty('firstName')); // false

Avoid Extending the Built-in Objects

Do not modify objects you do not own!

Avoid doing something like this that extends a built-in object:

Array.prototype.shuffle = function() {
  // ...
}
const array = [1, 2, 3, 4, 5];
array.shuffle();