1. Preparations –> Build Babel transcoding environment
This section is not the main point (if you have problems with this section, please refer to here), to make a long story short.
npm init
Copy the code
All press Enter to generate a package.json file and edit it as follows:
{
"name": "class-explain"."version": "1.0.0"."description": ""."main": "index.js"."dependencies": {},
"devDependencies": {
"@babel/cli": "^ 7.12.10"."@babel/core": "^ 7.12.10"."@babel/plugin-proposal-class-properties": "^ 7.12.1"."@babel/preset-env": "^ 7.12.11"
},
"scripts": {
"build": "./node_modules/.bin/babel src --out-dir lib"."test": "echo \"Error: no test specified\" && exit 1"
},
"author": ""."license": "ISC"
}
Copy the code
Create a. Babelrc file with the following contents:
{
"presets": ["@babel/preset-env"]."plugins": ["@babel/plugin-proposal-class-properties"]}Copy the code
At this point, our Babel compilation environment is ready.
2. Write a class file for testing
To demystize class, we’ll cover as much of the syntax in class as possible (let’s leave inheritance aside for the moment, the key syntax is properties (methods), static properties (methods), private properties (methods), and constructors). We’ll write a class that looks like this:
class Point {
// Static attributes
static getName() {
return "Rectangle";
}
/** * Private attributes */
#background = 'red';
/** * Ordinary member attributes */
width = 100;
height = 100;
/** * constructor *@param {Number} x
* @param {Number} y* /
constructor(x, y) {
this.x = x;
this.y = y;
console.log(this.#background);
}
/** * Ordinary member function */
toString() {
return "(" + this.x + "," + this.y + ")"; }}Copy the code
use
npm run build
Copy the code
If the command line executes successfully, we should get something like this:
"use strict";
function _classCallCheck(instance, Constructor) {
if(! (instanceinstanceof 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;
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true.configurable: true.writable: true}); }else {
obj[key] = value;
}
return obj;
}
var _background = new WeakMap(a);var Point = /*#__PURE__*/ (function () {
_createClass(Point, null[{key: "getName".value: function getName() {
return "Rectangle"; }},]);function Point(x, y) {
_classCallCheck(this, Point);
_background.set(this, {
writable: true.value: 'red'}); _defineProperty(this."width".100);
_defineProperty(this."height".100);
this.x = x;
this.y = y;
}
_createClass(Point, [
{
key: "toString".value: function toString() {
return "(" + this.x + "," + this.y + ")"; }},]);returnPoint; }) ();Copy the code
Let’s go through the code one by one:
2.1 Define the invocation form of a class
Since class requires a new call, this code prevents us from calling class directly (when calling a function with new, this points to the constructor, which is one way to strongly bind this and the highest priority form of strong binding (see JavaScript you Don’t Know (part 1)).
// Function definition
function _classCallCheck(instance, Constructor) {
if(! (instanceinstanceof Constructor)) {
throw new TypeError("Cannot call a class as a function"); }}// Function call
function Point(x, y) {
_classCallCheck(this, Point);
// Omit irrelevant code here
}
Copy the code
2.2 Generate class properties and methods, static properties and methods
Then generate properties, methods, static properties, methods for class,
// Define a set of attributes
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) {
// Escape methods or attributes
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
// Escape static methods or properties
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
// A method for defining attributes
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true.configurable: true.writable: true}); }else {
obj[key] = value;
}
return obj;
}
// Add static attributes and methods to class
var Point = /*#__PURE__*/ (function () {
// Add static attributes or methods
_createClass(Point, null[{key: "getName".// Static attributes
value: function getName() {
return "Rectangle"; }},]);function Point(x, y) {
_classCallCheck(this, Point);
_background.set(this, {
writable: true.value: 'red'});// Generate attributes for instances of the class
_defineProperty(this."width".100);
// Generate attributes for instances of the class
_defineProperty(this."height".100);
this.x = x;
this.y = y;
}
/**
* 添加方法
*/
_createClass(Point, [
{
key: "toString".value: function toString() {
return "(" + this.x + "," + this.y + ")"; }},]);returnPoint; }) ();Copy the code
Here there was a very key grammar: Object. DefineProperty (target: the Object, prop: string, descriptor: PropertyDescriptor), please highlight. So this method, if you’re using Vue, is a recursive way of setting a bidirectional binding, so it takes 3 parameters, the first parameter is the source object to set the property, the second parameter is the key value of the property, and the third parameter is a key property called PropertyDescriptor, This property has 6 properties (these 6 properties are very important, especially when combined with the decorator, will appear to change the decadent into magic, I only do syntax introduction here), respectively
1. Enumerable: if it’s true, either for-in or object.keys () will loop through the property.
The property of 64x can be deleted using the delete operator, and if it is set to true, it can be deleted without any additional information.
3. Value, the value of the attribute.
Writable: the value of the property is writable. If false, the value of the property is unwritable.
5. Get, the valuer function, if defined, will be called automatically when we get the property value;
6, set, the assignment function, if defined, when we set the property value, will be called automatically;
The bidirectional binding of Vue dynamically hijacks the content returned by the data function to establish a listening effect, so that the data changes and the view can be updated. DefineProperty also has a sibling called Object.defineProperties, which I won’t repeat here. The result of Babel transcoding is that if a class property or method is defined repeatedly, the later definition overrides the previous one. The static properties or methods of a class can be called directly by the class name. The methods of a class are defined on the primitive constructor, and the properties of a class are defined on the instance of a class.
2.3 Private Attributes
Instead of talking about private properties, let’s look at the transcoding results of Babel to learn about private properties
function _classPrivateFieldGet(receiver, privateMap) {
var descriptor = privateMap.get(receiver);
if(! descriptor) {throw new TypeError("attempted to get private field on non-instance");
}
if (descriptor.get) {
return descriptor.get.call(receiver);
}
return descriptor.value;
}
var _background = new WeakMap(a);var Point = /*#__PURE__*/ (function () {
/** * constructor *@param {Number} x
* @param {Number} y* /
function Point(x, y) {
_classCallCheck(this, Point);
_background.set(this, {
writable: true.value: 200});console.log(_classPrivateFieldGet(this, _background));
}
returnPoint; }) ();Copy the code
Since private properties can only be accessed inside a class, we can see that Babel is using WeakMap here, which is a new method introduced in ES6 to create object-to-value mappings using WeakMap (WeakMap keys can only be objects). Here we set the value through this, so we can only access the value through this, confirming the syntax rule that private properties can only be accessed inside a class. I have to commend this design idea, wonderful, 🤓, and I have also learned a usage scenario of WeakMap (I have discussed with developers about private attribute design in the gold mining forum before).
2.4 Constructors
After looking at the more complex code above, this is relatively simple, no different from what we wrote in ES5.
function Point(x, y) {
// Irrelevant code has been ignored
_classCallCheck(this, Point);
this.x = x;
this.y = y;
}
Copy the code
As you can see, defining properties or methods inside the constructor is done directly on the class instance.
2.5 summary
1. Static properties or methods are called directly from the class (static properties or methods are defined on the constructor);
Properties or access are invoked directly from an instance of the class (methods are defined on the stereotype of the constructor and properties are defined on the instance of the class).
3. Inside the constructor, properties or methods set by this are defined directly on the class instance;
4. Private attributes can only be accessed inside the class (WeakMap sets the reference of this, which can only be accessed through this in this scope, and cannot be accessed outside);
3, class inheritance
If I want to use class, it is the class inheritance syntax, so that JS in the inheritance syntax elegance of the above has a qualitative leap. Let’s continue writing the class file for testing:
import Point from './index.js';
class Rectangle extends Point {
/** * Private attributes */
#alpha = 0;
width = 100;
static height = 100;
constructor() {
super(a); }toString() {
console.log("Hello World");
super.toString();
}
static draw() {}
dispose(){}}Copy the code
Run the command to get the code after Babel transcodes,
"use strict";
// The auxiliary function is lazy loaded
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);
}
var _index = _interopRequireDefault(require("./index"));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
function _classCallCheck(instance, Constructor) {
if(! (instanceinstanceof 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;
}
function _get(target, property, receiver) {
if (typeof Reflect! = ="undefined" && Reflect.get) {
_get = Reflect.get;
} else {
_get = function _get(target, property, receiver) {
var base = _superPropBase(target, property);
if(! base)return;
var desc = Object.getOwnPropertyDescriptor(base, property);
if (desc.get) {
return desc.get.call(receiver);
}
return desc.value;
};
}
return _get(target, property, receiver || target);
}
function _superPropBase(object, property) {
while (!Object.prototype.hasOwnProperty.call(object, property)) {
object = _getPrototypeOf(object);
if (object === null) break;
}
return object;
}
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);
}
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 {
Date.prototype.toString.call(Reflect.construct(Date[],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);
}
// I will not repeat it here
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true.configurable: true.writable: true}); }else {
obj[key] = value;
}
return obj;
}
var _alpha = new WeakMap(a);var Rectangle = /*#__PURE__*/ (function (_Point) {
_inherits(Rectangle, _Point);
var _super = _createSuper(Rectangle);
/** * Private attributes */
function Rectangle() {
var _this;
_classCallCheck(this, Rectangle);
_this = _super.call(this);
_alpha.set(_assertThisInitialized(_this), {
writable: true.value: 0}); _defineProperty(_assertThisInitialized(_this),"width".100);
return _this;
}
_createClass(
Rectangle,
[
{
key: "toString".value: function toString() {
console.log("Hello World");
_get(_getPrototypeOf(Rectangle.prototype), "toString".this).call(this); }, {},key: "dispose".value: function dispose() {},},], [{key: "draw".value: function draw() {},},]);return Rectangle;
})(_index["default"]);
_defineProperty(Rectangle, "height".100);
Copy the code
In addition to the code we’ve already discussed, let’s analyze the code above:
3.1 Lazy Loading
Here, some helper functions use an important coding trick called “lazy loading.” Such as:
function _get(target, property, receiver) {
if (typeof Reflect! = ="undefined" && Reflect.get) {
_get = Reflect.get;
} else {
_get = function _get(target, property, receiver) {
var base = _superPropBase(target, property);
if(! base)return;
var desc = Object.getOwnPropertyDescriptor(base, property);
if (desc.get) {
return desc.get.call(receiver);
}
return desc.value;
};
}
return _get(target, property, receiver || target);
}
Copy the code
The purpose of lazy loading is to reduce unnecessary if-else branch statements and thus improve program execution efficiency. Let’s take the example from the book “JavaScript Advanced Programming” as an example. Suppose we need to encapsulate an Ajax function now (if you use Axios, please give me your contact information and I will help you to contact the best psychiatrist in West China 🤣). Since we need to support IE6, we will judge compatibility. However, most browsers on the market are no longer IE6, and these browsers still need to go through the compatibility if-else branch, the answer is definitely not. Therefore, we use the current host environment can directly support the branch path autotype function, after our function will no longer be the compatibility of judgment, so, after a function has been performed, will greatly improve our performance, and we only loss is the first time in a function to perform when one little performance.
Here we start with the constructor and work our way through the helper functions defined by Babel.
3.2 Associated prototype chain
First of all,
_inherits(Rectangle, _Point);
Copy the code
Rectangle is a subclass and Point is a parent class.
// Core inheritance functions
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);
}
// Set the primitive method
function _setPrototypeOf(o, p) {
// Manually modify the prototype chain if no Object.setPrototypeOf exists
_setPrototypeOf =
Object.setPrototypeOf ||
function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
Copy the code
First of all, let’s introduce the Object.create method. The author borrows flowers from the Buddha and directly posts the explanation of MDN
ObjectThe create (proto, [propertiesObject])Copy the code
Proto The prototype object of the newly created object. PropertiesObject optional. You need to pass in an Object whose property type refers to the second parameter of Object.defineProperties(). If this parameter is specified and not undefined, the incoming object's own enumerable properties (that is, properties defined by itself, rather than enumerated properties in its stereotype chain) will add the specified property value and corresponding property descriptor to the newly created object.Copy the code
So this code inside the core inheritance method
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true.configurable: true}});Copy the code
It means something like: Create creates an Object whose __proto__ points to the superClassd protoobject, but whose constructor points to the subClass itself, and makes the subClass protoobject point to the Object. Open the debugging tool and try it out. Temp = object. create(superClass && superclass. prototype, {constructor: {value: subClass, writable: true, configurable: true }, }) subClass.prototype = temp; Temp.__proto__ === superClass. Prototype (but temp’s constructor is subClass) subClass.prototype.__proto__ === superClass.prototype;
From the _inherits method, we draw two very important conclusions,
1, subclass.prototype.__proto__ === SuperClass. Prototype;
Subclass.__proto__ === SuperClass;
This is exactly the prototypic pointing relation of class described in teacher Ruan Yifeng’s INTRODUCTION to ES6. (I would like to repeat my personal memory of the pointing relationship of this primitive chain. Since static attributes can be inherited, static attributes are directly called by the class name, so we can directly draw the conclusion 2; Since non-private non-static methods or properties other than constructors are inheritable, and these methods or properties are defined on primitives, it follows that 1;)
3.3 Handling of constructors and constructors super
And then:
var _super = _createSuper(Rectangle);
Copy the code
First, there is an important keyword in class: super, through which you can call properties or methods of the parent class (an important way to achieve polymorphism in object-oriented front-end development). Also, if a subclass writes a constructor method, it must call super() at the beginning of the constructor method (if not, the program automatically generates the default) in order to inherit properties or methods defined in the constructor to the subclass. Let’s first look at the implementation of the call to super inside the constructor:
// Auxiliary functions
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);
}
// Auxiliary functions
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; }}// Auxiliary functions
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf
? Object.getPrototypeOf
: function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
// Handle the constructor if it has a return
// There is an interesting phenomenon here
// When we call a function with new, if the constructor returns a value, the underlying type is ignored and the object of new is returned. If the function returns a reference type, the reference type is returned
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
// assert whether super() was called at the beginning of the constructor;
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
/ / define super ()
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived),
result;
// Execute the constructor in order to inherit properties or methods defined in the constructor to subclasses.
// If there is a reflected API, the reflected API is used; otherwise, the parent constructor is called
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this.arguments);
}
return _possibleConstructorReturn(this, result);
};
}
var Rectangle = /*#__PURE__*/ (function (_Point) {
_inherits(Rectangle, _Point);
var _super = _createSuper(Rectangle);
/** * Private attributes */
function Rectangle() {
var _this;
_classCallCheck(this, Rectangle);
_this = _super.call(this);
// Error assertion if super has not been called;
_alpha.set(_assertThisInitialized(_this), {
writable: true.value: 0});// Error assertion if super has not been called;
_defineProperty(_assertThisInitialized(_this), "width".100);
return _this;
}
return Rectangle;
})(_index["default"]);
Copy the code
3.3 Handling of properties or methods called using super
Next, let’s look at what Babel does when we call a super property or function:
// before transcoding:
super.toString();
// After transcoding Babel, try to get the method from the original object of the current class
_get(_getPrototypeOf(Rectangle.prototype), "toString".this).call(this);
// An auxiliary method to get object properties
function _get(target, property, receiver) {
// Use the reflection API if the browser supports reflection
if (typeof Reflect! = ="undefined" && Reflect.get) {
_get = Reflect.get;
} else {
// Otherwise, try to look up the object from the original chain
_get = function _get(target, property, receiver) {
var base = _superPropBase(target, property);
// If the top of the chain is not found, the search is terminated
if(! base)return;
// If found, get the attribute descriptor
var desc = Object.getOwnPropertyDescriptor(base, property);
// Call the getter method if the PropertyDescriptor defines a getter
if (desc.get) {
return desc.get.call(receiver);
}
// Otherwise, return the real attribute value
return desc.value;
};
}
return _get(target, property, receiver || target);
}
// If the top of the chain is not found, then null is returned
function _superPropBase(object, property) {
while (!Object.prototype.hasOwnProperty.call(object, property)) {
object = _getPrototypeOf(object);
if (object === null) break;
}
return object;
}
Copy the code
4, summarize
4.1 Execution Process
Let’s review the class execution flow:
1, first judge whether there is inheritance, if so, first determine the original pointing relationship;
2, and then calls the constructor, if there is no constructor, Mr Into default constructed, or direct calls the constructor, judge whether there is a return in the constructor, if return is the base type, ignored, and returns the object, if the return is an object, it returns this object, otherwise, return to construct objects;
3. Then, initialize the internal properties (including the external properties) of the method and the private properties of the class.
4. Finally initialize your own methods, static properties or methods.
4.2 the conclusion
1. Class is still an ES5 constructor;
Static attributes or methods of a class are defined on the class.
Class methods are defined on class primitives, and attributes are defined on class instances.
4. Inside the constructor of the class, attributes or methods set by this are defined directly on the instance of the class.
5. The private attributes of the class can only be accessed from within the class.
(SuperClass) (SubClass) (SuperClass) (SuperClass) (SubClass) (SuperClass) (SuperClass) __proto__ === SuperClass. Prototype, subclass.__proto__ === SuperClass;
If SubClass inherits from SuperClass, the JS parser should be generated automatically. If SubClass inherits from SuperClass, the constructor must be called super() at the beginning of constructor.
Due to the limited level of the author, it is inevitable that there will be mistakes in the writing process. If there are any mistakes, please correct them. Your opinions will help me make better progress. If you want to reprint this article, please contact the author at [email protected]🥰