It is said that the ES6 Class is the syntactic sugar of ES5, so how is the ES6 Class implemented? What is the principle of its realization of inheritance? Let's use Babel to translate the code to see what the doorway is.
This article will start with the simplest code and analyze the related principles and the role of each function step by step. The code translation can be directly at 160b1e681c4bb5 Babel official website .
How ES6 Class is implemented
Let's start with the simplest Parent class:
class Parent{
constructor(){
this.a = 1
this.getA = function(){}
}
}
The result after translation is:
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Parent = function Parent() {
"use strict";
_classCallCheck(this, Parent);
this.a = 1;
this.getA = function () {};
};
As you can see, the class here is essentially the constructor in ES5. In addition to adding instance attributes and instance methods, it also calls a _classCallCheck
function.
_classCallCheck
function
This function will accept an instance and constructor as parameters. The internal instance instanceof Constructor
used to determine whether the class is called by new, and if not, an error will be thrown.
Next we try to add prototype methods and static methods to this class:
class Parent{
constructor(){
this.a = 1
this.getA = function(){}
}
getB(){}
getC(){}
static getD(){}
static getE(){}
}
After translating, get:
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
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);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
var Parent = /*#__PURE__*/ (function () {
"use strict";
function Parent() {
_classCallCheck(this, Parent);
this.a = 1;
this.getA = function () {};
}
_createClass(
Parent,
[
{
key: "getB",
value: function getB() {}
},
{
key: "getC",
value: function getC() {}
}
],
[
{
key: "getD",
value: function getD() {}
},
{
key: "getE",
value: function getE() {}
}
]
);
return Parent;
})();
emmm seems a bit complicated, but it doesn't matter, we just need to sort out the functions one by one.
It can be seen that the Parent
at this time has become an IIFE. After the IIFE is executed, it still returns to the Parent class, but it also encapsulates a call to the _createClass
_createClass
function
What does the _createClass
First, it can accept three parameters:
- The first parameter: class (here is class
Parent
- The second parameter: an array of objects, each object is a characteristic description object about the prototype method of the class (here are
getB
andgetC
) - The third parameter: the array of storage objects, each object is a characteristic description object about the static method of the class (here
getD
andgetE
)
Then, it will check whether the second and third parameters are passed in turn. If so, it will call the _defineProperties
function to define the prototype method for the prototype of the class and the static method for the class itself.
_defineProperties
function
What does the _defineProperties
It accepts a class (or class prototype) and an array of objects as parameters, and then traverses each object in the array, defines the characteristics of each method, and adds them to the class (or class prototype) one by one. The features involved here include:
enumberable
: Whether the attribute (method) is enumerable. If the method itself has defined the characteristic, then the characteristic is used; if not defined, the method is defined as non-enumerableconfigurable
: Whether the attribute (method) can be configuredwritable
: If the attribute is a data attribute instead of an accessor attribute, there will be avalue
, and then set the attribute to be writable
How ES6 inheritance is implemented
Okay, after we have basically figured out the principle of a class, let's now take a look at how ES6 implements inheritance.
Translate the following code:
class Parent{
constructor(){
this.a = 1
this.getA = function(){}
}
getB(){}
getC(){}
static getD(){}
static getE(){}
}
class Son extends Parent{
constructor(){
super()
}
}
Got:
"use strict";
function _typeof(obj) {
"@babel/helpers - typeof";
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
_typeof = function _typeof(obj) {
return typeof obj;
};
} else {
_typeof = function _typeof(obj) {
return obj &&
typeof Symbol === "function" &&
obj.constructor === Symbol &&
obj !== Symbol.prototype
? "symbol"
: typeof obj;
};
}
return _typeof(obj);
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "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);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf =
Object.setPrototypeOf ||
function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived),
result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
}
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError(
"this hasn't been initialised - super() hasn't been called"
);
}
return self;
}
function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Boolean.prototype.valueOf.call(
Reflect.construct(Boolean, [], function () {})
);
return true;
} catch (e) {
return false;
}
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf
? Object.getPrototypeOf
: function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
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);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
var Parent = /*#__PURE__*/ (function () {
function Parent() {
_classCallCheck(this, Parent);
this.a = 1;
this.getA = function () {};
}
_createClass(
Parent,
[
{
key: "getB",
value: function getB() {}
},
{
key: "getC",
value: function getC() {}
}
],
[
{
key: "getD",
value: function getD() {}
},
{
key: "getE",
value: function getE() {}
}
]
);
return Parent;
})();
var Son = /*#__PURE__*/ (function (_Parent) {
_inherits(Son, _Parent);
var _super = _createSuper(Son);
function Son() {
_classCallCheck(this, Son);
return _super.call(this);
}
return Son;
})(Parent);
It seems that emmm is getting more and more complicated. It's okay. Let's simplify it a bit (the functions explained above are skipped here), and then analyze them one by one slowly:
"use strict";
function _typeof(obj) { ... }
function _inherits(subClass, superClass) { ... }
function _setPrototypeOf(o, p) { ... }
function _createSuper(Derived) { ... }
function _possibleConstructorReturn(self, call) { ... }
function _assertThisInitialized(self) { ... }
function _isNativeReflectConstruct() { ... }
function _getPrototypeOf(o) { ... }
function _classCallCheck() { ... }
function _defineProperties() { ... }
function _createClass() { ... }
var Parent = /*#__PURE__*/ (function () {
function Parent() { ... }
_createClass(...);
return Parent;
})();
var Son = /*#__PURE__*/ (function (_Parent) {
_inherits(Son, _Parent);
var _super = _createSuper(Son);
function Son() {
_classCallCheck(this, Son);
return _super.call(this);
}
return Son;
})(Parent);
There are many new functions here. Some functions are not the focus of our discussion, and they can be analyzed separately. So here is a brief introduction to their functions, and then if you forget the functions of the functions, turn to here. Just look.
_typeof(obj)
function _typeof(obj) {
"@babel/helpers - typeof";
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
_typeof = function _typeof(obj) {
return typeof obj;
};
} else {
_typeof = function _typeof(obj) {
return obj &&
typeof Symbol === "function" &&
obj.constructor === Symbol &&
obj !== Symbol.prototype
? "symbol"
: typeof obj;
};
}
return _typeof(obj);
}
This is a tool function introduced by Babel, mainly for correct processing Symbol
It will first check whether the current environment supports the native Symbol
, and if it supports it, it will directly return typeof obj
expression; if it does not, it will check obj
is an instance of 060b1e681c538f implemented by Symbol
, and if it is, it will return its type ( That is, return "symbol"
), if not, return the calculation result of typeof obj
Here, this function assumes that our current environment supports Symbol
or through polyfill.
_setPrototypeOf()
function _setPrototypeOf(o, p) {
_setPrototypeOf =
Object.setPrototypeOf ||
function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
First check whether the current environment supports the direct calling of the Object.setPrototypeOf()
method. If it does not, use __proto__
manually establish a prototype relationship for the instance ( __proto__
is an exposed internal property, and direct operation is generally not recommended).
_possibleConstructorReturn(self,call)
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
If you have seen new
or [[Construct]]
, you will know that after assigning a non-empty object or function to the constructor as the return value, the return value after calling the function will not be an instance, but this object or function. Here is _possibleConstructorReturn
to accomplish this-look carefully at its name, does it mean "the value that the constructor may return"?
This function accepts two parameters, self
represents an instance of the constructor function, and call
represents the return value of the constructor function. The internal judgment is also very simple. call && (_typeof(call) === "object" || typeof call === "function")
is to check call
. When it is an object (note that typeof
is used for checking, it needs to exclude the possibility of null
) or function, it is directly used as the return value; otherwise, it is Return _assertThisInitialized(self)
. Wait, why is there a new function? Don't worry, we will then see what this function is for.
_assertThisInitialized(self)
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError(
"this hasn't been initialised - super() hasn't been called"
);
}
return self;
}
Look at the name of this function-"Assert this
has been initialized", that is to say, when calling this method, the result we expect is that this
has been initialized. Here, if the check finds that this
is undefined
, an error will be thrown, prompting us that because super()
not called, we cannot get this
; otherwise, it returns this
. Why use void 0
instead of undefined
? Because 060b1e681c5646 may be rewritten in non-strict mode, it is undefined
void 0
here.
_isNativeReflectConstruct()
function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Boolean.prototype.valueOf.call(
Reflect.construct(Boolean, [], function () {})
);
return true;
} catch (e) {
return false;
}
}
This method is used to detect whether the current environment supports native Reflect
. Why should this check be done? We will explain later.
Well, we have analyzed the role of these functions, and now turn directly to the bottom code, starting from the Son
subclass:
var Son = /*#__PURE__*/ (function (_Parent) {
_inherits(Son, _Parent);
var _super = _createSuper(Son);
function Son() {
_classCallCheck(this, Son);
return _super.call(this);
}
return Son;
})(Parent);
Son
here is also an IIFE, and actually returns a Son
subclass constructor. The difference is that it also encapsulates other method calls. Let's take a look at the effects of these methods one by one.
_inherits(Son,_Parent)
function _inherits(subClass, superClass) {
if (typeof superClass !== "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);
}
_inherit
is one of the core methods to implement inheritance. It can be said that its essence is the parasitic combined inheritance in ES5. This method accepts a parent class and a child class as parameters. First, it checks whether the parent class is a function or null
. If not, an error is thrown (why the parent class can be null
? See JS inheritance from extends Explain, I won’t repeat it here).
Then, call Object.create
set the prototype of the parent class to __proto__
subclass prototype. Here we will see that the second parameter is also passed in. This parameter is the descriptor of the attribute of the subclass prototype. We set the constructor
attribute, set it to be writable, configurable, and use value
constructor
points due to rewriting the prototype of the subclass. Why not set enumerable: false
? Because it is not enumerable by default, it does not need to be set.
Finally, we set the __proto__
to point to the parent class, which is not available in ES5. The purpose is to let the subclass inherit the static method of the parent class (methods that can be called directly through the class).
As you can see, by calling the _inherit
function, we have successfully allowed the child class to inherit the prototype method and static method of the parent class. However, how can the properties on the instance be inherited? This will continue to look down, and then we call the _createSuper()
function and pass in the derived class (subclass), this is not the point, the point is that it creates and returns another function _super
.
_super.call(this)
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived),
result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
}
The _createSuperInternal
here is _super
. When calling, we bind its internal this
as a subclass instance.
Before it first according to _isNativeReflectConstruct
check whether the current environment supports Reflect
, if supported, perform result = Reflect.construct(Super, arguments, NewTarget)
, otherwise execution result = Super.apply(this, arguments)
.
Reflect
should be used first. When performing Reflect.construct(Super, arguments, NewTarget)
time, will eventually return based Super
instance of the parent class constructor created, equivalent to the implementation of the new Super(...arguments)
, however, this instance __proto__
of constructor
is NewTarget
, so in a way, you can say This is an instance of a subclass, but it has all the attributes of an instance of the parent class.
You might say, isn't this the same as the following Super.apply
(borrowing constructor inheritance)? No. When we use Super.apply
, the new.target attribute will actually be lost, as shown below:
function Super(){
console.log(new.target)
}
new Super() // Super
Super.apply({}) // undefined
But if you use Reflect.consturct
to create the object, new.target
will not be lost:
function Super1(){
console.log('Super1')
console.log(new.target)
}
function Super2(){
console.log('Super2')
console.log(new.target)
}
const obj1 = Reflect.construct(Super1,{})
// 'Super1'
// Super1
const obj2 = Reflect.construct(Super1,{},Super2)
// 'Super1'
// Super2
It can be seen even in the absence through new
to call Super1
, new.target
still point to Super1
; and after the pass a third parameter, new.target
have not lost, just point to the Super2
(as we said, and to some extent, can Say obj1
is an instance of Super2
Reflect
is preferentially used here to ensure that new.target
will not be lost.
After that, result
may have three possible values:
- A subclass instance that inherits all the attributes of the parent class instance
- The call result of the parent class constructor may be a non-empty object returned from the parent class constructor
- The call result of the parent class constructor may be
undefined
How to deal with these different situations? _possibleConstructorReturn(this,result)
function mentioned above is called here. If it result
is a non-empty object, that is, the first and second value cases, then it directly returns result
; otherwise, it is the third case, and it is right at this time The subclass instance passed in at the beginning ( Super.apply
), that is, this
, is asserted, and then returned.
Now, let's go back to the Son
constructor. As you can see, it is _super.call(this)
that is returned after calling it, that is, result
or the enhanced this
is returned. Here result
we know that there are also two values, if it is a subclass instance that inherits all the attributes of the parent class instance, it is actually equivalent to the enhanced this
; if it is a non-defined return from the parent class constructor An empty object means that Son
constructor does not actually inherit the instance attributes declared in the parent class. Similar to the following:
function Parent(){
this.a = 1
return {b:1}
}
function Son(){
return Parent.call(this)
}
Son.prototype.__proto__ = Parent.prortotype
const obj = new Son()
console.log(obj)
// {b:1}
// 这里 `Son` 同样也是返回父类 `Parent` 的调用结果(一个对象),它并没有继承在父类上声明的实例属性 `a`。
At this point, our analysis is basically over. I hope you will learn something after reading this article. If you find any errors in the article, please feel free to correct me in the comment section.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。