One-time proficient in the principle of javascript prototype/inheritance/constructor/class (below)

One-time proficient in the principle of javascript prototype/inheritance/constructor/class (below)

In the last part, we explained the principles of front-end concepts such as constructors and prototypes, and learned how to share methods between instances through the prototype of the constructor. In the next part, we will mainly look at how to implement the es6 class and the inheritance of the class. Same as the previous article, the focus is on the principles behind it. Only when we understand why we have to design this way can we truly say [Proficient].

If you read the second part very hard, it means that you have not enough mastery of the prototype. Hey, please be sure to read and understand the first part first.

Master the principle of javascript prototype/inheritance/constructor/class once (on)

ES6 class

Recall the writing of es5 before:

function User(name, age) {
    this.name = name
    this.age = age
}

User.prototype.grow = function(years) {
    this.age += years
    console.log(`${this.name} is now ${this.age}`)
}
User.prototype.sing = function(song) {
    console.log(`${this.name} is now singing ${song}`)
}

const zac = new User('zac', 28)
 

How to write es6:

class User {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    grow(years) {
        this.age += years
        console.log(`${this.name} is now ${this.age}`)
    }
    sing(song) {
        console.log(`${this.name} is now singing ${song}`)
    }
}

const zac = new User('zac', 28)
 

When we call new User('zac', 28):

  1. Created a new objectzac
  2. constructorThe method runs automatically once, and at the same time assigns the passed parameters 'zac', 28to the new object

So what exactly is this class? In fact, class is just a function.

console.log(typeof User)//function
 

So how does the class work?

  1. First created a function called User
  2. Then put the code in the constructor of the class intact into the User function
  3. Finally, put the methods of class, such as grow, sing, into User.prototype

It's over, do you see it? The javascript class is just syntactic sugar for the constructor (of course the class also does some other small work)

es6 introduces the concept of class to us, which seems to be closer to other object-oriented programming languages, but unlike the class inheritance of other oop languages, the inheritance of javascript is still realized through prototypes.

ES6 extends

Let's look at how to implement inheritance between classes. es6 provides us with an extends method:

class User {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    grow(years) {
        this.age += years
        console.log(`${this.name} is now ${this.age}`)
    }
    sing(song) {
        console.log(`${this.name} is now singing ${song}`)
    }
}

class Admin extends User {
    constructor(name, age, address) {
        super(name, age)//to call a parent constructor
        this.address = address
    }
    grow(years) {
        super.grow(years)//to call a parent method
        console.log(`he is admin, he lives in ${this.address}`)
    }
}

const zac = new User('zac', 28)
 

Here we focus on the next two supercalls, the first thing to be clear is that the superkeywords are classprovided to us. There are two main uses:

  1. super(...)It is used to call the constructor method of the parent class (this can only be called in the constructor)
  2. super.method(...)Is used to call the method of the parent class

Overwrite the constructor of the parent class

Let's look at these two points separately, and solve the first question first: Why call it in the constructor of the subclass super()? The reason is very simple, because javascript stipulates that the class that comes through inheritance (extends) must be called super()in the constructor, otherwise an thiserror will be reported when called in the constructor !

If you don't believe me, see:

class Admin extends User {
    constructor(name, age, address) {
        this.name = name
        this.age = age
        this.address = address
    }
    ...
}

const zac = new admin('zac', 28, 'China') 
//VM1569:3 Uncaught ReferenceError: 
//Must call super constructor in derived class before accessing 'this' or returning from derived constructor
 

In a simple explanation, why JavaScript is designed like this: Because when used newto instantiate a class, there is an essential difference between the directly created class and the class created by extends:

  1. The former will first create an empty object, and then assign this empty object to this
  2. The latter does not do this directly, because it only needs to wait for its parent to do this.

Therefore, the subclass must be called in its own construtor super()to let its parent class execute the parent class's constructor, otherwise thisit will not be created, and then we will get an error as shown in the above example.

Why write super(props) in react?

By the way, now you should be able to understand why we wrote this sentence when we were writing react components, super(props)right?

class Checkbox extends React.Component {
  constructor(props) {
   // this
    super(props);
   // this 
    this.state = { isOn: true };
  }
 //...
}
 

Of course, there is a small problem here. What if I don t pass props super()?

//React 
class Component {
  constructor(props) {
    this.props = props;
   //...
  }
}

// 
class Checkbox extends React.Component {
  constructor(props) {
    super(); 
    console.log(this.props);//undefined   
 //
}
 

This is not difficult to understand, right? If you don't pass props to the parent component, naturally you cannot call this.props in the constructor. But in fact, you can still call this.props normally in other places, because react does one more thing for us:

 //React 
  const instance = new YourComponent(props);
  instance.props = props;
 

Override the method of the parent class

Relatively speaking, super.method()it is easier to understand. There is a grow method in our parent class User, and our subclass Admin also wants to have this method, and may also want to add some other operations to this method. So, it first calls the grow method of the parent class through the super provided by the class, and then adds its own logic.

Here are some thinking classmates who might think, why can I call the method of the parent class through super? Why can I write super.method()? If you are thinking about this question, it means you really love to think, you are great!

Simply understand, since it super.method()is the method of calling the parent class, and our subclasses are derived from the parent class, combined with the knowledge of the prototype mentioned before, super.method()shouldn't it be equivalent this.__proto__.method()? Intuitively speaking, this is indeed the case. Let's do a simple experiment to see:

let user = {
  name: "User",
  sing() {
    console.log(`${this.name} is singing.`)
  }
}

let admin = {
  __proto__: user,
  name: "Admin",
  sing() {
    this.__proto__.sing.call(this)//(*)
    console.log('calling from admin')
  }
}

admin.sing();//Admin is singing. calling from admin
 

As you can see, the user object is the prototype of the admin object. Mainly (*)looking at this sentence, we call the sing method of the prototype object user in the context (this) of the current object. Note that I used it .call(this). If we don't have this, we execute this.__proto__.sing()in the context of the prototype object user when we execute , so this.namethis points to the user object when we execute :

...
let admin = {
  __proto__: user,
  name: "Admin",
  sing() {
    this.__proto__.sing()
    console.log('calling from admin')
  }
}

admin.sing();//User is singing. calling from admin
 

Here is an explanation by the way this, knocking on the blackboard, no matter whether you found thisit in the object or the prototype , it will always be the object on the left of the dot (.). If it is user.sing(), this is the user on the left of (.); if admin.sing()this is the admin on the left of (.).

Then we look at the above example again, the method we call is admin.sing(), so when running the sing method in admin, this is admin, so:

  1. If it is this.__proto__.sing(), the caller is this.__proto__, which is equivalent to the admin.__proto__user object, so the last thing printed out is: User is singing.
  2. If so this.__proto__.sing.call(this), at this time we manually changed the caller to admin through call, so the final printout is: Admin is singing.

Okay, it's a bit too far, we will come back again. The example just now seems to prove the super.method()equivalent this.__proto__.method(), let's look at the following code:

let user = {
  name: "User",
  sing() {
    console.log(`${this.name} is singing.`)
  }
}

let admin = {
  __proto__: user,
  name: "Admin",
  sing() {
    this.__proto__.sing.call(this)//(*)
    console.log('calling from admin')
  }
}

let superAdmin = {
  __proto__: admin,
  name: "SuperAdmin",
  sing() {
    this.__proto__.sing.call(this)//(**)
    console.log('calling from superAdmin')
  }
}

superAdmin.sing();//VM1900:12 Uncaught RangeError: Maximum call stack size exceeded
 

Run the above code, and an error is reported immediately. The error tells us that the range of the maximum call stack is exceeded. This error generally means that our code appears to be called infinitely. Let's analyze it layer by layer:

  1. 1. let s look at the calling method:, superAdmin.sing()so when running the first (**)sentence, this=superAdmin, so:
this.__proto__.sing.call(this)//(**)
//
superAdmin.__proto__.sing.call(this)
//
admin.sing.call(this)//admin sing this superAdmin
 
  1. Then it runs to the first (*)sentence, this time this=superAdmin, so:
this.__proto__.sing.call(this)//(*)
//
superAdmin.__proto__.sing.call(this)
//
admin.sing.call(this)//
 

Then, in the end, you will know that admin.sing calls the caller itself in a loop. So, it is impossible to solve this problem simply through this. In order to solve this problem, javascript designed a new internal property [[HomeObject]]. Whenever a function is designated as a method of an object, this method has a property [[HomeObject]], which is fixed to the object:


let user = {
  name: "User",
  sing() { 
    console.log(`${this.name} is singing.`)
  }
}

//admin.sing.[[HomeObject]] == admin
let admin = {
  __proto__: user,
  name: "Admin",
  sing() {   
    super.sing()
    console.log('calling from admin')
  } 
}

//admin.sing.[[HomeObject]] == admin
let superAdmin = {
  __proto__: admin,
  name: "SuperAdmin",
  sing() {
    super.sing() 
    console.log('calling from superAdmin')
  }
} 

superAdmin.sing()
//SuperAdmin is singing.
//calling from admin 
//calling from superAdmin
 

ok, when we run superAdmin.sing(), that is, execute super.sing(), whenever a superkeyword appears, the javascript engine will find the [[HomeObject]]object of the current method , then find the prototype of this object, and finally call the corresponding method on this prototype.

So when we call superAdmin.sing(), it is equivalent to executing:

const currentHomeObject = this.sing.[[HomeObject]]
const currentPrototype = Object.getPrototypeOf(currentHomeObject)
currentPrototype.sing.call(this)
 

How ES5 implements extends

Let's compare and see how es5 implements the extends syntax:

function User(name, age) {
    this.name = name
    this.age = age
}

User.prototype.grow = function(years) {
    this.age += years
    console.log(`${this.name} is now ${this.age}`)
}
User.prototype.sing = function(song) {
    console.log(`${this.name} is now singing ${song}`)
}

function Admin(name, age, address) {
    User.call(this, name, age)//(*)
    this.address = address
}

Admin.prototype = Object.create(User.prototype)//(**)
Admin.prototype.grow = function(years) {
        User.prototype.grow.call(this, years)
        console.log(`he is admin, he lives in ${this.address}`)
}
Admin.prototype.constructor = Admin//(***)



const zac = new Admin('zac', 28, 'China')
 

If you have completely absorbed and understood the content of the next chapter, the above code should be well understood, right? I will take everyone to analyze it again:

  1. In the first (*)sentence, we want the Admin constructor to have the attributes name and age of the User constructor, so we use the current context this to execute the User constructor
  2. Now we can find the name, age, and address properties in the object instantiated by Admin, but we still can t use the grow method, because the grow method is defined on User.prototype, so in the first (**)sentence, we set Admin.prototype It is a new object with User.prototype as the prototype. Here you may be wondering why you should use Object.create instead of directly assigning User.prototype to Admin? The reason is that we don t want Admin and User to share the same prototype. This is the original intention of why we use inheritance.
  3. As we said in the previous article, every function has a prototype object, and there is a construtor object in this object that points to the function itself. At this time, the prototype of our Admin constructor is directly inherited from the prototype of the User constructor, so Admin.prototype.constructor === User.prototype.constructor, so we need to manually modify Admin.prototype. constructor, point it to the constructor Admin itself. This is what the first (***)sentence does

summary

Well, I think it s almost the end of the writing. We started with creating an object. We talked about constructors because we want to create objects in batches. We also talked about prototypes because we want to share methods between objects. Finally, we talked about objects. inherit. The whole context should be relatively clear.

This is the first article of the "Front-end Principles Series". The next article is planned to talk about the topic of call stack/execution context/closure/event loop mechanism. Remember to pay attention to it and see you next time.