PS: Pre-knowledge: prototype chain

twitter

When it comes to inheritance, the first impression is all the methods in the Little Red Book, primary inheritance, combined inheritance, parasitic inheritance balabala.

Beginner JS to see the inheritance of the heart is broken, py, Java a keyword to fix things you can play so many tricks.

In fact, this is a language design level problem, JS is not a strict object-oriented system like Java, but…

We know that object-oriented systems can be implemented in the way of data + procedures (yes, I am talking about SICP chapter 3), so what raw materials JS provides for us to implement object-oriented?

JavaScript is a prototype-based, function-first language, a multi-paradigm language that supports object-oriented programming, imperative programming, and (from Wiki) functional programming.

It’s basically using a prototype, specifically a prototype chain.

Prototype chain

Quickly understand

We understand the prototype chain with an interview question.

Object.prototype.a = 'a'

Function.prototype.b = 'b'

function Animal() {}

const obj = new Animal()

console.log(obj.a) // 'a'
console.log(obj.b) // undefined
Copy the code

PS: Too long to see

  1. The newobjObjects are constructorsAnimalAn instance of the
  2. thenobjthe__proto__Attribute points toAnimal.prototype
  3. Animal.prototypeIs an object, therefore its__proto__Attribute points toObject.prototype
  4. inobjLook foraAttribute not found
  5. Go to theobj.__proto__Keep looking, no luck
  6. Go to theobj.__proto__.__proto__Continue to find
  7. Find the propertiesa, returns its value'a'
  8. inobjLook forbAttribute not found
  9. Go to theobj.__proto__Keep looking, no luck
  10. Go to theobj.__proto__.__proto__Keep looking, no luck
  11. obj.__proto__.__proto__.__proto__fornull, the search ended, not foundbProperty, returnsundefined

The xxx.__proto__.__proto__.__proto__ is the prototype chain, which is actually a series of related objects.

obj -> Animal.prototype -> Object.prototype
Copy the code

Summary: A prototype chain is a series of objects that are linked together.

Little doubt

So where is our b property?

Actually, on the Animal constructor, or on all function objects.

We know that function literals are the same as MDN:

const Animal = new Function()
Copy the code

The prototype chain at this time:

Animal -> Function. Prototype -> ObjectCopy the code

So you can get to B through the prototype chain, you can even get to A.

Function. Prototype is a Function object.

The succession of ES5

Function Person(name) {this.name = name} Person. Prototype. SayHi = function () {console.log('Hi, this is ', Function Student(name, grade) {person.call (this, grade); Class (Person. Prototype, {constructor: {value: {constructor:}) class (Person. Prototype, {constructor: {value:}) Student, writable: true, configurable: true, enumerable: False,}}) / / subclass Student. My way. The prototype doHomework = function () {the console. The log (' so much homework! ')}Copy the code

As you can see from the previous article, the constructor itself and the constructor’s Prototype attribute remain our focus.

Steps to complete inheritance:

  1. throughnewKeyword call, create an instance object of a subclassthis(Auto Complete)
  2. Constructor calls the parent class’s constructor to givethisObject adds attributes, subclass instances have attributes defined in the parent class
  3. Modify the prototype of the constructor, mainly by adding missing onesconstructorAttributes and will prototype__prop__The prototype object whose property is set to the parent class (handles the prototype chain, throughObject.createComplete)

Object.create returns a new Object, setting the first argument to __proto__ of the returned Object

Take a look at the results:

const p = new Person('Joel')
const s = new Student('Bill', 3)
console.log(p, s)
console.log(s instanceof Student, s instanceof Person)
Copy the code

Emmmm, behaving as expected, just too long.

And as mentioned above, this is just one paradigm for implementing inheritance, which means that words written by different people can have multiple ways of writing inheritance, which is messy. Or you can turn to the tool library.

The extend keyword was invented to solve both of these problems.

what the hell does extend do

Continue with the example from the previous article

class Person { constructor(name) { this.name = name } sayHi() { console.log('Hi, this is ', this.name) } waveArm = () => { console.log(this.name, 'is waving arm') } } class Student extends Person { constructor(name, grade) { super(name) this.grade = grade } doHomework() { console.log('so much homework! ')}}Copy the code

The code compiled by Babel looks like this (functions that appeared in the previous article are not listed in detail) :

'use strict'; var _createClass = function () { ... } (); function _possibleConstructorReturn(self, call) { if (! self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass ! == "function" && superClass ! == null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } function _classCallCheck(instance, Constructor) { ... } var Person = function () { function Person(name) { var _this = this; _classCallCheck(this, Person); this.waveArm = function () { console.log(_this.name, 'is waving arm'); }; this.name = name; } _createClass(Person, [{ key: 'sayHi', value: function sayHi() { console.log('Hi, this is ', this.name); } }]); return Person; } (); var Student = function (_Person) { _inherits(Student, _Person); function Student(name, grade) { _classCallCheck(this, Student); var _this2 = _possibleConstructorReturn(this, (Student.__proto__ || Object.getPrototypeOf(Student)).call(this, name)); _this2.grade = grade; return _this2; } _createClass(Student, [{ key: 'doHomework', value: function doHomework() { console.log('so much homework!'); } }]); return Student; }(Person);Copy the code

You can see that there are two more functions:

  • _inherits
  • _possibleConstructorReturn

Let’s still start at the bottom.

The Person and Student variables are our parent and subclass, respectively. The Student variable still holds a function returned by IIFE. Let’s start with IIFE (tweaked code order, manually upgraded function declarations).

var Student = function (_Person) {
    function Student(name, grade) {
        _classCallCheck(this, Student);

        var _this2 = _possibleConstructorReturn(this, (Student.__proto__ || Object.getPrototypeOf(Student)).call(this, name));

        _this2.grade = grade;
        return _this2;
    }

    _inherits(Student, _Person);

    _createClass(Student, [{
        key: 'doHomework',
        value: function doHomework() {
            console.log('so much homework!');
        }
    }]);

    return Student;
}(Person);
Copy the code

Processing of prototype chain

The _inherits and _createClass functions handle the stereotype chain.

_createClass was detailed in the previous article.

The Student function inside IIFE is the constructor for subclasses. Ignore it and focus on the _inherits function.

function _inherits(subClass, superClass) { if (typeof superClass ! == "function" && superClass ! == null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }Copy the code

The signature of the _inherits function is easy to understand

  • Ttf_subclass: subClass
  • SuperClass: parent class

Look again at the if block at the beginning.

if (typeof superClass ! == "function" && superClass ! == null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); }Copy the code

It requires that the parent must be a function or null.

You may be wondering if the parent class can still be null. What does null inherit? This seemed very different from our experience in ES5. Because in ES5 the superclass is definitely a function. Don’t worry, keep reading.

Notice the error message thrown when the test fails. The “Super expression” here is the constructor of the parent class.

Next up is our favorite part of the processing prototype chain. The difference from ES5 is only when the parent class is null.

subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: {
            value: subClass,
            enumerable: false,
            writable: true,
            configurable: true
        }
});
Copy the code

Two branches:

  • If the parent class isnullifObject.createThe return is one of none__proto__Property, that is, the prototype chain is broken here.
  • Otherwise it’s the same as what we did in ES5.

The final if block, formatted as follows:

if (superClass) {
    Object.setPrototypeOf
    ? Object.setPrototypeOf(subClass, superClass)
    : subClass.__proto__ = superClass;
}
Copy the code

There are also two cases:

  1. The parent class fornull, if the code block does not execute, then the parent class__proto__Point to theFunction.prototype, a function object.
  2. Otherwise subclass__proto__Let it be the parent constructor, which is also a function object. This operation is not available in ES5 and is subclassed as specified in ES6__proto__Property points to the parent class.

The prototype chain section shows that Function objects are actually instances of the Function constructor, so the prototype chain of the constructor looks like this

subClass -> superClass -> Function.prototype -> Object.prototype
Copy the code

What if superClass is null?

subClass -> Function.prototype -> Object.prototype
Copy the code

This is no different than a normal constructor.

Here’s a picture

This is part of the class parsing I put together for the ECMA-262 6th Edition. Combining this diagram should make it very clear how the prototype chain is processed.

You can see that the flowchart starts with no parent, that is, defining a class directly or extending a parent through extend.

The constructor itself

function Student(name, grade) {
    _classCallCheck(this, Student);

    var _this2 = _possibleConstructorReturn(this, (Student.__proto__ || Object.getPrototypeOf(Student)).call(this, name));

    _this2.grade = grade;
    return _this2;
}
Copy the code

_classCallCheck was analyzed in the last article.

It is important to _possibleConstructorReturn this function

function _possibleConstructorReturn(self, call) { if (! self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }Copy the code

The function signature doesn’t seem to give us much information, considering how it is called:

  1. Self: Very simple, inside when the constructor is calledthisobject
  2. The call:(Student.__proto__ || Object.getPrototypeOf(Student)).call(this, name)

What the hell is “call”

(Student.__proto__ || Object.getPrototypeOf(Student)).call(this, name)
Copy the code

We already know that the __proto__ of a subclass is set to the parent constructor or function. prototype, so this is what we did in ES5: call the parent constructor in a subclass.

We know that normal constructors do not have an explicit return value, because calls using the new keyword return this by default if the return value is not explicitly specified. These constructors return undefined when called directly.

_possibleConstructorReturn from name can know, if the superclass constructor with clear return values (must be an object), then return to it, otherwise it returns the current of this object.

So the question is, why this behavior?

Start with the new keyword

We need to know before we can figure this outnewWhat keywords do, refer to ES standard:

See that new returns Construct(constructor, argList)

Note this function, which is mentioned several times in this article.

And then what Construct does, the criteria is here, so if you’re interested, you can read it for yourself.

Since this paper does not want to involve much knowledge of environment variables, Construct’s behavior is explained as briefly as possible.

Each function has a [[ConstructorKind]] attribute inside, and when the function is a generator or inherits a constructor of a class (like our Student), kind is “derived”, otherwise “base”.

Construct calls constructor again.[[Construct]]

When kind is “base”, [[construct]] does the following:

  1. Create a new object and process the prototype chain, making the new object an instance of the constructornew.targetThe function that points to.
  2. The current environmentthisValue binding for the newly created object
  3. Execute the function body (at which point our code starts executing)
  4. If the function has an explicit return value and the return value is oneobjectAnd return it
  5. Otherwise returnsthis

When kind is “derived”, [[construct]] does the following:

  1. Execute the function body (at this point our code starts executing, little tip:superIt was just implemented here.)
  2. If the function has an explicit return value and the return value is oneobjectAnd return it
  3. Otherwise returnsthis

See the difference? The second case does not bind this to the current environment, so an error will be reported if this is used:

This is where the super keyword comes in

Super is called as a function

ES5 inheritance essentially creates an instance object of the subclass, this, and then adds the Parent class’s methods to this (parent.apply (this)). ES6 has a completely different inheritance mechanism, essentially creating an instance object of the parent class, this (so the super method must be called first), and then modifying this with the constructor of the subclass.

The above paragraph is from ruan Yifeng teacher ECMAScript 6 introduction.

Emmmmm. I don’t understand it at first.

Super () is bound to this ();

Simplified flow chart:

In the figure:

  • newTargetisnew.targetPointing function
  • funcIs the constructor of the parent class
  • Continue to call with these argumentsConstruct
  • callConstructIt’s recursive until whenkindTerminates only if it is “base” or if a function has an explicit return value

It is created in the parent class, but it is still an instance of the subclass (its __proto__ refers to the prototype of the subclass), because new.target is still the function followed by the new keyword.

Emmmm, in an object-oriented language like PY, an instance is created from the class of that class, but there seems to be a bit of ambiguity in JS prototype inheritance…

Problems with ES5 inheritance

Digress to _possibleConstructorReturn function.

Imagine the following scenario, in ES5, inheriting the Array constructor.

So in our ES5 implementation, we call Array.call(this, args). However, this call doesn’t change this at all, because Array ignores this and our new is an empty object.

function MyArr() {
    Array.call(this, arguments)
}

MyArr.prototype = Object.create(Array.prototype, {
    constructor: {
        value: MyArr,
        writable: true,
        configurable: true,         enumerable: true,
    },
})

const arr = new MyArr()

arr[0] = 'JS'

console.log(arr.length) // 0
Copy the code

When we want to inherit a built-in type such as Array with ES5 we lose some special behavior, such as the binding of the Length property to the number of elements stored in the Array, because our instance was not created using Array.

But we also know that in ES5 these two calls are the same.

Array(5) // [empty × 5]
new Array(5) // [empty × 5]
Copy the code

That is, Array ignores this but explicitly returns an Array object.

In the previous section, we learned that a constructor can specify its own return value, so when a parent class has an explicit return value, Babel replaces the child instance with the parent’s return value, thus avoiding the loss of special behavior mentioned above.

But then our inheritance is invalid, because instead of returning this, everything on the subclass’s prototype chain is lost.

How does ES6 solve this problem

Function objects are called function objects because there are methods [[call]] and [[construct]] inside. In ES5, we actually call the parent constructor [[call]]. In ES6, if you look closely, You’ll notice that the super keyword is called [[construct]].

The most significant difference between the two calls is the value of new.target (related to environment variables)

  • through[[call]]Call the constructor, inside the constructornew.targetforundefined
  • through[[construct]]Call the constructor, inside the constructornew.targetfornewThe function called by the keyword

As mentioned above, the __proto__ of the created instance is confirmed from new.target, so when inheriting an Array from ES6 (super), the prototype chain is handled inside an Array constructor we can’t touch, something polyfill can’t do…

At this time, to recall the _possibleConstructorReturn the behaviour of the function, is a no…

Afterword.

The level is limited, the article inevitably has the error, welcome to correct.

There are other aspects of super that are not covered in this article.

Personally, I prefer grammar candy, because it’s cool to use.

It took so long to write… Graduation and internship have been a bit busy.

But at least I found out.