This article is from Tencent IMWEB community: imweb. IO /
JavaScript has no class, prototype chain to implement inheritance
The first language I learned in school was CPP, which is a statically typed language. In addition, the realization of object-oriented has the class keyword directly, and only one design idea of object-oriented is mentioned, which makes it difficult for me to understand the inheritance mechanism of javascript language.
JavaScript has no concept of “subclasses” and “superclasses”, and no distinction between “classes” and “instances”. It relies on the “prototype chain” for inheritance.
When learning, I would like to ridicule, the cost of so much effort to simulate the class, that JS why not start to design the class keyword but the beginning only class as a reserved word? (ES6 has the class keyword, which is the syntactic sugar of the prototype.)
I always wondered, “Is it a design flaw that JS doesn’t have a class?”
It turns out that when JavaScript was designed, all of its data types were objects. At first, JavaScript was designed to be a simple scripting language. The designer’s JavaScript was full of objects, and there had to be a mechanism to link all objects together. But introducing a “class” is too “formal” and makes it harder to get started.
What if you want to implement inheritance but don’t want to use classes?
Brendan Eich, the designer of JavaScript, found it possible to generate instances using the new command, as in the c++ and Java languages.
The new command is then introduced into JavaScript to generate an instance object from the prototype object. But JavaScript doesn’t have “classes”, so how should prototype objects be represented?
It occurred to him that both c++ and Java use the new command to call the constructor of the “class”, so he made a simple design. Instead of using a class in JavaScript, the new command is followed by a constructor.
One disadvantage of using constructors to generate instance objects is that you cannot share properties and methods.
Each instance object has its own copy of properties and methods. This not only fails to share data, but is also a huge waste of resources.
With this in mind, Brendan Eich decided to set a Prototype property for the constructor.
This property contains a Prototype object (yes, the prototype property value is a Prototype object). All properties and methods that need to be shared by instance objects are stored in this object. Those that need not be shared are stored in constructors.
Instance objects, once created, automatically reference the properties and methods of the Prototype object. That is, the properties and methods of the instance object are divided into two types, one local and the other referenced.
Since all instance objects share the same Prototype object, the Prototype object looks as if it is a prototype of the instance object, and the instance object looks as if it “inherits” the Prototype object.
If you’re not familiar with c++, Java, or any other programming language, you’ll probably fall asleep after reading this. Ok, let’s get straight to the code
Prototype chain inheritance
// Prototype chain inheritance
/ / parent class
// Have the attribute name
function parents(){
this.name = "JoseyDong";
}
// Add a getName method to the parent's prototype object
parents.prototype.getName = function(){
console.log(this.name);
}
/ / subclass
function child(){}// The prototype object of the subclass points to the instance object of the parent class
child.prototype = new parents()
// Create an instance object of a subclass, if it has the attributes and methods of the parent class, then the inheritance is implemented
let child1 = new child();
child1.getName(); // => JoseyDong
Copy the code
With only one subclass instance object, we don’t seem to see much of a problem. However, in the real world, we would create many instances to inherit from the parent class. After all, the more we inherit, the more code will be copied
// Prototype chain inheritance
/ / parent class
// Have the attribute name
function parents(){
this.name = ["JoseyDong"];
}
// Add a getName method to the parent's prototype object
parents.prototype.getName = function(){
console.log(this.name);
}
/ / subclass
function child(){}// The prototype object of the subclass points to the instance object of the parent class
child.prototype = new parents()
// Create an instance object of a subclass, if it has the attributes and methods of the parent class, then the inheritance is implemented
let child1 = new child();
child1.getName(); // => ["JoseyDong"]
// Create an instance object of a subclass to implement inheritance before child1 modifies name
let child2 = new child();
// Modify the name attribute of the instance object child1 of the subclass
child1.name.push("xixi");
// Create another instance object of the subclass, implement inheritance after child1 changes name
let child3 = new child();
child1.getName();// => ["JoseyDong", "xixi"]
child2.getName();// => ["JoseyDong", "xixi"]
child3.getName();// => ["JoseyDong", "xixi"]
Copy the code
Most of the time, the values in our instance objects will change depending on the situation. For example, in addition to joseydong, our child1 was given a new name xixi by her friends. We changed the name value of child1. Child1, Child2, and Child3 were three separate children, but all three were given new names!
This means that the stereotype chain inherits all the values in the same memory, so that if you change the values in that memory, the values in the other inherited subclass instances will change.
This is not what we wanted, as only Child1 was given a new name. Also, if I wanted to pass arguments to the parent class through subclass instance objects, I couldn’t.
Borrowing constructor
// Constructor inheritance
function parents(){
this.name = ["JoseyDong"];
}
// In a subclass, use the call method constructor to implement inheritance
function child(){
parents.call(this);
}
let child1 = new child();
let child2 = new child();
child1.name.push("xixi");
let child3 = new child();
console.log(child1.name);// => ["JoseyDong", "xixi"]
console.log(child2.name);// => ["JoseyDong"]
console.log(child3.name);// => ["JoseyDong"]
Copy the code
Using the constructor method, we only change the name of child1. The name attribute of child2 and child3 is not affected
Also, since call() supports passing arguments, we can pass arguments to the parent in the child
// The constructor implements inheritance
// Subclasses pass arguments to their parent classes
function parents(name){
this.name = name;
}
The call method supports passing parameters
function child(name){
parents.call(this,name)
}
let child1 = new child("I am child1");
let child2 = new child("I am child2");
console.log(child1.name);// => I am child1
console.log(child2.name);// => I am child2
Copy the code
Ok, now we use constructor inheritance to make up for the disadvantages of using prototype chain inheritance, and also the advantages of using constructor inheritance:
1. Avoid the attribute of reference type being shared by all instances
2. You can pass parameters to parent in child
However, there are drawbacks to this approach because methods are defined in constructors and are created every time an instance is created.
Combination of inheritance
We find that inheritance through the stereotype chain is all about reusing the same properties and methods; Inheritance via constructors is all about separate properties and methods. So we decided to take advantage of this by combining two approaches: reusing functions by defining methods on prototypes, and ensuring that each instance has its own attributes through constructors.
Here I give a chestnut, let you feel the benefits of combination inheritance ~
// Combinatorial inheritance
// The idol Trainee competition has started to register
// For the preliminary competition, we found a class of trainees
// This class of trainees all have the property name, but the value of the name is different, and they all have the same hobbies
// Only trainees who can sing and dance rap can enter the preliminary competition
function student(name){
this.name = name;
this.hobbies = ["sing"."dance"."rap"];
}
// We found a more special category in the student category to enter the semi-finals
// Of course, we already know that there is a name attribute in the preliminary round, and different trainees have different names, so we use the constructor method to inherit
// In the meantime, we would like to ask students to describe their age again, and each subclass can add its own attributes
// Of course, the exact name and age are determined by each trainee instance
// The class just tells you that it has this property
function greatStudent(name,age){
student.call(this,name);
this.age = age;
}
// Everyone has the same hobby value, so use the prototype chain to inherit
// Every object has a constructor. A prototype object is also an object and also has a constructor
// When we point a child's prototype to an instance of the parent class, we also point a child's constructor to the parent class
// We need to manually refer the constructor of the subclass prototype object back to the subclass
greatStudent.prototype = new student();
greatStudent.prototype.constructor = greatStudent;
// Final Kunkun and fake Kunkun are in the final
let kunkun = new greatStudent('kunkun'.'18');
let fakekun = new greatStudent('fakekun'.'28');
// Let's welcome two contestants to introduce their attribute values
console.log(kunkun.name,kunkun.age,kunkun.hobbies) // => kunkun 18 ["sing", "dance", "rap"]
console.log(fakekun.name,fakekun.age,fakekun.hobbies) // => fakekunkun 28 ["sing", "dance", "rap"]
// At this point, Kunkun says his other hidden skill is playing basketball
kunkun.hobbies.push("basketball");
console.log(kunkun.name,kunkun.age,kunkun.hobbies) // => kunkun 18 ["sing", "dance", "rap", "basketball"]
console.log(fakekun.name,fakekun.age,fakekun.hobbies)// => fakekun 28 ["sing", "dance", "rap"]
// We can see that fake Kunkun did not copy Kunkun's basketball skills
// And if there is a new player at this time, a dark horse from the preliminary round
// It can be seen that Black Horse has not learned Kunkun's hidden ability
let heima = new greatStudent('heima'.'20')
console.log(heima.name,heima.age,heima.hobbies) // => heima 20 ["sing", "dance", "rap"]
Copy the code
As can be seen, composite inheritance avoids the disadvantages of prototype chain inheritance and constructor inheritance, and combines the advantages of both, becoming the most common inheritance method in javascript.
Primary inheritance
The idea of this inheritance is to use the object passed in as a prototype for the object created.
function createObj(o){
function F(){};
F.prototype = o;
return new F();
}
Copy the code
Let’s implement the original inheritance and see what happens
// Original type inheritance
function createObj(o){
function F(){};
F.prototype = o;
return new F();
}
let person = {
name:'JoseyDong'.hobbies: ['sing'.'dance'.'rap']}let person1 = createObj(person);
let person2 = createObj(person);
console.log(person1.name,person1.hobbies) // => JoseyDong ["sing", "dance", "rap"]
console.log(person2.name,person2.hobbies) // => JoseyDong ["sing", "dance", "rap"]
person1.name = "xixi";
person1.hobbies.push("basketball");
console.log(person1.name,person1.hobbies) //xixi ["sing", "dance", "rap", "basketball"]
console.log(person2.name,person2.hobbies) //JoseyDong ["sing", "dance", "rap", "basketball"]
Copy the code
Person1 hobbies: Person2 hobbies: Person2
This is because attribute values containing reference types always share corresponding values, just like stereotype chain inheritance
Person1. name is not changed because person1 and person2 have separate name values. Instead, the statement person1.name = “xixi” adds a name attribute to the person1 instance object, while the name value on its prototype object has not been changed, so the name of person2 remains unchanged. Because when we look for properties on an object, we always look for instance objects first, and then look for properties on prototype objects if we don’t find them. If there is a property of the same name on the instance object and the prototype object, the value on the instance object is always taken first.
ESMAScript5 adds the object.create () method to normalize the original type inheritance
Parasitic inheritance
Create a function that encapsulates only the inheritance process, internally does the enhancement object in some form, and finally returns the object.
// Parasitic inheritance
function createObj(o){
let clone = Object.create(o);
clone.sayName = function(){
console.log('hi');
}
return clone
}
let person = {
name:"JoseyDong".hobbies: ["sing"."dance"."rap"]}let anotherPerson = createObj(person);
anotherPerson.sayName(); // => hi
Copy the code
Of course, using parasitic inheritance to add functions to objects, like borrowing the constructor pattern, creates methods every time an object is created.
Parasitic combinatorial inheritance
As mentioned earlier, composite inheritance is the most commonly used inheritance pattern in javascript. Let’s review the code for composite inheritance:
// Combinatorial inheritance
function student(name){
this.name = name;
this.hobbies = ["sing"."dance"."rap"];
}
function greatStudent(name,age){
student.call(this,name);
this.age = age;
}
greatStudent.prototype = new student();
greatStudent.prototype.constructor = greatStudent;
let kunkun = new greatStudent('kunkun'.'18');
Copy the code
The biggest drawback of composite inheritance is that the parent constructor is called twice
Once is when setting the prototype of a subclass instance:
greatStudent.prototype = new student();
Copy the code
Once when creating an instance of a subtype:
let kunkun = new greatStudent('kunkun'.'18');
Copy the code
In this example, if we print the kunkun object, we’ll see that both greatStudent. Prototype and Kunkun have a property called Hobbies.
In fact, the property values of the instance object and the prototype object are the same, and when looking for the property values, the property values found in the instance object will not be found in the prototype object, and this part of the prototype object values will actually waste storage space.
So how do we get better at avoiding repeated calls this time around?
What if instead of using greatStudent.prototype = new Student (), we call greatStudent.prototype directly to student.prototype?
See how to do this:
// Parasitic combinatorial inheritance
function student(name){
this.name = name;
this.hobbies = ["sing"."dance"."rap"];
}
function greatStudent(name,age){
student.call(this,name);
this.age = age;
}
// Three key steps to implement inheritance
// Use the F null function as a medium between a subclass and its parent class to prevent modifying the subclass's prototype from affecting the parent class's prototype
let F = function(){};
F.prototype = student.prototype;
greatStudent.prototype = new F();
let kunkun = new greatStudent('kunkun'.'18');
console.log(kunkun);
Copy the code
Print result:
As you can see, the Kunkun instance’s prototype object no longer has the Hobbies property on it.
Finally, we encapsulate the inheritance method:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function prototype(child, parent) {
let prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
// When we use:
prototype(Child, Parent);
Copy the code
To quote the praise of parasitic combinatorial inheritance from JavaScript Advanced Programming:
The efficiency of this approach is that it calls the Parent constructor only once, and thus avoids creating unnecessary, redundant properties on Parent. Prototype. At the same time, the prototype chain stays the same; Therefore, instanceof and isPrototypeOf can also be used normally. Parasitic combinatorial inheritance is generally considered by developers to be the ideal inheritance paradigm for reference types.
All in all, this is the best way to implement JS inheritance.
ES6 implements inheritance
However, ES6 has since implemented inheritance through the extends keyword.
// ES6
class parents {
constructor(){
this.grandmather = 'rose';
this.grandfather = 'jack'; }} class children extends parents{constructor(mather,father){//super; super(); this.mather = mather; this.father = father; }}let child = new children('mama'.'baba');
console.log(child) // =>
// father: "baba"
// grandfather: "jack"
// grandmather: "rose"
// mather: "mama"
Copy the code
Subclasses must call the super method from the constructor method or they will get an error when creating a new instance. This is because the subclass does not have its own This object, but inherits the parent’s This object and then processes it.
The this keyword can only be used after super is called, otherwise an error will be reported. This is because subclass instances are built based on processing superclass instances, and only super methods can return superclass instances.
The essence of ES5 inheritance is to create a subclass instance object, this, and then add the Parent class’s methods to this (parent.call (this)).
The inheritance mechanism in ES6 essentially creates an instance object of the parent class, this (so the super() method must be called first), and then modifs this with the constructor of the subclass.
The core code for es6 implementation inheritance is as follows:
function _inherits(subType, superType) {
subType.prototype = Object.create(superType && superType.prototype, {
constructor: {
value: subType,
enumerable: false,
writable: true,
configurable: true}});if(superType) { Object.setPrototypeOf ? Object.setPrototypeOf(subType, superType) : subType.__proto__ = superType; }}Copy the code
The proto property of a subclass: represents the inheritance of the constructor, always pointing to the parent class. Proto property of the prototype property: Indicates method inheritance, always pointing to the Prototype property of the parent class.
In addition, ES6 can customize subclasses of native data structures (such as Array, String, etc.), which ES5 cannot do.