Understanding Inheritance
According to MDN, inheritance in programming allows a child entity to inherit properties and behaviours from a parent, enabling code reuse and extension. In JavaScript, this is achieved through objects, where each object has an internal link to another object known as its prototype.
Let's illustrate this with an example:
class Person {
talk() {
return 'talking'
}
}
const me = new Person()
me.talk() // talking
me // Person { Prototype: { talk() } }Here, me does not directly contain the talk method but inherits it from the Person class through its prototype.
me.age = 25
me // {age: 25, Prototype: { talk() } }
Person.prototype === me.__proto__ // true
me.__proto__.talk() // talkingAdding a property directly to an instance gets stored on that specific object and does not modify the prototype. However, methods inherited from the prototype remain accessible via __proto__.
Modifying the Prototype Method
If we modify talk() on Person.prototype, all objects linked to this prototype will reflect the change
Person.prototype.talk = function() {
return 'new talking';
};
console.log(me.talk()); // "new talking"
// Any other instance linked to `Person.prototype` will also reflect this change
const you = new Person();
console.log(you.talk()); // "new talking"How ES6 Classes Work Under the Hood
ES6 classes are essentially syntactic sugar over JavaScript’s prototype-based inheritance. Underneath, they function using constructor functions and prototypes:
function Person() {}
Person.prototype.talk = function () {
return 'Talking';
};
const me = new Person();
const you = new Person();
console.log(me.talk()); // "Talking"
console.log(you.talk()); // "Talking"Constructor Functions and Their Behavior
In JavaScript, when using a constructor function, properties and methods can be assigned either directly to the instance (this) or to the prototype. The difference is in how they are stored and shared across instances.
function Person() {
this.talk = function() {
return 'talking'
}
}
const me = new Person()
me.talk() // talking
me // Person {talk: (), Prototype: {} }Here, the talk method is directly added to each instance (me). This means every new instance gets its own copy of talk(), unlike the prototype-based approach used in ES6 classes. This results in unnecessary duplication and increased memory usage.
function Person() {
this.age = 15
}
const me = new Person()
me.age // 15
Person.prototype.age // undefined
Person.prototype.age = 25
me.age // still 15- The
ageproperty is assigned directly tome, so it exists only on the instance. - Adding
agetoPerson.prototypelater does not affectme, since instance properties take precedence over prototype properties.
Best Practice: Use this for Properties, Prototype for Methods
For optimal performance and memory efficiency:
- Properties (unique to each instance) should be assigned directly to
thisinside the constructor. - Methods (shared across instances) should be added to
Person.prototype.
function Person() {
this.age = 15; // Instance-specific property
}
// Adding method to prototype
Person.prototype.talk = function() {
return 'talking';
};
const me = new Person();
const you = new Person();
console.log(me.talk()); // "talking"
console.log(you.talk()); // "talking"
console.log(me); // Person { age: 15 }, talk() exists in prototype
console.log(you); // Person { age: 15 }, talk() exists in prototypePrototypal Inheritance
In JavaScript, prototype inheritance is a mechanism by which objects can inherit properties and methods from other objects. Every object in JavaScript has an internal link (referred to as [[Prototype]]) to another object, called its prototype. This is the foundation of how inheritance works in JavaScript.
const person = {}
person.name = 'Anmol'
person // { name: 'Anmol', [[Prototype]]: Object }person.toString() // this property is on the proto object.
person.__proto__ === Object.prototype // trueLet’s take an example of arrays
const names = ['Anmol', 'Rahul'];
console.log(names);
// Output:
// ['Anmol', 'Rahul']
// [[Prototype]]: Array(0) → contains all array methods
// push: ƒ push()
// pop: ƒ pop()
// map: ƒ map()
// filter: ƒ filter()
// [[Prototype]]: ObjectWhen you declare an array like const names = ['Anmol', 'Rahul'];, it is an instance of the Array object and follows this prototype chain:
-
Instance Level (
names)- The array stores its elements:
0: "Anmol"and1: "Rahul".
- The array stores its elements:
-
Prototype Level (
Array.prototype)- The array inherits built-in array methods like
.push(),.pop(),.map(),.filter(), etc.
- The array inherits built-in array methods like
-
Higher-Level Prototype (
Object.prototype)-
Array.prototypeitself is linked toObject.prototype, inheriting methods liketoString()andhasOwnProperty().
-
The prototype chain for names looks like this:
names → Array.prototype → Object.prototype → nullnames.__proto__.__proto__ === Object.prototype // true
Using Object.create() for Prototypal Inheritance
const human = {
kind: 'human'
}
const anmol = Object.create(human)
anmol // [[Prototype]]: { kind: "human" [[Prototype]]: Object }
anmol.kind // humanThe Object.create(proto) method creates a new object and links it to the provided prototype (proto). In this example, anmol is an empty object but inherits the kind property from human via its [[Prototype]] chain.
proto vs prototype
function Person(name) {
this.name = name
}
const me = new Person('Anmol')
me.prototype // undefined
Person.prototype // { constructor: Dude, Prototype: {Object} }-
prototypeis a property of constructor functions (Personin this case). -
me.prototypeisundefinedbecause instances do not have aprototypeproperty—only constructor functions do.
me.__proto__ // { constructor: Dude, Prototype: {Object} }
me.__proto__ === Person.prototype // trueThus, __proto__ and prototype serve the same purpose but are accessed differently—one from the instance (__proto__) and the other from the constructor function (prototype).