https://javascript.info/prototype-inheritance
Prototype, function, and New
https://scotch.io/tutorials/better-javascript-with-es6-pt-ii-a-deep-dive-into-classes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
"use strict"; function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } // Calling Food with 'new' is a "constructor call", and results in its returning an object const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5); console.log(chicken_breast.protein) // 26 // Failing to call Food with 'new' results in its returning 'undefined' const fish = Food('Halibut', 26, 0, 2); console.log(fish); // 'undefined' |
When you call a function with new, four things happen under the hood:
- A new object gets created (let’s call it O)
- O gets linked to another object, called its prototype
- The function’s this value is set to refer to O
- The function implicitly returns O
We can also rewrite our Food function to work without the new keyword:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
"use strict"; // Eliminating the need for 'new' -- just for demonstration function Food (name, protein, carbs, fat) { // Step One: Create a new Object const obj = { }; // Step Two: Link prototypes -- we'll cover this in greater detail shortly Object.setPrototypeOf(obj, Food.prototype); // Step Three: Set 'this' to point to our new Object // Since we can't reset `this` inside of a running execution context, // we simulate Step Three by using 'obj' instead of 'this' obj.name = name; obj.protein = protein; obj.carbs = carbs; obj.fat = fat; // Step Four: Return the newly created object return obj; } const fish = Food('Halibut', 26, 0, 2); console.log(fish.protein); // 26 |
Everything is straightforward, except the setPrototypeOf.
Prototype Inheritance
Under normal circumstances, all objects in JavaScript — including Functions — are linked to another object, called its prototype.
If you request a property on an object that the object doesn’t have, JavaScript checks the object’s prototype for that property. In other words, if you ask for a property on an object that the object doesn’t have, it says: “I don’t know. Ask my prototype.”
This process — referring lookups for nonexistent properties to another object — is called delegation.
1 2 3 4 5 6 7 8 9 10 11 12 |
"use strict"; // joe has no toString property . . . const joe = { name : 'Joe' }, sara = { name : 'Sara' }; Object.hasOwnProperty(joe, toString); // false Object.hasOwnProperty(sara, toString); // false // . . . But we can call it anyway! joe.toString(); // '[object Object]', instead of ReferenceError! sara.toString(); // '[object Object]', instead of ReferenceError! |
The output from our toString calls is utterly useless, but note that this snippet doesn’t raise a single ReferenceError!
That’s because, while neither joe or sara has a toString property, their prototype does.
When we look for sara.toString(), sara says, I don’t have a toString property. Ask my prototype.
JavaScript, obligingly, does as told, and asks Object.prototype if it has a toString property. Since it does, it hands Object.prototype’s toString back to our program, which executes it.
It doesn’t matter that sara didn’t have the property herself — we just delegated the lookup to the prototype.
In other words, we can access non-existent properties on an object as long as that object’s prototype does have those properties. We can take advantage of this by assigning properties and methods to an object’s prototype, so that we can use them as if they existed on the object itself.
Even better, if several objects share the same prototype — as is the case with joe and sara above — they can all access that prototype’s properties, immediately after we assign them, without our having to copy those properties or methods to each individual object.
This is what people generally refer to as prototypical/prototypal inheritance — if my object doesn’t have it, but my object’s prototype does, my object inherits the property.
In reality, there’s no “inheritance” going on, here.
In class-oriented languages, inheritance implies behavior is copied from a parent to a child.
In JavaScript, no such copying takes place — which is, in fact, one of the major benefits of prototypes over classes because we can dynamically switch to different prototypes and the prototype would “delegate” properties to those objects.
Setting an Object’s Prototype
The function, Object, has a property, called .prototype, which points to any object Object.prototype that you want.
The object, Object.prototype, has a property, called .constructor, which points to a function (Object). Keep in mind that this Object.prototype.constructor property is just a public property of Object.prototype. It is not used in any way in the construction of new objects or resolution of property names.
1 2 3 |
const tootsie_roll = new Food('Tootsie Roll', 0, 26, 0); Object.getPrototypeOf(tootsie_roll) === Food.prototype; // true tootsie_roll.constructor === Food; // true |
You can also change the property to reference other objects. Rabbit.Property references an object. Any object created with new will now have their [[Prototype]] reference object {eats: true}. When we change Rabbit.Property to an empty object {}, further new objects created will then have their [[Prototype]] reference the new prototype object.
Prototype properties will over shadow objects that delegate from them
When both your object and its prototype has the same property, the prototype will have precedence.
1 2 3 4 5 6 7 8 9 10 11 |
function Me() { this.name = this.name || "Dejan"; } function You() { this.name = this.name || "Ivan"; } Me.prototype = new You(); somebody = new Me(); console.log(somebody.name); // Ivan |
Now we add the function “cook” to our prototype, in which we can then use.
1 2 3 4 5 6 7 8 |
"use strict"; Food.prototype.cook = function cook () { console.log(`${this.name} is cooking!`); }; const dinner = new Food('Lamb Chops', 52, 8, 32); dinner.cook(); // 'Lamb Chops are cooking!' |
Further Reading – Prototype Chain Details
In programming, we often want to take something and extend it.
In JavaScript, objects have a special hidden property [[Prototype]] (as named in the specification), that is either:
– null
– references another object. (all properties/methods of that object will be inherited)
That object is called “a prototype”:
When we want to read a property from object, and it’s missing, JavaScript automatically takes it from the prototype.
The property [[Prototype]] is internal and hidden, but there are many ways to set it.
One of them is to use __proto__, like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let animal = { eats: true }; let rabbit = { jumps: true }; // animal is the prototype of rabbit // rabbit inherits from animl rabbit.__proto__ = animal; console.log(rabbit.eats); // true console.log(rabbit.jumps); // true |
// So if animal has a lot of useful properties and methods,
// then they become automatically available in rabbit.
// Such properties are called “inherited”.
__proto__ IS NOT the same as [[Prototype]].
[[Prototype]]’s getter/setter is __proto__
If the subclass does not have the property/method, then JavaScript automatically takes it from the parent class.
1 2 3 4 5 6 7 8 9 10 11 12 |
let animal = { eats: true }; let rabbit = { jumps: true }; rabbit.__proto__ = animal; // (*) // we can find both properties in rabbit now: console.log( rabbit.eats ); // true (**) console.log( rabbit.jumps ); // true |
Then, when console.log tries to read property rabbit.eats (**), it’s not exist in rabbit, so JavaScript follows the [[Prototype]] reference and finds it in animal (look from the bottom up).
Here we can say that animal is the prototype of rabbit or rabbit prototypally inherits from animal.
So if animal has a lot of useful properties and methods, then they become automatically available in rabbit. Such properties are called “inherited”.
Inherited Method
If we have a method in animal, it can be called on rabbit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let animal = { eats: true, walk() { alert("Animal walk"); } }; let rabbit = { jumps: true, __proto__: animal }; // walk is taken from the prototype rabbit.walk(); // Animal walk |
Longer Prototype Chain
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
let animal = { eats: true, walk() { alert("Animal walk"); } }; let rabbit = { jumps: true, __proto__: animal }; let longEar = { earLength: 10, __proto__: rabbit } // walk is taken from the prototype chain longEar.walk(); // Animal walk alert(longEar.jumps); // true (from rabbit) |
getter setter in prototype inheritance
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
let user = { name: 'John', surname: 'Smith', set fullName(value) { console.log('set fullName'); [this.name, this.surname] = value.split(' '); }, get fullName() { console.log('get fullName'); return `${this.name} ${this.surname}`; } }; let admin = { __proto__: user, isAdmin: true }; console.log(admin.fullName); // John Smith (*) // setter triggers! admin.fullName = 'Alice Cooper'; // (**) console.log(admin.fullName); |
An interesting question may arise in the example above: what’s the value of this inside set fullName(value)?
Where the properties this.name and this.surname are written: user or admin?
The answer is simple: this is not affected by prototypes at all.
No matter where the method is found: in an object or its prototype. In a method call, this is always the object before the dot.
So, the setter actually uses admin as this, not user.
That is actually a super-important thing, because we may have a big object with many methods and inherit from it.
Then we can run its methods on inherited objects and they will modify the state of these objects, not the big one.
For instance, here animal represents a “method storage”, and rabbit makes use of it.
The call rabbit.sleep() sets this.isSleeping on the rabbit object:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// animal has methods let animal = { walk() { if (!this.isSleeping) { alert('I walk'); } }, sleep() { this.isSleeping = true; } }; let rabbit = { name: 'White Rabbit', __proto__: animal }; // modifies rabbit.isSleeping rabbit.sleep(); console.log(rabbit.isSleeping); // true console.log(animal.isSleeping); // undefined (no such property in the prototype) |
If we had other objects like bird, snake etc inheriting from animal, they would also gain access to methods of animal. But this in each method would be the corresponding object, evaluated at the call-time (before dot), not animal. So when we write data into this, it is stored into these objects.
As a result, methods are shared, but the object state is not.
In JavaScript, all objects have a hidden [[Prototype]] property that’s either another object or null.
We can use obj.__proto__ to access it (there are other ways too, to be covered soon).
The object referenced by [[Prototype]] is called a “prototype”.
If we want to read a property of obj or call a method, and it doesn’t exist, then JavaScript tries to find it in the prototype list, from its immediate parent, and on up.
Write/delete operations work directly on the object, they don’t use the prototype (unless the property is actually a setter).
If we call obj.method(), and the method is taken from the prototype, the obj’s “this” still references the obj. So methods always work with the current object even if they are inherited.
Special
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
let animal = { jumps: null }; let rabbit = { __proto__: animal, jumps: true }; alert( rabbit.jumps ); // ? (1) delete rabbit.jumps; alert( rabbit.jumps ); // ? (2) delete animal.jumps; alert( rabbit.jumps ); // ? (3) |
rabbit.jumps overrides its prototype reference’s jumps. Thus we get true.
Then we delete rabbit.jumps. This deletes the rabbit’s jumps (the one that overrides the prototype’s jumps).
Since the jump that overrides the prototype’s jump is now deleted, “rabbit.jumps” then refers to its prototype’s jumps, which comes out to be null.
We delete animal.jumps.
Since rabbit has no jumps, and then it goes to its prototype and sees no jumps, then jumps would be undefined, because undefined is assigned to anything that is undeclared.
Including Prototype, properties that describe the state of a particular object, is written into the object
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
let hamster = { stomach: [], eat(food) { this.stomach.push(food); } }; let speedy = { __proto__: hamster }; let lazy = { __proto__: hamster }; // This one found the food speedy.eat("apple"); alert( speedy.stomach ); // apple alert( lazy.stomach ); // apple |
Speedy derives from hamster.
lazy derives from hamster.
Stomach, since it is a property that describes the state of an object, is written into itself.
Thus, that’s why when Speedy calls stomach, it does this:
1) accesses this.stomach, stomach here belongs to hamster.
2) calls push on this.stomach. It pushes a string into array stomach.
When Lazy calls stomach, it does the same thing as above.
Thus, that’s when a parent class has a property that describes the state of an object, all its deriving objects will share that property.
In order to fix this you can do 2 things:
1)
Essentially, we work on “this”, the child object that is deriving.
Then, we create a new array with the food name, and references the child object’s property to it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
let hamster = { set eat(foodName) { if (this.stomachContents) { this.stomachContents.push(foodName); } else { this.stomachContents = [foodName]; } // create stomachContents property for "this" // then assign the property to a new array with foodName as its 1st element. }, get stomach() { return this.stomachContents; } }; let speedy = { __proto__: hamster }; let lazy = { __proto__: hamster }; // This one found the food speedy.eat = 'apple'; speedy.eat = 'orange'; speedy.eat = 'watermelon'; console.log( speedy.stomach ); // apple lazy.eat = 'walnut'; // This one also has it, why? fix please. console.log( lazy.stomach ); // apple // As a common solution, all properties that describe the state of a particular object, // like stomach above, are usually written into that object. |
2) Instead of working on “this” in the parent object, we simply put all methods to be shared by “this” in the parent object, and leave the stomach property for its children like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
let hamster = { stomach: [], eat(food) { this.stomach.push(food); } }; let speedy = { __proto__: hamster, stomach: [] }; let lazy = { __proto__: hamster, stomach: [] }; // Speedy one found the food speedy.eat("apple"); alert( speedy.stomach ); // apple // Lazy one's stomach is empty alert( lazy.stomach ); // <nothing> |
__proto__ and prototype usage
http://sporto.github.io/blog/2013/02/22/a-plain-english-guide-to-javascript-prototypes/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
// create object literal defaultArmour var defaultArmour = { name: "Leather Jacket", weight: 1, defense: 1, color: "clear", cost: 10 }; // create object literal defaultWeapon var defaultWeapon = { name: "stick", weight: 1, attack: 1, color: "brown", cost: 0 }; // create function Peon. // By default, its prototype is pointing to an object wiht constructor reference pointing back to Peon function Peon(name, level) { // var this = {} // reference the proto of the empty object to Foo's prototype, whatever it may point to // this is the reference to an instantion of Peon. We initially assign the __proto__ property // of the the 'this' instantiation to Foo.prototype. // this.__proto__ = Foo.prototype // public property this.name = name; this.level = level; // public function this.display = function() { console.log("--- displaying Peon info ---"); console.log("Peon: " + this.name); console.log("Level: " + this.level); var prototype = Object.getPrototypeOf(this); if (prototype && prototype.armour) { console.log("-Wearing-"); console.log(prototype.armour.name); console.log("defense: " + prototype.armour.defense); console.log("cost: " + prototype.armour.cost); } else { console.log("You're naked! Quick, find some armour"); } if (prototype && prototype.weapon) { console.log(prototype.weapon.name); console.log("attack: " + prototype.weapon.attack); console.log("cost: " + prototype.weapon.cost); } else { console.log("You're empty handed. Quick! find some weapon!"); } } // return this; } // add property 'armour' to function prototype. Armour reference object defaultArmour Peon.prototype.armour = defaultArmour; // add property 'weapon' to function prototype. Armour reference object defaultArmour Peon.prototype.weapon = defaultWeapon; var rickyPeon = new Peon("ricky", 1); rickyPeon.display(); // instantiation's proto change rickyPeon.__proto__ = null; rickyPeon.display(); |