- Better JavaScript with ES6, pt. II: A Deep Dive into Classes
- The Nuggets translation Project
- Translator: Malcolm
- Proofread by: linkage, Jack-kingdom
For old
At the beginning of this article, we want to say one thing:
In essence, ES6 classes are mostly a convenient syntax for creating old-fashioned constructors, not new wizardry. Axel Rauschmayer, ES6 author
Functionally, the class declaration is a syntactic sugar, just a little more powerful than the prototype-based behavior delegate we’ve been using. This paper will start with the relationship between new grammar and prototype, and carefully study the class keyword of ES2015. The following contents will be mentioned in the article:
- Define and instantiate classes;
- use
extends
Create a subclass; - subclasses
super
Statement invocation; - And an important example of symbol method.
Along the way, we’ll pay special attention to how class declaration syntax essentially maps to prototype-based code.
Let’s start at the beginning.
Take a step back: Classesnotwhat
JavaScript “classes” are different from classes in Java, Python, or any other object-oriented language you might have used. The latter might be more accurately called a “class-oriented” language.
In a traditional class-oriented language, we create classes that are templates for objects. When we need a new object, we instantiate the class. This step tells the language engine to copy the methods and properties of the class onto a new entity, called an instance. Instances are our own objects and have no intrinsic connection to the parent class after instantiation.
JavaScript has no such replication mechanism. “Instantiating” a class in JavaScript creates a new object, but the new object is not independent of its parent class.
Instead, it creates an object connected to the prototype. Even after instantiation, changes to the stereotype are passed to the new object being instantiated.
Prototyping is an incredibly powerful design pattern in itself. Class provides a concise syntax for many technologies that use prototypes to mimic the mechanics of traditional classes.
To sum up:
- JavaScript does not have the class concepts of Java and other object-oriented languages;
- JavaScript
class
Much of it is just the syntactic sugar of stereotype inheritance, as opposed to traditional class inheritanceBig difference.
With that out of the way, let’s look at class first.
Class basics: declarations and expressions
We create the class using the class keyword, followed by a variable identifier, and finally a block of code called the class body. This writing is called a class declaration. Class declarations that do not use the extends keyword are called base classes:
"use strict"; Class Food {constructor (name, protein, carbohydrates, fat) {this.name = name; // Food is a base class. this.protein = protein; this.carbs = carbs; this.fat = fat; } toString () { return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F` } print () { console.log( this.toString() ); }} const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5); chicken_breast.print(); P: / / 'Chicken Breast | 26 g: 0 g C: : 3.5 g F' console. The log (chicken_breast. Protein); // 26 (LINE A)Copy the code
Note the following:
- Classes can only contain method definitions, not data attributes;
- When defining a method, you can use a shorthand method definition.
- Unlike creating objects, we cannot separate method definitions with commas in the body of the class;
- We can refer directly to the class’s properties (such as LINE A) on the instantiated object.
Classes have a unique feature called the Contructor constructor. In the constructor we can initialize the properties of the object.
A constructor definition is not required. If we do not write the constructor, the engine inserts an empty constructor for us:
"use strict"; Class NoConstructor {/* JavaScript inserts this code: constructor () {} */} const nemo = new NoConstructor(); // It works, but it's not interestingCopy the code
Assigning a class to a variable is called a class expression. This is an alternative to the syntax above:
"use strict"; Const Food = class {// Same class definition as above... } // This is a named class expression, we can refer to it by name in the class body const Food = class FoodClass {// Same class definition as above... // Add a new method to prove that we can reference FoodClass by internal name... printMacronutrients () { console.log(`${FoodClass.name} | ${FoodClass.protein} g P :: ${FoodClass.carbs} g C :: ${foodclass.fat} g F ')}} const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5); ${foodclass.fat} g F ')}} const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5); chicken_breast.printMacronutrients(); P: / / 'Chicken Breast | 26 g: 0 g C: : 3.5 g F' / / but not in the external reference try {the console. The log (FoodClass. Protein); } catch (err) {// pass}Copy the code
This behavior is similar to that of anonymous and named function expressions.
useextends
Create subclasses and use super calls
Classes created using extends are called subclasses, or derived classes. This usage is straightforward, so we build it directly in the example above:
"use strict"; Class FatFreeFood extends Food {constructor (name, protein, carbs) {super(name, protein, carbs, 0); } print () { super.print(); console.log(`Would you look at that -- ${this.name} has no fat! `); } } const fat_free_yogurt = new FatFreeFood('Greek Yogurt', 16, 12); fat_free_yogurt.print(); // 'Greek Yogurt | 26g P :: 16g C :: 0g F / Would you look at that -- Greek Yogurt has no fat! 'Copy the code
Derived classes have all the features of base classes we discussed above, but there are several new features:
- Use a subclass
class
Keyword declaration, followed by an identifier, then usedextend
Keyword, write one lastArbitrary expression. This expression is usually just an identifier, butIt could theoretically be a function. - If your derived class needs to reference its parent class, it can
super
The keyword. - A derived class cannot have an empty constructor. Even if the constructor is called once
super()
You also have to write it explicitly. Derived classes doThere is noConstructor. - In the constructor of the derived class,Must beFirst call
super
Before being usedthis
This is only true in constructors, but can be used directly in other methodsthis
).
There are only two usage scenarios for the super keyword in JavaScript:
- Called in the subclass constructor. If initializing the derived class is a constructor that needs to use the parent class, we can call it in the constructor of the subclass
super(parentConstructorParams)
Pass any required parameters. - A method that references a parent class. In a regular method definition, derived classes can use dot operators to refer to methods of their parent class:
super.methodName
.
Our FatFreeFood demonstrates both cases:
- In the constructor, we simply call
super
And pass the amount of fat into0
. - In our
print
Method, we called it firstsuper.print
“Before adding the other logic.
Believe it or not, I’m convinced that this covers the basic syntax of class, and that’s all you need to know to get started.
Deep learning prototype
Now let’s look at how class maps to the prototype mechanism inside JavaScript. We will focus on the following:
- Create an object using a construct call;
- The nature of prototype connection;
- Property and method delegates;
- Use prototype simulation classes.
Create an object using a construct call
Constructors are nothing new. Calling any function with the new keyword causes it to return an object — this step is called to create a constructor call, which is usually called a constructor:
"use strict"; function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } // Const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5); Console. log(chicken_breast.protein) // 26 // Undefined 'const fish = Food('Halibut', 26, 0, 2); console.log(fish); // 'undefined'Copy the code
When we call a function using the new keyword, the following four steps are performed internally:
- Create a new object (call it O here);
- Give O a link to other objects, called a prototype;
- The function of the
this
The reference is toO; - The function implicitly returns O.
Between steps 3 and 4, the engine executes the specific logic in your function.
Knowing this, we can rewrite the Food method to work without the new keyword:
"use strict"; Function Food (name, protein, carbohydrates, fat) {// First step: create a new object const obj = {}; // Step 2: Link prototypes -- We'll explore the concept of object.setprototypeof (obj, food.prototype) in more detail later; // We are using 'obj' instead of 'this' to simulate the third step, obj.name = name; obj.protein = protein; obj.carbs = carbs; obj.fat = fat; // Step 4: Return obj; } const fish = Food('Halibut', 26, 0, 2); console.log(fish.protein); / / 26Copy the code
Three of the four steps are straightforward. Creating an object, assigning a property, and then writing a return declaration is not a problem for most developers to understand — yet it’s the dark magic archetype that stumps everyone.
Intuitively understand the prototype chain
In general, all objects in JavaScript, including functions, link to another object, which is called a prototype.
If we access a property that doesn’t exist on the object itself, JavaScript checks for that property on the prototype of the object. In other words, if you ask an object for attributes it doesn’t have, it will say to you, “I don’t know that, ask my prototype.”
The process of finding nonexistent properties on another object is called delegation.
"use strict"; // Joe has no toString method... const joe = { name : 'Joe' }, sara = { name : 'Sara' }; Object.hasOwnProperty(joe, toString); // false Object.hasOwnProperty(sara, toString); / / false / /... But we can still call it! joe.toString(); // '[object object]' instead of a reference error! sara.toString(); // '[object object]' instead of a reference error!Copy the code
Although our toString output is completely useless, please note that this code does not raise any ReferenceError! This is because although Joe and Sara don’t have toString attributes, their prototypes do.
When we look for the sara.toString() method, Sara says, “I don’t have toString, find my prototype.” As mentioned above, JavaScript kindly asks Object.prototype if it has a toString attribute. Since the prototype has this property, JS returns the toString from Object.prototype to our program and executes it.
It doesn’t matter that Sara doesn’t have attributes — we’ll delegate the lookup to the prototype.
In other words, we can access properties that don’t exist on an object as long as its prototype has those properties. We can use this to assign properties and methods to the object’s prototype, and then we can call those properties as if they actually exist on that object.
Even better, if several objects share the same stereotype — as in Joe and Sara’s example above — when we assign attributes to the stereotype, they are all accessible, without having to copy those attributes to each object individually.
That’s why they call it stereotype inheritance — if my object doesn’t have it, but my object’s stereotype does, then my object can inherit that property too.
In fact, there is no “inheritance” happening here. In class-oriented languages, inheritance refers to the act of copying a property from a parent class to a child class. In JavaScript, this copying doesn’t happen, and in fact this is one of the main advantages of stereotype inheritance over class inheritance.
Before we dive into how archetypes actually come about, let’s do a quick recap:
joe
和sara
There is noInherit onetoString
The properties of the;joe
和sara
In fact, fundamentalThere is nofromObject.prototype
On “inheritance”;joe
和sara
是linktheObject.prototype
On;joe
和sara
Link to theThe sameObject.prototype
On.- If we want to find an object (let’s call itO) prototype, we can use
Object.getPrototypeof(O)
.
Then we repeat: objects do not “inherit” from their archetypes. They just delegate to the prototype.
The above.
Let’s take a look inside.
Sets the prototype of the object
We’ve learned that basically every object (referred to as O below) has a prototype (referred to as P below), and then when we look for a property that’s not on O, the JavaScript engine looks for that property on P.
So we have two questions:
- How to play the above case function?
- Where do these prototypes come from?
A function named Object
Before the JavaScript engine executes the program, it creates an environment for the program to execute internally. Within the execution environment, it creates a function called Object and an associated Object called Object.Prototype.
In other words, Object and Object.prototype are always present in any executing JavaScript program.
This Object looks like any other function at first glance, but what makes it special is that it is a constructor that returns a new Object when called:
"use strict"; typeof new Object(); // "object" typeof Object(); // This Object function does not require the new keyword to be calledCopy the code
This Object. Prototype Object is a… Object. Just like any other object, it has properties.
Here’s what you need to know about Object and Object.prototype:
Object
functionThere’s one called.prototype
Property pointing to an object (Object.prototype
);Object.prototype
objectThere’s one called.constructor
Property pointing to a function (Object
).
In fact, this general scheme applies to all functions in JavaScript. When we create a function — hereafter called someFunction — that function will have a.prototype property that points to an object called someFunction.prototype.
In contrast, the somefunction. prototype object has a property called.contructor whose reference refers back to the function someFunction.
"use strict"; function foo () { console.log('Foo! '); } console.log(foo.prototype); / / points to an object called 'foo'. The console log (foo. Prototype. Constructor); / / to 'foo' function foo prototype. The constructor (); / / output 'Foo! '- to prove that there are only' foo prototype. The constructor 'such a method and point to the functionCopy the code
Here are some key points to keep in mind:
- All functions have a property called
.prototype
, which points to the function’s associated object. - All function prototypes have a property called
.constructor
That points to the function itself. - A function prototype
.constructor
You don’t have to point to the function that created the prototype… It’s a little convoluted, and we’ll talk more about that in a minute.
There are some rules for prototyping functions. Before we begin, let’s outline three rules for prototyping objects:
- “Default” rules;
- use
new
Implicit setting prototype; - use
Object.create
Explicitly set the stereotype.
The default rules
Consider this code:
"use strict";
const foo = { status : 'foobar' };
Copy the code
Very simple. What we do is we create an object called foo and give it a property called Status.
Then JavaScript does a little more work behind the scenes. When we create an Object literally, JavaScript points the Object’s prototype to Object.prototype and sets its.constructor to Object:
"use strict";
const foo = { status : 'foobar' };
Object.getPrototypeOf(foo) === Object.prototype; // true
foo.constructor === Object; // true
Copy the code
usenew
Implicitly set the stereotype
Let’s look at the Food example we adjusted earlier.
"use strict";
function Food (name, protein, carbs, fat) {
this.name = name;
this.protein = protein;
this.carbs = carbs;
this.fat = fat;
}
Copy the code
Now we know that the function Food will be associated with an object called food.prototype.
When we create an object using the new keyword, JavaScript will:
- Set this object’s prototype to point to our use
new
Of the function called.prototype
Properties; - Of this object
.constructor
Point us to usenew
The constructor called to.
const tootsie_roll = new Food('Tootsie Roll'.0.26.0);
Object.getPrototypeOf(tootsie_roll) === Food.prototype; // true
tootsie_roll.constructor === Food; // trueCopy the code
This allows us to do the following dark magic:
"use strict"; Food.prototype.cook = function cook () { console.log(`${this.name} is cooking! `); }; const dinner = new Food('Lamb Chops', 52, 8, 32); dinner.cook(); // 'Lamb Chops are cooking! 'Copy the code
useObject.create
Explicitly set the stereotype
Finally, we can manually set the Object’s prototype reference using the object.create method.
"use strict"; const foo = { speak () { console.log('Foo! '); }}; const bar = Object.create(foo); bar.speak(); // 'Foo! ' Object.getPrototypeOf(bar) === foo; // trueCopy the code
Remember the four things JavaScript does behind the scenes when you call a function with new? Object. Create does three things:
- Create a new object;
- Set its prototype reference;
- Return this new object.
You can look at the polyfill on MDN for yourself. (Polyfill is a patch code that implements a new function for the old code. This means that the old version of JS does not have the Object. Create function.
simulationclass
behavior
Using prototypes directly to simulate class-oriented behavior requires some finesse.
"use strict"; function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } Food.prototype.toString = function () { return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`; }; function FatFreeFood (name, protein, carbs) { Food.call(this, name, protein, carbs, 0); } / / set "ttf_subclass relations / / = = = = = = = = = = = = = = = = = = = = = / / LINE A: : FatFreeFood. Prototype = object.create (food.prototype); Constructor (FatFreeFood. Constructor, "constructor", {enumerable: false, writeable : true, value : FatFreeFood });Copy the code
In Line A, we need to set FatFreeFood. Prototype to A new object whose prototype reference is Food.prototype. If we do not, our subclasses cannot access the methods of the superclass.
Unfortunately, this leads to rather bizarre results: FatFreeFood. Constructor is Function, not FatFreeFood. To keep everything working, we need to manually set FatFreefood.constructor on Line B.
One of the motivations for the class keyword is to free developers from the awkwardness of mimicking class behavior with prototypes. It does offer a solution to avoid the common pitfalls of stereotype syntax.
Now that we’ve explored so much about JavaScript prototyping, it’s easy to see how the class keyword makes things easier.
Let’s dig a little deeper
Now that we understand the need for a JavaScript prototype system, we’ll close this article by delving into three methods supported by classes, as well as a special case.
- The constructor;
- Static method;
- Prototype method;
- A special case of a prototype method: the “tag method.”
Not the three sets of methods I proposed, thanks to Dr. Rauschmayer’s definition in his book Exploring ES6.
Class constructor
A class’s constructor method is used to focus on our initialization logic, and the constructor method has several special points:
- Only in constructors can we call the parent class’s constructor;
- It handles all the work of setting up the prototype chain behind the scenes;
- It is used as a class definition.
The second point is that one of the main benefits of using classes in JavaScript is to quote the title of 15.2.3.1 from exploring ES6:
The prototype of a subclass is a superclass
As we’ve seen, manual setup is tedious and error-prone. If we use the class keyword, JavaScript takes care of the setup internally, which is another advantage of using class.
The third point is interesting. In JavaScript, a class is just a function — it’s equivalent to the constructor method in a class.
"use strict"; Class Food {// Same class definition as before... } typeof Food; // 'function'Copy the code
Unlike the usual way of using a function as a constructor, we cannot call a class constructor directly without using the new keyword:
const burrito = Food('Heaven', 100, 100, 25); // Type error
This raises another question: what happens when we call the function constructor without new?
The short answer is: for any function that does not explicitly return undefined. We just need to trust that any user who uses our constructor will use the constructor call. That’s why the community has agreed to capitalize the constructor: to remind the consumer to call it with new.
"use strict";
function Food (name, protein, carbs, fat) {
this.name = name;
this.protein = protein;
this.carbs = carbs;
this.fat = fat;
}
const fish = Food('Halibut', 26, 0, 2); // D'oh . . .
console.log(fish); // 'undefined'
Copy the code
The longer answer is: return undefined unless you manually check to see if the use is called by new and then do your own processing.
ES2015 introduces an attribute to make this detection easy: [new.target](developer.mozilla.org/en-US/docs/…) .
New. target is a property defined on all functions called with new, including class constructors. When we call a function using the new keyword, the value of new.target in the function body is the function itself. This value is undefined if the function is not called by new.
"use strict"; Function Food (name, protein, carbs, fat) {// If the user forgot to manually call if (! new.target) return new Food(name, protein, carbs, fat); this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } const fish = Food('Halibut', 26, 0, 2); // No problem! fish; // 'Food {name: "Halibut", protein: 20, carbs: 5, fat: 0}'Copy the code
It works fine in ES5:
"use strict"; function Food (name, protein, carbs, fat) { if (! (this instanceof Food)) return new Food(name, protein, carbs, fat); this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; }Copy the code
The MDN documentation goes into more detail about new.target and includes the ES2015 specification as a reference for those interested. The description of [[Construct]] in the specification is instructive.
A static method
Static methods are the constructor’s own methods and cannot be called by the class’s instantiation object. We use the static keyword to define static methods.
"use strict"; Class Food {// Same as before... // Add static describe () {console.log(' 'Food' is a data type that stores nutritional information '); } } Food.describe(); // 'Food' is a data type that stores nutritional information 'Copy the code
Static methods are similar to direct attribute assignment in older constructors:
"use strict"; function Food (name, protein, carbs, fat) { Food.count += 1; this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } Food.count = 0; Food.describe = function count () {console.log(' you created ${food.count} Food '); }; const dummy = new Food(); Food.describe(); // "You created a food"Copy the code
Prototype method
Any method that is not a constructor and a static method is a prototype method. The prototype method is called because we did this by attaching methods to the prototype of the constructor.
"use strict"; // Use ES6: class Food {constructor (name, protein, carbohydrates, fat) {this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } toString () { return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`; } print () { console.log( this.toString() ); }} // In ES5: function Food (name, protein, fat) {this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } // The name "prototype method" probably comes from the way we used to do this by attaching methods to the prototype of the constructor. Food.prototype.toString = function toString () { return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`; }; Food.prototype.print = function print () { console.log( this.toString() ); };Copy the code
It should be noted that generators are perfectly acceptable for method definition.
"use strict"; class Range { constructor(from, to) { this.from = from; this.to = to; } * generate () { let counter = this.from, to = this.to; while (counter < to) { if (counter == to) return counter++; else yield counter++; } } } const range = new Range(0, 3); const gen = range.generate(); For (let val of range.generate()) {console.log(' Generator is ${val}. '); // Prints: // Generator has the value 0. // Generator has the value 1. // Generator has the value 2.Copy the code
Mark method
Finally, let’s talk about the notation method. These are methods called Symbol values that the JavaScript engine recognizes and uses when we use the built-in constructor in a custom object.
The MDN documentation provides a brief overview of what a Symbol is:
Symbol is a unique and immutable data type that can be used as an attribute identifier for an object.
Creating a new symbol gives us a value that is considered unique to the program. This is useful for naming attributes of objects: we can be sure that we don’t accidentally overwrite any attributes. Using symbols for keys is also not infinite, so they are largely invisible to the outside world (not exactly, available via reflect.ownkeys)
"use strict"; Const secureObject = {// This key can be considered unique [new Symbol("name")] : 'Dr. Secure A. F.'}; console.log( Object.getKeys(superSecureObject) ); // [] -- Get console.log(reflect.ownkeys (secureObject)); // [Symbol("name")] -- but not completely hiddenCopy the code
More interestingly for us, this gives us a way to tell the JavaScript engine to use a particular method for a particular purpose.
So-called “well-known symbols” are the keys of specific objects, and the JavaScript engine fires specific methods when you use them in defining objects.
This is a bit weird for JavaScript, so let’s look at an example:
"use strict"; // Inheriting Array allows us to intuitively use 'length' and also gives us access to built-in methods, Class FoodSet extends Array {// Foods collects any parameters passed into an Array // See also: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator constructor(... foods) { super(); this.foods = []; Foods.foreach ((food) => this.foods.push(food))} * [symbol.iterator] () {let position = 0; while (position < this.foods.length) { if (position === this.foods.length) { return "Done!" } else { yield `${this.foods[ position++ ]} is the food item at position ${position}`; }} // When our user uses the built-in array method, return an array type object // instead of a FoodSet type object. Static get [symbol.species] () {return Array; static get [symbol.species] () {return Array; } } const foodset = new FoodSet(new Food('Fish', 26, 0, 16), new Food('Hamburger', 26, 48, 24)); // When we use for... When of operates with FoodSet, JavaScript will use // we used [symbol.iterator] for (let food of foodset) {// Print all food console.log(food); } // JavaScript creates and returns a new object when we execute the filter method on an array. The new object is created using this object as the default constructor. However, most code expects filter to return an array, We tell JavaScript to use the array constructor const healthy_foods = foodset.filter((food) => food.name!) by overriding [symbol.species] // == 'Hamburger'); console.log( healthy_foods instanceof FoodSet ); // console.log( healthy_foods instanceof Array );Copy the code
When you use for… When of iterates over an object, JavaScript will attempt to execute the iterator method of the object, which is the method associated with the Symbol. Iterator property of the object. If we provide our own method definition, JavaScript will use our own. If you don’t specify it, if you have a default implementation you use the default, if you don’t, you don’t implement it.
Symbo.species is more exotic. In a custom class, the default symbol. species function is the constructor of the class. When our subclasses have built-in collections (such as Array and Set), we often want to be able to use subclasses when using instances of the parent class.
By returning an instance of a parent class rather than a derived class, we are better able to ensure the availability of our subclasses in most code. Symbol. Species can do this.
Don’t bother with this feature if you don’t really need it. This use of Symbol — or all use of Symbol — is relatively rare. These examples are just for illustration:
- We can use JavaScript’s built-in specific constructors in custom classes;
- Two common examples show how to do this.
conclusion
The ES2015 class keyword does not give us the “real classes” of Java or SmallTalk. Rather, it just provides a more convenient syntax for creating objects associated by stereotypes, nothing new per se.
I’ve pretty much covered JavaScript prototyping in our discussion, but I need to say one more thing: A look at Kyle Simpson’s This and Object Prototyping article provides A comprehensive review of what has been described above, and appendix A is also closely related to this article.
For more details on the ES2015 class, check out Dr. Rauschmayer’s exploration of ES6: Classes. This is where I got my inspiration for this article.
Finally, if you have any questions, leave me a comment or tweet me on Twitter. I will answer everyone’s questions as best I can.
How do you feel about class? Love it, hate it, or feel nothing? Everyone has an opinion – say yours below!