We’ll discuss an old problem: creating object constructors in JavaScript.
Web front-end JavaScript
Existing problems
Suppose we wanted to create the most typical example of object-oriented design: the Circle class. Suppose we’re writing a Circle for a simple Canvas library. In addition, we may want to know how to do the following:
- Draw a given Circle on a given Canvas.
- Record the total number of circles made.
- Tracks the radius of a given circle and how invariants are applied to its value.
- Calculates the area of a given circle.
The current JS convention says that we should first create a function as a constructor; We then add any properties we might want to the function itself, excerpted from the article, and replace the constructor’s Prototype property with an Object. The Prototype object will contain all the properties that the instance object created by the constructor should contain at the beginning. Even a simple example ends up being a lot of boilerplate when you write it all out:
function Circle(radius) {
this.radius = radius;
Circle.circlesMade+ +; //circlesMade is instance independent} Circle.draw = function draw(circle, canvas) { /* Canvas drawing code */ }
Object.defineProperty(Circle, "circlesMade", {
get: function() { return ! this._count ?0 : this._count;
},
set: function(val) { this._count = val; }}); Circle.prototype = {
area: function area() {
return Math.pow(this.radius, 2) * Math.PI; }};Object.defineProperty(Circle.prototype, "radius", {
get: function() {
return this._radius;
},
set: function(radius) { if (! Number.isInteger(radius))
throw new Error("Circle radius must be an integer."); this._radius = radius; }});Copy the code
Not only is the code cumbersome, it’s also unintuitive. It requires a good understanding of how functions work and how various installed properties enter the instance object being created. If this method seems complicated, don’t worry. The point of this article is to show a simpler way to write code.
Syntax for method definitions
ES6 provides a new syntax for adding special properties to objects. While it’s easy to add the area method to Circle. Prototype, adding getter/setter pairs to RADIUS feels a lot more complicated. As JS moves towards a more object-oriented approach, there is interest in designing cleaner ways to add object accessors. We need a new way to add methods to an object, just as methods have been added to obj using obj.prop = method. You don’t need to use complicated apis like Object.defineProperty. People want to be able to do the following things easily:
- Add normal function attributes to the object.
- Add generator function attributes to the object.
- Add normal accessor function attributes to the object.
- Add any of the above, just as you did with the [] syntax on the final object. We call these Computed property names.
There are things you couldn’t do before. For example, there is no way to define a getter or setter to assign to obj.prop. Therefore, new syntax must be added. You can now write code like this:
varObj = {// the method is added without the function keyword, using the attribute name as the function name. method(args) { ... }, // make the method a generator by adding a * * *genMethod(args) {... } / / in | get the help of the | and | set | accessor (accessors) can use inline writing now. // Note that getters registered this way cannot have arguments get propName() {... }, // Note that setters registered in this way can only have one parameter set propName(arg) {... }, // To solve the fourth problem above,[]The syntax can appear anywhere the method name appears. You can use Symbols, call functions, concatenate strings, or any other expression that produces an attribute ID. // This syntax also applies to accessors and generators.[functionThatReturnsPropertyName()](args) { ... }};Copy the code
Using this new syntax, we can now rewrite the above code snippet:
function Circle(radius) {
this.radius = radius;
Circle.circlesMade+ +; } Circle.draw = function draw(circle, canvas) { /* Canvas drawing code */ }
Object.defineProperty(Circle, "circlesMade", {
get: function() { return ! this._count ?0 : this._count;
},
set: function(val) { this._count = val; }}); Circle.prototype = {
area() {
return Math.pow(this.radius.2) * Math.PI; }, get radius() { return this._radius; }, set radius(radius) { if (! Number.isInteger(radius)) throw new Error("Circle radius must be an integer."); this._radius = radius; }};Copy the code
This code is not identical to the code snippet above. Method definitions in object literals are registered as configurable and enumerable, while accessors (get, set) registered in the first code snippet will be non-configurable and non-enumerable. In practice, this is rarely noticed, and I’ve decided to omit the listing and configurability above for brevity.
But things are looking up, right? Unfortunately, even with this new way of defining the syntax, there’s nothing we can do about the Circle definition because we haven’t defined the function yet. When you define a function, there is no way to assign attributes to it.
Class definition syntax
While this is better, it still doesn’t satisfy those who want a cleaner solution to object-oriented design in JavaScript. They argue that other languages have a construct for dealing with object-oriented design called a class.
Very good. So, let’s add classes to JS.
We want a system that allows us to add methods to named constructors and methods to its.prototype so that they appear in class construction instances. Now that we have a fancy new way of defining syntax, we should definitely use it. Then, we just need a way to distinguish between what is common across all instances of a class and what functions are specific to a given instance. In c++ or Java, the keyword is static. It looks as good as the others. Let’s use it.
Now, it would be useful to have a way to specify that one of these methods is called as a constructor. In c++ or Java, it would be named the same as the class, with no return type. Since JS does not return a type, we need a.constructor property, which we call constructor for backward compatibility.
Putting them together, we can override the Circle class:
class Circle {
constructor(radius) {
this.radius = radius;
Circle.circlesMade+ +; }; static draw(circle,canvas{/ /Canvas drawing code}; static get circlesMade() { return ! this._count ?0 : this._count;
};
static set circlesMade(val) {
this._count = val;
};
area() {
return Math.pow(this.radius.2) * Math.PI; }; get radius() { return this._radius; }; set radius(radius) { if (! Number.isInteger(radius))
throw new Error("Circle radius must be an integer.");
this._radius = radius;
};
}
Copy the code
Not only can we put everything related to the circle together, but everything looks clean. It’s definitely better than when we started. Even so, some people may have doubts. I will try to predict and solve the following problems:
-
What’s with the semicolon?
In order to “make things look more like traditional classes,” we decided to use more traditional delimiters. Don’t like it? It is optional. The delimiter is not required.
-
What if I don’t want constructors, but I still want to put methods on the object I’m creating?
That’s no problem. The constructor method is completely optional. If you do not provide one, an empty constructor() {} is added by default.
-
Can a constructor be a generator?
Can’t. Adding a constructor that is not a normal method causes TypeError. This includes generators and accessors.
-
Can I define a constructor with a computed property name ([computed Property Name])?
No. It’s hard to detect, so we’re not going to try. If you define a method with a computed property name that ultimately returns constructor, you still get a method named constructor that ultimately isn’t a constructor of the class.
-
What if I change the value of Circle? Will this lead to new Circle behavior errors?
Won’t! Much like a function expression, a class gets an internal binding with its specified name. External forces cannot change this binding, so no matter what you set the Circle variable in the enclosing scope Circle. CirclesMade++ in the constructor will run as expected.
The article is excerpted from Le Byte