This article was actually written by me a few months ago. I happened to see some problems that I didn’t write clearly, so I turned it over and refried it
- Blog address
- My lot
Just a Star or follow it
ES6’s Class is just a dose of syntactic sugar, but it gives programmers from many other languages a clearer way to write code, even if it’s essentially prototypes and prototype chains. But now we can let functions do their own thing, instead of being both parents (logical functions) and parents (constructors). But have you ever wondered what happens to ES6 classes after they are compiled? Where is the method added? What about static methods? How is inheritance (super) implemented?
The Class syntax in this article will not include private attributes/decorators and other syntax not yet identified in the ES proposal
Basic Class code analysis
Let’s start with the simplest Class:
class Parent {
constructor(name) {
this.name = name || "Parent";
this.age = 21;
}
sayName() {
console.log(this.name);
}
saySth(str) {
console.log(str);
}
static staticMethod(str) {
console.log(str); }}const p = new Parent("Parenttt");
p.sayName();
p.saySth("HelloP");
Parent.staticMethod("Sttttatic");
Copy the code
This is a simple piece of code that constructs a Parent class. We use the Babel compiler and set the compile target to ES2015-strict.
Compile the results
Let’s first look at the compiled results:
"use strict";
function _instanceof(left, right) {
// ...
}
function _classCallCheck(instance, Constructor) {
// ...
}
function _defineProperties(target, props) {
// ...
}
function _createClass(Constructor, protoProps, staticProps) {
// ...
}
var Parent = /*#__PURE__*/ (function() {
function Parent(name) {
_classCallCheck(this, Parent);
this.name = name || "Parent";
this.age = 21;
}
_createClass(
Parent,
[...]
);
returnParent; }) ();var p = new Parent("Parenttt");
p.sayName();
console.log(p.myName);
p.saySth("HelloP");
Parent.staticMethod("Sttttatic");
Copy the code
The first thing you’ll notice is that the Parent class is actually a function (the same Parent function returned from inside IIFE), which is instantiated after we do something to it in _createClass. Obviously, the key is in _createClass, and you can easily guess what methods and attributes are added in this step.
But inside the Parent function, we first call the _classCallCheck() method, which, as the name implies, checks how the call is made.
// _classCallCheck(this, Parent);
function _classCallCheck(instance, Constructor) {
if(! _instanceof(instance, Constructor)) {throw new TypeError("Cannot call a class as a function"); }}Copy the code
We all know that when a function is called with the new keyword, the internal this will refer to the current instance object. In this method, we mainly check whether the class is called with the new keyword. Otherwise we assume that the class was incorrectly called as a function (const p = Parent(),). The _instanceof() method is a complement to the original instanceof method.
function _instanceof(left, right) {
if( right ! =null &&
typeof Symbol! = ="undefined" &&
right[Symbol.hasInstance]
) {
return!!!!! right[Symbol.hasInstance](left);
} else {
return left instanceofright; }}Copy the code
A distinction is made between cases where the environment supports and does not support Symbol:
[Symbol. HasInstance](p) : [Symbol. HasInstance](p) : [Symbol. You can override this method inside the class, as follows:
class Foo {
// Static methods are called as foo instance foo
// Otherwise you need to say foo instance new foo ()
static [Symbol.hasInstance](ins){
// Returns a Boolean}}Copy the code
Similarly common built-in Symbol values are symbol. iterator. Separator [Symbol. Split](String. Prototype. split(separator, limit) = separator[Symbol. Split](this, Limit)) symbol.toprimitive (called when the object is converted to a value of the original type, see symbol.toprimitive), etc
Then there is the _createClass() method we are interested in, and we can look at its input arguments first:
_createClass(
Parent,
[
{
key: "sayName".value: function sayName() {
console.log(this.name); }, {},key: "saySth".value: function saySth(str) {
console.log(str); },},], [{key: "staticMethod".value: function staticMethod(str) {
console.log(str); }},]);Copy the code
So this is very simple, just passing the method in as a key-value pair, and if you look at it you’ll see that there are actually three arguments, and the static method is taken as a separate third argument.
Let’s look at its internal implementation:
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor); }}Copy the code
The key is the _defineProperties method. We can see that it makes some modifications to the modifier and then adds this property to the class using the native method. You can see that the instance method is added to the stereotype and the static method is added to the class (the constructor is the class).
This may solve some of the puzzles of ES6 Class syntax in the beginning:
- All internal methods are not enumerable because of their qualifiers
enumerable
Is set to false unless you change the compilation result. - How static methods and instance methods are called
- The constructor properties of the prototype object point directly to the class itself
Parent.prototype.constructor === Parent
What is the essence of ES6 Class?
- It’s still a function, but the class itself is a constructor
- Methods are added to the prototype object (instance method) or to the class itself (static method)
- It is checked before being called, so it cannot be called as a function
This is just the simplest example. Let’s look at an inheritance example:
inheritance
The source code:
class Parent {
// ...
}
class Child extends Parent {
constructor(name) {
super(a);this.name = name || "Child";
this.age = 0;
}
ownMethod() {
console.log("Own Method"); }}Copy the code
This time the code is a little more complicated, so I’ll just pick out the new bits
function _typeof(obj) {
// ...
}
function _createSuper(Derived) {
// ...
}
function _possibleConstructorReturn(self, call) {
// ...
}
function _assertThisInitialized(self) {
// ...
}
function _isNativeReflectConstruct() {
// ...
}
function _getPrototypeOf(o) {
// ...
}
function _inherits(subClass, superClass) {
// ...
}
function _setPrototypeOf(o, p) {
// ...
}
var Child = /*#__PURE__*/ (function(_Parent) {
_inherits(Child, _Parent);
var _super = _createSuper(Child);
function Child(name) {
var _this;
_classCallCheck(this, Child);
_this = _super.call(this);
_this.name = name || "Child";
_this.age = 0;
return _this;
}
_createClass(Child, [
{
key: "ownMethod".value: function ownMethod() {
console.log("Own Method"); }},]);return Child;
})(Parent);
Copy the code
You can see a lot more logic, but! A large portion of the logic is still focused on enhancing the native approach and doing some boundary case handling.
Let’s break it down one by one, starting with the _inherits() method in the first line:
function _inherits(subClass, superClass) {
if (typeofsuperClass ! = ="function"&& superClass ! = =null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true.configurable: true}});if (superClass) _setPrototypeOf(subClass, superClass);
}
Copy the code
-
Boundary case handling, the parent class must be a function type (because the class itself is a function), and cannot be null
-
The __proto__ of a subclass’s prototype object points to the parent class’s prototype object:
Note: only functions have prototype, but every object has __proto__, and __proto__ can be understood as the prototype object of the object’s constructor
The create() method creates a new object, using the first argument as the __proto__ of the new object // The second argument is the same as the second argument to Object.defineProperties() subClass.prototype = Object.create(superClass.prototype, descriptor); Copy the code
If the subclass instance is C, then there are
c.__proto__ === Child.prototype; // true c.__proto__.__proto__ === Parent.prototype; //true Copy the code
If you can’t remember the concept of a prototype, take a look at this image to quickly recall it:
-
If the parent class is not null, use the enhanced _setPrototypeOf() method to modify the subclass.prototype object of the subClass. This method actually only does some compatibility processing
// _setPrototypeOf(subClass, superClass); function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } Copy the code
Child.__proto__ === Parent; // true Copy the code
__proto__ sets subclass.prototype. __proto__ and subclass.__proto__ to the parent class, respectively.
The _createSuper(Child) method follows:
// var _super = _createSuper(Child);
function _createSuper(Derived) {
return function() {
var Super = _getPrototypeOf(Derived),
result;
if (_isNativeReflectConstruct()) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this.arguments);
}
return _possibleConstructorReturn(this, result);
};
}
Copy the code
Leaving aside the other methods involved, let’s take a look at the general idea:
-
Gets the Child prototype object, the parent class
-
If Reflect is available in the environment, the constructor is created using the reflect.construct syntax
-
Otherwise, just use Apply
_getPrototypeOf()
:
// var NewTarget = _getPrototypeOf(this).constructor;
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf
? Object.getPrototypeOf
: function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
Copy the code
Again, compatibility, so I’m going to skip this.
_isNativeReflectConstruct()
:
function _isNativeReflectConstruct() {
if (typeof Reflect= = ="undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy= = ="function") return true;
try {
Date.prototype.toString.call(Reflect.construct(Date[],function() {}));
return true;
} catch (e) {
return false; }}Copy the code
Reflect. Construct (target, args) if You haven’t seen Reflect, you can read ruan’s article. Use the word reflect. construct(target, args) equivalent to new Target (… Args), which is equivalent to a way to call the constructor without using new.
Date.prototype.toString.call(Reflect.construct(Date[],function() {}));
Copy the code
This code is more like the test code after testing to make sure the environment supports Reflect… Make sure it works properly.
_possibleConstructorReturn()
// _possibleConstructorReturn(this, result)
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
function _typeof(obj) {
/ /... TypeOf completion enhancement is not introduced
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError(
"this hasn't been initialised - super() hasn't been called"
);
}
return self;
}
Copy the code
And the interior of the Child class:
var Child = /*#__PURE__*/ (function(_Parent) {
_inherits(Child, _Parent);
var _super = _createSuper(Child);
function Child(name) {
var _this;
_classCallCheck(this, Child);
_this = _super.call(this);
_this.name = name || "Child";
_this.age = 0;
return _this;
}
_createClass(Child, [
{
key: "ownMethod".value: function ownMethod() {
console.log("Own Method"); }},]);return Child;
})(Parent);
Copy the code
You can see that in this step the main thing is to ensure that the _super()(super()) method is called.
Break down
ES6 inheritance is the focus, let’s expand on it:
Inheritance of ES5, taking parasitic combinatorial inheritance as an example:
function P(name, age) {
this.name = name || "linbudu";
this.age = age || 21;
}
P.prototype.desc = function() {
return this.name + this.age + "FE";
};
function C(name, age) {
P.call(this, name, age);
}
(function() {
let tmp = function() {};
tmp.prototype = P.prototype;
C.prototype = newtmp(); }) (); C.prototype.constructor = C;let c1 = new C("son1".21);
let c2 = new C("son2".21);
console.log(c1.desc()); // son1-21-FE
console.log(c2.desc()); // son2-21-FE
P.prototype.desc = () = > "Quit!";
console.log(c1.desc()); // Quit!
Copy the code
Call /apply (desc); call/apply (desc); call/apply (desc); call/apply (desc);
For ES6, generate the superclass instance object (result in _createSuper() is the superclass instance), and then modify subclass this with the superclass instance (this process requires that super() be called in the subclass constructor first), followed by the subclass’s own instance/static properties and methods. So in a subclass, you need to call super() before you can call this to add your own attributes.
Also, in the ES5 example above, the __proto__ attribute of subclass C points not to P, but to function. prototype. Remember that? __proto__ can be understood as the protoobject of the Object’s constructor, and in the ES6 example we actually used the object.create () method to change __proto__. This can be interpreted as one of the important differences: the composition of the prototype chain.
Then it’s time to repeat what we did in the first example, adding the subclass’s methods to the object itself or the prototype object. This is why methods of the same name can be shielded from superclass methods. This part of the code is actually the compilation of a single Class to a function in the first example, so I won’t repeat it here
Fried rice over! It feels like reviewing knowledge points before the gaokao