Classes, prototypes, and constructors in JavaScript
Each JavaScript object is a collection of attributes that have no relationship with each other. In JavaScript, you can also define classes of objects so that each object shares some properties. This "sharing" feature is very useful. Class members or instances contain properties to store or define their state, some of which define their behavior (commonly known as methods). These behaviors are usually defined by classes and shared by all instances. For example, suppose a class called Complex is used to represent Complex numbers, and some Complex operations are defined. A Complex instance should contain the real and imaginary parts (States) of the Complex. Similarly, the Complex class also defines the addition and multiplication operations (behaviors) of the Complex.
In JavaScript, the implementation of a class is based on its prototype inheritance mechanism. If both instances inherit properties from the same prototype object, we say they are instances of the same class.
If two objects inherit from the same prototype, it often means (but not absolute) that they are created and initialized by the same constructor.
If you are familiar with object-oriented programming with strong types such as Java and C + + (strong / weak types refer to the strictness of type checking, and specifying data types for all variables is called "strong type"), you will find that classes in JavaScript are very different from classes in Java and C + +. Although the writing method is similar, and many classic class characteristics (such as encapsulation, inheritance and polymorphism of traditional classes) can be "simulated" in JavaScript, it is best to understand the class and prototype based inheritance mechanism of JavaScript, as well as the differences from the class and class based inheritance mechanism of traditional Java (of course, there is a language similar to Java).
An important feature of classes in JavaScript is "dynamically inheritable".
We can think of classes as types.
Duck typing programming philosophy, which weakens the type of object and strengthens the function of object.
Defining classes is one of the effective ways to develop modules and reuse code.
Classes and prototypes
In JavaScript, all instance objects of a class inherit properties from the same prototype object. Therefore, the prototype object is the core of the class. Inherit() this function returns a newly created object, which inherits from a prototype object. If you define a prototype object, then pass inherit() Function creates an object inherited from it, thus defining a JavaScript class. Usually, the instance of the class needs further initialization, usually by defining a function to create and initialize the new object. The following example defines a prototype object and a factory for a class representing the "range of values" Function to create and initialize an instance of a class.
example: A simple JavaScript class // range.js: implement a class that can represent the range of values // This factory method returns a new scope object function range(from, to) { // Use the inherit() function to create an object that inherits from the prototype object defined below // The prototype object is stored as an attribute of the function and defines the method (behavior) shared by all scope objects var r = inherit(range.methods); // Stores the start and end positions (States) of the new scope object // These two properties are not inheritable, and each object has a unique property r.from = from; r.to = to; // Returns the newly created object return r; } // The prototype object defines methods that are inherited by each scope object range.methods = { // If x is within the range, it returns true; otherwise, it returns false // This method can compare numeric ranges, strings and date ranges includes: function (x) { return this.from <= x && x <= this.to; }, // Ten is called once for each integer in the range // This method can only be used as a numeric range foreach: function (f) { for (var x = Math.ceil(this.from); x <= this.to; x++) f(x); }, // Returns a string representing this range toString: function () {return "(" + this.from + "..." + this.to + ")";} }; // Here are some examples of using range objects var r = range(1, 3); // Create a scope object r.includes(2); // =>True: 2 is within this range r.foreach(console.log); // Output 1 2 3 consol.log(r); // Output (1... 3)
In the above example, some code is useless. This code defines a factory method range(), which is used to create a new range object. We notice that it is called range() The function defines an attribute range. Methods to quickly store the prototype object of the defined class. Hanging the prototype object on the function is no big deal, but it is not a common practice. Also, pay attention to range() The function defines the from and to attributes for each range object to define the start and end positions of the range. These two attributes are non shared and, of course, non inheritable. Finally, note that the shared and inheritable methods defined in range.methods use the from and to attributes and the this keyword. In order to refer to them, they use this Keyword to refer to the object that calls this method. Methods of any class can read the properties of the object through this basic usage.
Classes and constructors
The above example shows one of the methods to define a class in JavaScript. However, this method is not commonly used. After all, it does not define a constructor. The constructor is used to initialize a newly created object. Use the keyword new to call the constructor. Calling the constructor with new will automatically create a new object, so the constructor itself only needs to initialize the state of the new object An important feature of calling the constructor is that the prototype property of the constructor is used as the prototype of the new object. This means that all objects created through the same constructor inherit from the same object, so they are members of the same class. The following example modifies the "scope class" in the example and uses the constructor instead of the factory function:
Example: use constructor to define "scope class" // rangez.js: another implementation of a class that represents a range of values // This is a constructor to initialize the newly created scope object // Note that there is no object created and returned here, just initialization function Range(from, to) { // Stores the start and end positions (States) of the range object // These two properties are not inheritable, and each object has a unique property this.from = from; this.to = to; } // All scope objects inherit from this object // Note that the name of the property must be "prototype" Range.prototype = { // If x is within the range, return true; Otherwise, false is returned // This method can compare numeric ranges, strings and date ranges includes: function (x) { return this.from <= x && x <= this.to; }, // f is called once for each integer in the range // This method can only be used for numeric ranges foreach: function (f) { for (var x = Math.ceil(this.from); x <= this.to; x++) f(x); }, // Returns a string representing this range toString: function () (return "(" + this.from + "..." + this.to + ")";} }; // Here are some examples of using range objects var r = range(1, 3); // Create a scope object r.includes(2); // => true: two Within this range r.foreach(console.log); // Output 1 2 3 console.log(r); // Output (1... 3)
A careful comparison of the code in the above two examples shows the difference between the two techniques for defining classes. First, notice that when the factory function range () is converted to a constructor, it is renamed range (). A common programming convention is followed here: in a sense, defining a constructor is both defining a class and the class name should be capitalized. Ordinary functions and methods are lowercase.
Also, note that the range () constructor is called through the new keyword (at the end of the sample code), while the range () factory function does not have to use new. The above two examples use two methods to create a new object. One is to create a new object by calling an ordinary function, and the other is to use constructor calls to create a new object. Since the range () constructor is called through the new keyword, there is no need to call inherit() or any other logic to create a new object. A new object has been created before calling the constructor. You can get the new object through the this keyword. The range () constructor simply initializes this. The constructor does not even have to return the newly created object. The constructor will automatically create the object, call the constructor as a method of the object, and finally return the new object. In fact, there is another reason why the naming rules of constructors (initial capitalization) are so different from ordinary functions. Constructor calls are different from ordinary function calls. The constructor is used to "construct a new object". It must be called through the keyword new. If the constructor is used as an ordinary function, it often won't work normally. Developers can determine whether the function should be preceded by the keyword mew through the naming convention (constructor initial uppercase, ordinary method initial lowercase).
Another important difference between the above two examples is the naming of prototype objects. The prototype in the first sample code is range.methodso. This naming method is very convenient and has good semantics, but it is too arbitrary.
The prototype in the second sample code is Range.prototype, which is a mandatory naming. Calls to the Range() constructor will automatically use Range.prototype as the prototype of the new Range object.
Finally, it should be noted that in the above two examples, the two types of definition methods are the same, and the scope method definition and calling method are exactly the same.
Identification of constructors and classes
As mentioned above, the prototype object is the unique identification of a class: if and only if two objects inherit from the same prototype object, they are instances of the same class. The constructor that initializes the state of the object cannot be used as the identification of the class. The prototype attribute of the two constructors may point to the same prototype object. Then the instances created by these two constructors belong to the same class.
Although constructors are not as basic as prototypes, constructors are the "external representation" of classes. Obviously, the name of the constructor is usually used as the class name. For example, we say that the Range() constructor creates a Range object. More fundamentally, however, constructors are used when using the instanceof operator to detect whether an object belongs to a class. Suppose there is an object r here. We want to know whether R is a Range object. Let's write this:
r instanceof Range // Returns true if r inherits from Range.prototype
In fact, the instanceof operator does not check whether the work is initialized by the Range() constructor, but whether r inherits from Range.prototype(). However, instanceof's syntax reinforces the concept that a constructor is a public identifier of a class.
constructor property
In the above example, Range.prototype is defined as a new object that contains the methods required by the class. In fact, it is not necessary to create a new object. It is convenient to define the methods on the prototype with the attributes of a single object. Any JavaScript function can be used as a constructor, and a prototype attribute is required to call the constructor. Therefore, every JavaScript function (except the function returned by the Function.bind() method in ECMAScript 5) automatically has a prototype attribute. The value of this property is an object, which contains the only non enumerable property. The value of constructor property is a function object:
var F = function() {}; // This is a function object var p = F.prototype; // This is the prototype object associated with F var c = p.constructor; //This is the function associated with the prototype c === F // =>True: F.prototype.constructor==F for any function
You can see that there is a predefined constructor attribute in the constructor prototype, which means that the constructor inherited by objects usually refers to their constructor. Since the constructor is the "public identity" of the class, this constructor property provides the class for the object.
var o = new F(); // Create an object of class F o.constructor === F // =>True, the constructor attribute refers to this class
As shown in the following figure, the following figure shows the relationship between the constructor and the prototype object, including the back reference from the prototype to the constructor and the instance created by the constructor.
It should be noted that the above figure uses the Range() constructor as an example, but in fact, the Range class defined in example 9-2 rewrites the predefined Range.prototype object with its own new object. This newly defined prototype object does not contain a constructor attribute. Therefore, the instance of the Range class does not contain the constructor attribute. We can remedy this problem by explicitly adding a constructor to the prototype:
Range.prototype = { constructor: Range, // Explicitly set constructor backreference includes: function(x) { return this.from <= x && x <= this.to; }, foreach: function(f) { for(var x = Math.ceil(this.from); x <= this.to; x++) f(x); }, toString: function() { return "(" + this.from + "..." + this.to + ")"; } };
Another common solution is to use the predefined prototype object, which contains the constructor attribute, and then add methods to the prototype object in turn:
// Extend the predefined Range.prototype object without overriding it // The Range.prototype.constructor property is automatically created Range.prototype.includes = function (x) {return this.from <= x && x <= this.to;}; Range.prototype.foreach = function (f) { for (var x = Math.ceil(this.from); x <= this.to; x++) f(x); }; Range.prototype.toString = function () { return "(" + this.from + "..." + this.to + ")"; };