Skip to main content

All about prototypes

Prototype inheritance

Every object have a special hidden property [[Prototype]] it is either null or a reference to another object.

Whenever you read a property from object and if it is missing, JavaScript will automatically take it from the prototype.

Setting prototype

One of the way to set the prototype is  to use the special name __proto__:

let animal = {
	eat: true
};

let rabbit = {
	jump: true
};

rabbit.__proto__ = animal;

console.log(rabbit.eat); // true, taken from the prototype animal
console.log(rabbit.jump); // true

If animal has any useful method, then since it is a prototype to the rabbit it is able to call it directly as well!

let animal = {
  eats: true,
  walk() {
    console.log("Walking");
  }
};

let rabbit = {
  jumps: true,
  __proto__: animal // Another way of setting the prototype
};

// walk is taken from the prototype
rabbit.walk(); // Walking
Limitation

The only rules on assigning prototype is that there should be no circular references, and the value of __proto__ should be either an object or null all other types are simply ignored and have no effect.

Modifying prototype state

What if we did something like this?

let user = {
	name: "Ricky"
    last: "Lu"
    
    get fullName() {
    	return this.name + " " + this.last;
	}
    
    set fullName(value) {
    	[this.name, this.last] = value.split(" ");
	}
};

let admin = {
	__proto__: user,
    isAdmin: true
};

console.log(admin.fullName); // Ricky Lu, this works as expected

admin.fullName = "Another person"; // Now what happens to user.name and user.last?

admin.fullName // Another perons
user.fullName // Ricky Lu, it stays! The state of user is protected

As you can see if you tried to modify the admin's name by calling the prototype's setter function the state change will be enforced on admin and not user! Even though you are calling user's setter method. This is because this always binds to the object that it is called on, and in this case, it is assigning admin.name and admin.last to be "Another" and "person" respectively. user is unchanged.

Even if you do something like this:

let user = {
	name: "Ricky"
    last: "Lu"
};

let admin = {};

admin.name = "Another";
admin.last = "person";

user -> Will still be "Ricky Lu"
admin -> Will be "Another person", because it is assigning name and last property to admin

This behavior is needed because you wouldn't want to modify the prototype's state if multiple object has it as it's prototype, the changes will be untraceable and should be on the object itself not prototype!

for...in loop

Using for...in it will iterate over the inherited properties as well if it is enumerable, however, Object.keys(obj) will only return the keys this object has itself, not inherited ones:

let animal = {
  eats: true
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

// Object.keys only returns own keys
console.log(Object.keys(rabbit)); // jumps

// for..in loops over both own and inherited keys
for(let prop in rabbit) console.log(prop); // jumps, then eats

Native prototypes

These are the built-in prototypes that all of the objects inherits from.

Object.prototype

This is the default prototype that any object created inherits from. And itself's prototype is just null because there is no one above it.

This object has some default method that all object contains toString, hasOwnProperty,...

hasOwnProperty(prop)

Return true/false depending on whether or not the object has that particular property itself, and not inherited.

Interesting aside

If you set a property to an object and marked it as enumerable: false, and you console.log the object, that property will not show up because it is not iterated over using for...in loop!

F.prototype

When writing constructor functions, i.e. functions that are called with new F(), where F is a constructor function we can take advantage and use F.prototype to do prototype inheritance as well.

If we assigned an object to F.prototype then when new F() is invoked, the object that is created will have its __prototype__ pointed to the object that we have assigned. So it is another way that we can do prototype inheritance, instead of setting it manually after the object is created using {...}.

let animal = {
	eat: true
};

function Rabbit(name) {
	this.name = name;
}

Rabbit.prototype = animal;

let rabbit = new Rabbit("White"); // rabbit.__proto__ == animal

console.log(rabbit.eat); // true

Keep in mind that F.prototype is only used when new F() is called, so if you decide to change it prototype after you created the object, future object created using the constructor function will have the newer prototype, but existing object keep the old one.

This only concerns with constructor methods, nothing else.