Almost all languages have the concept of object-oriented. The object-oriented nature of JavaScript is based on prototype object system. When it comes to object-oriented, we have to mention inheritance.
Understanding new
It's easier for someone who has no experience in other languages to understand the inheritance of JavaScript.
Unlike class inheritance in other languages, prototype inheritance is used in JavaScript, but on the surface, it is more like class based inheritance, probably because of the use of new keyword. The new keyword is used to call a constructor. A function is called a constructor not because of its own characteristics, but because of new. That is, only functions called through new can become constructors.
Since new is so magical, it is necessary to explore what has been achieved inside.
- Create an empty object that inherits the prototype of the constructor.
- Point this of the constructor to the empty object and execute the constructor.
- For example, if the object type is returned after the constructor is executed, it will be returned directly, otherwise the object created above will be returned.
Here is a simple implementation code:
function myNew(constructor, param) { // Constructor is the constructor. param simulates the parameters of the constructor. Here is only one parameter example // Create an empty object that inherits the prototype property of the constructor const obj = Object.create(constructor.prototype) // Point this of the constructor to obj, execute the constructor to get the return result const result = constructor.apply(obj, param) // If the returned result is the object type after the function is executed, it will be returned directly. Otherwise, it will return the obj object return (typeof result === 'object' && result != null) ? result : obj } function Animal(species) { this.species = species } const cat = myNew(Animal, 'Catamount') console.log(cat) // {categories: "felines"}
Combined with the above three steps of new implementation, it is easier to understand the code.
Note that if the constructor has an explicit return value and the return value type is an object. Then the result returned by the constructor is no longer the target instance, but the explicit return value.
How to implement inheritance
There are many ways to implement inheritance in JavaScript, each with its own advantages and disadvantages. Here are a list of common ways.
I. inheritance method of constructor
function Animal(species) { this.species = species || "Animal" } function Cat(name, color, species) { Animal.call(this, species) this.name = name this.color = color } Animal.prototype.age = 10 var cat1 = new Cat("Mao Mao", "black", "Catamount") console.log(cat1.species) // Catamount console.log(cat1.age) // undefined
The implementation of this inheritance is to execute the parent constructor in the subclass constructor and point this of the parent constructor to the instance of the subclass. The advantages are easy to understand, but the disadvantages are obvious.
==Disadvantage = =: cannot inherit properties and methods on the parent prototype.
2. Prototype chain inheritance mode
function Animal() { this.species = "Animal" this.list = [1, 2, 3] } function Cat(name, color) { this.name = name this.color = color } // Point Cat's prototype object to an instance of Animal // It is equivalent to completely overriding the original value of the prototype object. Cat.prototype = new Animal() // The bank will explain in detail below Cat.prototype.constructor = Cat var cat1 = new Cat("Chinese rhubarb", "yellow") var cat2 = new Cat("Xiao Huang", "black") //For inheritance implemented in this way, prototype objects of different instances point to the same instance of Animal. When accessing attributes, if there is no such attribute in the instance, it will be found in Cat.prototype (an instance of Animal). //However, calling cat1.categories to assign a value here will not look up, but just adding a Category attribute in the CAT1 instance will not affect the attribute in the CAT1 prototype object (Animal instance). Therefore, the value of cat2.categories does not change. This way does not explain the problem. See the following code cat1.species = "Catamount" console.log(cat1.species) // Catamount console.log(cat2.species) // Animal // When you call the push method of the array, you will search along the prototype chain, find the list in the prototype object and modify the value. As mentioned above, the prototype objects of different instances point to the same instance of Animal, so the value read by cat2.list is also changed. cat1.list.push(4) console.log(cat1.list) // [1,2,3,4] console.log(cat2.list) // [1,2,3,4]
With regard to Cat.prototype.constructor = Cat, it is to reassign the constructor property on the prototype object of Cat constructor.
Because any prototype object has a constructor attribute, pointing to its constructor. If there is no Cat.prototype = new Animal(), Cat.prototype.constructor points to Cat, but after executing this code, Cat.prototype.constructor points to Animal.
Amount to
Cat.prototype.constructor === Animal //true
Moreover, every instance created by the constructor also has a constructor property, which reads the constructor property of the prototype object of the constructor.
Amount to
cat1.constructor === Cat.prototype.constructor // true
Therefore, cat1.constructor also points to Animal
cat1.constructor === Animal // true
The result is a mess of inheritance
The constructor has been modified manually. Although the problem of confusion has been solved, it can be seen in the code that this implementation method also has disadvantages.
==Benefits = =:
An instance is a subclass instance and also an instance of a parent class;
The instance can access the new prototype properties and prototype methods of the parent class;
The child prototype shares the parent prototype, and the parent prototype does not share the child prototype;
==Disadvantages = =:
Inherited instance properties. All subclasses share the instance properties of the same parent instance;
Cannot pass a parameter to the parent constructor;
III. The prototype chain inherits the new version and directly inherits the prototype
Based on the improvement of the second prototype chain mode, we want to solve the shortcomings of the previous mode.
function Animal() { this.age = 10 } function Cat() {} Animal.prototype.species = "Animal" Cat.prototype = Animal.prototype Cat.prototype.constructor = Cat var cat1 = new Cat() console.log(cat1.species) // Animal console.log(cat1.age) // undefined Cat.prototype.gender = "formall" var a = new Animal() console.log(a.gender) // formall
In this way, skip new Animal() and inherit Animal.prototype directly. Imagine not sharing the same parent instance property, but it will cause another problem. Cat.prototype and Animal.prototype now point to the same object, so any modification to cat.prototype will be reflected in Animal.prototype. At the same time, the child instance cannot access the parent instance property.
==Disadvantages = =:
The subclass parent class shares the prototype object;
Cannot inherit the parent instance property;
IV. the prototype chain inherits and revises the version and uses empty objects
First, we solve the problem that the parent class of the subclass shares the prototype object. The practical way is to create an intermediate object.
function Animal() { this.age = 10 } function Cat() { } var F = function () { } F.prototype = Animal.prototype Cat.prototype = new F() Cat.prototype.constructor = Cat Animal.prototype.species = "Animal" Cat.prototype.gender = "formall" var cat1 = new Cat() var a = new Animal() console.log(cat1.species) // Animal console.log(cat1.age) // undefined console.log(a.gender) // undefined
Obviously, this method solves the problem of sharing prototype objects between child and parent classes, but the problem of not inheriting the properties of parent instance still exists. The instance properties of the parent class are still inaccessible.
==Benefits = =:
The child class adds the prototype property, and the parent class does not update;
==Disadvantages = =:
Cannot inherit the parent instance property;
V. combination inheritance (constructor + prototype chain)
So many kinds of inheritance have been realized, but each has its disadvantages. Can we extract its essence from its dross? Implement a better way of inheritance.
function Animal(species, age) { this.species = species || "Animal" this.age = age || 10 this.list = [1,2,3] } function Cat() { Animal.call(this) } Cat.prototype = Object.create(Animal.prototype) Cat.prototype.constructor = Cat var c1 = new Cat('cat1') var c2 = new Cat('cat2') var a1 = new Animal('ani',10) // Verification of prototype object sharing of child and parent class Animal.prototype.area = "Asia" Cat.prototype.gender = "formall" console.log(c1.area) // Asia console.log(a1.gender) // undeifined // Authentication cannot access parent instance properties console.log(c1.species) // Animal // The problem of verifying the properties of different instances sharing the parent class instance c1.list.push(4) console.log(c1.list) // [1,2,3,4] console.log(c2.list) // [1,2,3]
In fact, there are many ways to implement inheritance, so we will not give examples one by one. There is no best way. Different implementations have their own advantages and disadvantages. Finding the best one is the best one.