For other translations of this series, see [JS Working Mechanism – Xiaobai’s 1991 column – Nuggets (juejin. Cn)] (juejin.cn/column/6988… Reading Index for this chapter: 3 This chapter is of little practical significance, but it expands the scope
This chapter discusses various postures for implementing JS classes and inheritance. We’ll start by looking at how prototypes work and examine the way some popular libraries simulate class-based inheritance. Then, we’ll look at how to transform, add some features not supported by native, and use Babel and TS to support classes in the ES2015 specification. Finally, take a look at some examples of how V8 implements class natively.
An overview of
In JS, everything we create is an object and has no primitive type. For example, create a string:
const name = "SessionStack";
Copy the code
Then call the object’s method immediately:
console.log(name.repeat(2)); // SessionStackSessionStack
console.log(name.toLowerCase()); // sessionstack
Copy the code
Unlike other languages, when you declare a string or a number in JS, you automatically create an object. This object contains values and provides different methods on the primitive type
Another interesting thing is that a complex type like array is also an object, and if you examine the Typeof array, you can see that it’s an object. The index of each element in the array is actually the property of the object. So, when we access the index of an array, we actually access the properties of an object. When it comes to data storage, the following two definitions are the same:
Let names = [" SessionStack "]; Let names = {" 0 ":" SessionStack ", "length" : 1}Copy the code
So the performance of accessing elements in an array is the same as accessing properties in an object. Theoretically, accessing an array element should be faster than accessing an object attribute, since objects are hashed. But in JS, arrays and objects are hash mapped and take the same amount of time to access.
Use prototype simulation classes
When you think of objects, you first think of classes. Most of the applications we build are based on classes and relationships between classes. Although objects are ubiquitous in JS, it is not typically class-based integration, but rather, relies on prototypes.
In JS, every object is associated with another object – its prototype. When accessing an object’s properties or methods, it looks first in the object itself and then in its prototype.
Take a look at an example that defines the constructor for our base class
function Component(content) {
this.content = content;
}
Component.prototype.render = function() {
console.log(this.content);
}
Copy the code
We added a Render function to the prototype because we wanted it to be found by every Component class instance. Whenever RENDER is accessed from any instance, it is first looked up in the instance, then looked up in the prototype, and finally found in the prototype.
Now try extending the Component class. Let’s introduce a new Child class.
function InputField(value) {
this.content = `<input type="text" value="${value}" />`;
}
Copy the code
If you want InputField to extend the Component class’s methods and call its Render method, you need to change its prototype. When calling an instance method of the Child class, you don’t want to look up an empty prototype. This lookup extends to the Component class.
InputField.prototype = Object.create(new Component());
Copy the code
In this way, the Render method can be found in the Component class prototype. To implement inheritance, we need to link the InputField prototype to an instance of the Component class. Most libraries use the Object.setPrototypeof method
However, this is not the only thing we can do every time we extend a class:
- Sets the instance of the parent class to be the prototype of the subclass
- The parent constructor is called in the subclass constructor so that the initialization logic of the parent constructor is performed
- Introduces a way to access the parent method. When overriding a superclass method, you want to call its original implementation
As you can see, if you want to implement class-based inheritance, you have to perform complex logic every time. We often need to create many classes, so it makes sense to encapsulate the related code into reusable functions. To solve the problem of class-based inheritance, developers use different libraries to simulate it. The popularity of these solutions highlights the shortcomings of the language itself. Therefore, ECMAScript 2015 introduced a new syntax to implement class-based inheritance.
Class conversion
When new features are proposed in ES6 or ECMAScript 2015, the JavaScript developer community is eager for engine and browser support. A good way to do this is through transcoding. It allows code to be written using ECMAScript 2015 and then converted to JS code that can be run by any browser. This includes using class-based inheritance to write classes and convert them into executable code.
One of the most popular JS converters is Babel. Let’s look at how the transformation works:
class Component {
constructor(content) {
this.content = content;
}
render() {
console.log(this.content)
}
}
const component = new Component('SessionStack');
component.render();
Copy the code
Babel will transform the class definition in this way
var Component = function () { function Component(content) { _classCallCheck(this, Component); this.content = content; } _createClass(Component, [{ key: 'render', value: function render() { console.log(this.content); } }]); return Component; } ();Copy the code
As you can see, the code is converted to ES5 and can be executed in any environment. In addition, some features have been added that are part of the Babel standard library. The compilation results include the _classCallCheck and _createClass functions. _classCallCheck ensures that the builder function is not called as a normal function. This is done by checking whether the context of the function being called is an instance of Component. Check if this refers to the instance. _createClass creates attributes of an object (class) by passing in an array of objects containing keys and values.
To see how inheritance works, let’s examine the InputField class, which inherits from Component
class InputField extends Component { constructor(value) { const content = `<input type="text" value="${value}" />`; super(content); }}Copy the code
Here is the output we get when we process the above example using Babel. The above example, using Babel, produces this output
var InputField = function (_Component) {
_inherits(InputField, _Component);
function InputField(value) {
_classCallCheck(this, InputField);
var content = '<input type="text" value="' + value + '" />';
return _possibleConstructorReturn(this, (InputField.__proto__ || Object.getPrototypeOf(InputField)).call(this, content));
}
return InputField;
}(Component);
Copy the code
In this example, the inherited logic is encapsulated in the _inherits function call. It does the same thing we did earlier which is set the prototype of the subclass to be an instance of the parent class.
Babel did a couple of things. First, ES2015 is parsed and then converted into an intermediate representation — the AST. The AST is then converted to a different syntactic abstraction tree, and each node of the new tree has been converted to the corresponding ES5. Finally, turn the AST into code.
The Babel of AST
All nodes in the AST have only one parent node. In Babel, there is a basic node type. This type indicates what the node looks like and where to find it in the code. There are many different node types, such as Literals for string, numbers, NULls, and so on. There is also a special type of class node. It is a subclass of the base node class and extends itself by adding field variables to store references to the base class and by treating the body of the class as a separate node.
We convert the following code to AST:
class Component {
constructor(content) {
this.content = content;
}
render() {
console.log(this.content)
}
}
Copy the code
Here is its AST:
Once the AST is created, each node is converted to an ES5 equivalent node and then converted back to ES5-compliant code. The converter looks for the nodes farthest from the root node and converts them into code. Their parent nodes are then converted into code. This process is called depth-first traversal.
In the above example, the two MethodDefinition nodes are generated first, then the code for the body node of the class, and finally the code for the ClassDeclaration node.
TypeScript conversion
Another popular transformation framework is TS. TS introduces a new syntax for writing JS applications and then converting them to ES5 code. Look at an example of TS:
class Component {
content: string;
constructor(content: string) {
this.content = content;
}
render() {
console.log(this.content)
}
}
Copy the code
Its AST tree:It also supports inheritance
class InputField extends Component { constructor(value: string) { const content = `<input type="text" value="${value}" />`; super(content); }}Copy the code
The conversion result is as follows:
var InputField = /** @class */ (function (_super) {
__extends(InputField, _super);
function InputField(value) {
var _this = this;
var content = "<input type="text" value="" + value + "" />";
_this = _super.call(this, content) || this;
return _this;
}
return InputField;
}(Component));
Copy the code
The final result is also ES5, with some TS library functions included. __extends encapsulates the same inheritance logic as discussed in the first part. Babel and TS have become widely used, and standard classes and class-based inheritance have become a standard way to build JS applications. This further advances native support for browser classes.
Native support
Chrome introduced native support in 2014. Allows classes to be declared and executed without any libraries or compilers
But native support is actually a syntactic sugar. This is just an elegant syntax that can be converted to the original syntax already supported by the language. Using new, easy-to-use class definitions comes down to creating constructors and modifying prototypes.
V8 support
Take a look at how V8 supports ES2015. As discussed earlier, the new syntax is first converted into valid JS code and then added to the AST. The class definition is converted to a [ClassLiteral] node and added to the AST.
This node stores couple of things. First, it holds the constructor as a separate function. It also holds a list of class properties. They can be a method, a getter, a setter, a public field or a private field. This node also stores a reference to the parent class that this class extends which again stores the constructor, list of properties and the parent class. This node holds two key pieces of information. First, it treats the constructor as a separate function. It also has a set of class properties, such as methods, getters, setters, public fields or private fields. This node also holds a reference to a parent class, which also holds constructors, attribute sets, and parent class references, and so on.
Once the newly generated ClassLiteral has been converted into code, it can be converted into various functions and prototypes