Master the principle of JavaScript prototype / Inheritance / constructor / class at one time (I)

Time:2021-7-27

Recently, I interviewed many front-end students and found that many students have weak front-end foundation and can use react / Vue and other libraries or frameworks, but they are ambiguous about some core front-end concepts, such as prototype, inheritance, scope, event cycle, etc. So many times when you ask deeper questions or when it comes to principles, you hesitate and can’t answer them.

Therefore, we plan to update a new series, focusing on the core basic knowledge of the front-end, so that everyone can ride the wind and waves both on the growth of the front-end technology and in the interview process!

Today we talk about the core concept of javascript: prototype. Other articles will be continuously updated.

Although today we’re going to talk about the prototype of JavaScript, in order to let you know why we want to design such a thing, I’m going to start with how to generate an object.

Generate a simple object

The simplest way to generate objects:

let user = {}
user.name = 'zac'
user.age = 28

user.grow = function(years) {
    this.age += years
    console.log(`${this.name} is now ${this.age}`)
}

Generating a user object in this way is very simple. What if you need to generate a pile of user objects? We can create a function specifically to generate user:

function User(name, age) {
    let user = {}
    user.name = name
    user.age = age

    user.grow = function(years) {
        this.age += years
        console.log(`${this.name} is now ${this.age}`)
    }
    return user
}

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

This function actually has a special name called factory function

Create an object using object.create

But now we have a problem with this function. Every time we instantiate a user, we have to reallocate memory and create a growth method. How to optimize it? We can move all the methods in the user object:

const userMethods = {
    grow(years) {
        this.age += years
        console.log(`${this.name} is now ${this.age}`)
    }
}

function User(name, age) {
    let user = {}
    user.name = name
    user.age = age

    user.grow = userMethods.grow
    return user
}

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

After moving out, we encounter another troublesome problem. If we need to add a new method to the user, such as sing,

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

At this time, we need to add corresponding methods in the user:

function User(name, age) {
    let user = {}
    user.name = name
    user.age = age

    user.grow = userMethods.grow
    user.sing = userMethods.sing
    return user
}

This brings endless trouble to the later maintenance. Is there any way we can avoid it? Now our user function generates an empty object {} every time. Can we directly use the usermethods object as the blueprint to generate an object? In this way, you can directly use the methods in usermethods.

JavaScript provides us with this method:Object.create(proto), this method generates an empty object and sets the parameter proto to its own prototype[[Prototype]]。 What’s the use of prototypes? In short, if we find a property or method in an object and can’t find it, the JavaScript engine will continue to find it in the prototype of the object. If we can’t find it again, we will continue to find it in the prototype of the object prototype until we find or encounter null. This process is the prototype chain. OK, let’s rewrite the user:

function User(name, age) {
    let user = Object.create(userMethods)
    user.name = name
    user.age = age

    return user
}

I wonder if you have noticed that my user function is capitalized. What is the name of such a function in JavaScript? Constructor, that isconscrutor, it is specially used to construct objects!

With the help of the prototype attribute of the function

Now there is another problem. Our user constructor has to be used in conjunction with usermethods. It seems very troublesome. Is there any method in JavaScript that allows us to save writing this usermethods object?

yes , we have! Now I want to talk about a very important concept — what is a prototypeprototype? Knock on the blackboard!Each function created in JavaScript has the attribute prototype, which points to an object (this object contains a constructor attribute pointing to the original function)

It looks like a tongue twister. In fact, it’s easy to understand. Let’s take an example. We create a function called a, which naturally contains the prototype attribute. When printed, we can see that it is an object. This object naturally has an attribute called constructor, and the F function it points to is our a function itself.

function a() {}
console.log(a.prototype)  // {constructor: ƒ}

By the way, I’d like to talk about what we just saidObject.create(proto), didn’t I also mention the prototype[[Prototype]]Are you? Knock on the blackboard! It should be noted here that as shown below, the prototype of the object can be passedObject.getPrototypeOf(obj)Or ancient writing__proto__Get; The function itself has an attribute called prototype, which can be found directly on the functionf.prototype。 The two are not the same thing.

const b = {}
const c = Object.create(b)

console.log(Object.getPrototypeOf(c) === b)  //true
console.log(c.__proto__ === b) // true

OK, now we’re going back to the original topic. It’s known that each function has its own prototype attribute. Can we make good use of this? We don’t need to put the public methods of the user object in usermethods at all. Just put them directly in the prototype of the user function. Hello!

function User(name, age) {
    let user = Object.create(User.prototype)
    user.name = name
    user.age = age

    return user
}

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 = User('zac', 28)

Use constructors to generate objects

My God, it’s simple, elegant and generous! So simple and elegant that JavaScript decided to integrate this into the JavaScript language, so it officially produced the constructorconstructor, which is specially used to construct objects. The use method is used before the constructornewInstructions. Let’s see how to write it directly with JavaScript constructor:

function UserWithNew(name, age) {
    // let this = Object.create(User.prototype)
    this.name = name
    this.age = age

    // return this
}

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 UserWithNew('zac', 28)

Compared with the user function we wrote above, is it not much different? Those differences arenewCommand to do!

What does the new instruction do

Here, let’s expand a little. If someone wants you to write onenewCommand, is it easy to catch? To sum up, there are four things:

  1. Generate a new object
  2. Set the prototype for this object
  3. Use this to execute the constructor
  4. Return this object

Now go and write one by yourself! I can’t write down what you want to see me again:

function myNew(constructor, args) {
    const obj = {}
    Object.setPrototypeOf(obj, constructor.prototype)
    constructor.apply(obj, args)
    return obj
}

Of course, there must be problems with mynew in the production environment:

  1. We may have multiple parameters and call like thismyNew(constructor, 1, 2, 3)
  2. Normally, we don’t write return when we write a constructor, but in some extreme cases, some people’s constructors will return an object by themselves
  3. The first two sentences can be abbreviated into one sentence

So rewrite:

function myNew(constructor, args) {
    const obj = Object.create(constructor.prototype)
    const argsArray = Array.prototype.slice.apply(arguments) 
    const result = constructor.apply(obj, argsArray.slice(1))
    if(typeof result === 'object' && result !== null) {
        return result
    }
    return obj
}

Note the second sentence here. Because arguments is an array like thing, it actually does not have the slice method, so we borrow this method from array.prototype. The first use of slice is to convert arguments into an array. The second use of slice is to get a new array without the first element.

Here, I’d like to continue to talk about it. Let me give an example:

const a = [1, 2, 3]
a.toString() 

Let’s think about why a this array has a method called toString?

  1. First of all, JavaScript is used to create an array for younew Array(1, 2, 3)Array a created for you
  2. This array function is actually a constructor. Combined with the various knowledge we mentioned earlier, we can get the prototype of array a__ proto__ It’s array.prototype(a.__proto__ === Array.prototype)
  3. Since there is no toString method on array a, JavaScript will find it on its prototype array.prototype
  4. Hey, I found it
  5. If you don’t find it, you’ll go to the prototype of array.prototype(a.__proto__.__proto__ === Object.prototype

Finally, we will rewrite the mynew function by using the rest syntax of ES6 (see my ES6 series of articles if you don’t understand it):

function myNew(constructor, ...args) {
    const obj = Object.create(constructor.prototype)
    const result = constructor.apply(obj, args)
    if(typeof result === 'object' && result !== null) {
        return result
    }
    return obj
}

Constructor vs factory function

Before I finish, I have to mention this a little. We first introduced the factory function, then said it was bad, and then introduced the constructor. But in fact, the two of them, who is good and who is bad, still have to look at dialectically.

Factory functions have at least two advantages over constructors:

  1. The factory can have private variables and methods
  2. The factory function does not have this and will not encounter the bugs caused by this

But these two benefits are gradually becoming less obvious

  1. First, the JavaScript proposal designs private variables for class, with#As follows, the variable waterlimit can only be accessed inside class, but not outside class. However, this proposal has not been fully implemented yet.
  2. The arrow function of ES6 also solves this problem in most cases.
class CoffeeMachine {
  #waterLimit = 200;
  ...

summary

That’s about it. I’ve told you all about prototype, prototype chain, constructor and new. I hope I’ve made it clear. By the way, ES6 didn’t you bring the writing of class? Tomorrow, I’ll rewrite our user constructor with class, and the concepts of extend inheritance will be discussed one after another. Let’s look forward to it.

Master the principle of JavaScript prototype / Inheritance / constructor / class at one time (Part 2)

It took 3 hours to finish it. If you think it is useful to you, remember to collect some praise. In addition, Shenzhen Ali continues to recruit people. You are welcome to hook up with private letters