Home >Web Front-end >JS Tutorial >In-depth understanding of JavaScript series (18): ECMAScript implementation of object-oriented programming_Basic knowledge
Introduction
This chapter is the second part about the object-oriented implementation of ECMAScript. In the first part, we discussed the introduction and comparison of CEMAScript. If you have not read the first part, before proceeding with this chapter, I strongly recommend that you read the first part. 1 article, because this article is too long (35 pages).
English original text:http://dmitrysoshnikov.com/ecmascript/chapter-7-2-oop-ecmascript-implementation/
Note: Due to the length of this article, errors are inevitable and are constantly being revised.
In the introduction, we extended to ECMAScript. Now, when we know its OOP implementation, let’s define it accurately:
Data Type
Although ECMAScript is a dynamically weakly typed language that can dynamically convert types, it still has data types. In other words, an object must belong to a real type.
There are 9 data types defined in the standard specification, but only 6 can be directly accessed in ECMAScript programs. They are: Undefined, Null, Boolean, String, Number, and Object.
The other three types can only be accessed at the implementation level (ECMAScript objects cannot use these types) and are used in specifications to explain some operational behaviors and save intermediate values. These 3 types are: Reference, List and Completion.
Therefore, Reference is used to explain operators such as delete, typeof, and this, and contains a base object and a property name; List describes the behavior of the parameter list (in new expressions and function calls); Completion is used to explain the behavior of break, continue, return and throw statements.
Primitive value types
Looking back at the 6 data types used in ECMAScript programs, the first 5 are primitive value types, including Undefined, Null, Boolean, String, Number, and Object.
Primitive value type example:
These values are implemented directly on the bottom layer. They are not objects, so there is no prototype or constructor.
Uncle’s note: Although these native values are similar in name to the ones we usually use (Boolean, String, Number, Object), they are not the same thing. Therefore, the results of typeof(true) and typeof(Boolean) are different, because the result of typeof(Boolean) is function, so functions Boolean, String, and Number have prototypes (also mentioned in the reading and writing attributes chapter below).
If you want to know what type of data it is, it is best to use typeof. There is an example that you need to pay attention to. If you use typeof to determine the type of null, the result is object. Why? Because the type of null is defined as Null.
The specification doesn't imagine explaining this, but Brendan Eich (the inventor of JavaScript) noticed that null is mostly used in places where objects appear, as opposed to undefined, such as setting an object to a null reference. However, some people in some documents attributed it to a bug, and put the bug in the bug list that Brendan Eich also participated in the discussion. The result was that the result of typeof null was set to object (despite the 262-3 The standard defines that the type of null is Null, and 262-5 has modified the standard to say that the type of null is object).
Object type
Next, the Object type (not to be confused with the Object constructor, we are only discussing abstract types now) is the only data type that describes ECMAScript objects.
Object is an unordered collection of key-value pairs.
An object is an unordered collection of key-value pairs
The key value of an object is called an attribute, and an attribute is a container for primitive values and other objects. If the value of an attribute is a function we call it a method.
For example:
Dynamic
As we pointed out in Chapter 17, objects in ES are completely dynamic. This means that we can add, modify or delete the properties of the object at will while the program is executing.
For example:
Some properties cannot be modified - (read-only properties, deleted properties or non-configurable properties). We will explain it later in the attribute properties.
In addition, the ES5 specification stipulates that static objects cannot be extended with new properties, and its property pages cannot be deleted or modified. They are so-called frozen objects, which can be obtained by applying the Object.freeze(o) method.
In the ES5 specification, the Object.preventExtensions(o) method is also used to prevent extensions, or the Object.defineProperty(o) method is used to define properties:
Built-in objects, native objects and host objects
It is necessary to note that the specification also distinguishes between built-in objects, element objects and host objects.
Built-in objects and element objects are defined and implemented by the ECMAScript specification, and the differences between the two are insignificant. All objects implemented by ECMAScript are native objects (some of them are built-in objects, some are created when the program is executed, such as user-defined objects). Built-in objects are a subset of native objects that are built into ECMAScript before the program starts (for example, parseInt, Match, etc.). All host objects are provided by the host environment, usually the browser, and may include window, alert, etc.
Note that the host object may be implemented by ES itself, fully complying with the semantics of the specification. From this point of view, they can be called "native host" objects (as soon as possible theoretically), but the specification does not define the concept of "native host" objects.
Boolean, String and Number objects
In addition, the specification also defines some native special packaging classes. These objects are:
1. Boolean object
2. String object
3. Digital objects
These objects are created through the corresponding built-in constructors and contain native values as their internal properties. These objects can convert primitive values and vice versa.
In addition, there are objects created by special built-in constructors: Function (function object constructor), Array (array constructor) RegExp (regular expression constructor), Math (math module), Date (date constructor) (container), etc. These objects are also values of the Object object type. Their differences from each other are managed by internal properties, which we discuss below.
Literal
For the values of three objects: object, array and regular expression, they have abbreviated identifiers called: object initializer, array initializer, and regular expression. Initializer:
Note that if the above three objects are reassigned to new types, then the subsequent implementation semantics will be used according to the newly assigned types. For example, in the current implementation of Rhino and the old version of SpiderMonkey 1.7, it will The object is successfully created using the constructor of the new keyword, but in some implementations (currently Spider/TraceMonkey) the semantics of literals do not necessarily change after the type is changed.
Regular expression literals and RegExp objects
Note that in the following two examples, the semantics of regular expressions are equivalent in the third edition of the specification. The regexp literal only exists in one sentence and is created in the parsing stage, but the one created by the RegExp constructor is It is a new object, so this may cause some problems. For example, the value of lastIndex is wrong during testing:
Associative array
Various textual static discussions, JavaScript objects (often created using object initializer {}) are called hash tables, hash tables, or other simple names: hash (a concept in Ruby or Perl), management Array (a concept in PHP), dictionary (a concept in Python), etc.
There are only such terms, mainly because their structures are similar, that is, using "key-value" pairs to store objects, which is completely consistent with the data structure defined by the theory of "associative array" or "hash table". In addition, the hash table abstract data type is usually used at the implementation level.
However, although the terminology describes this concept, it is actually a mistake. From the perspective of ECMAScript: ECMAScript has only one object and type and its subtypes, which is no different from "key-value" pair storage, so There is no special concept on this. Because the internal properties of any object can be stored as key-value pairs:
Also, since objects can be empty in ECMAScript, the concept of "hash" is also incorrect here:
Please note that the ES5 standard allows us to create objects without prototypes (implemented using the Object.create(null) method). From this perspective, such objects can be called hash tables:
However, even if it is considered that "hash" may have a "prototype" (e.g., a class that delegates hash objects in Ruby or Python), in ECMAScript, this terminology is incorrect because there is a gap between the two representations. There is no semantic difference (i.e. using dot notation a.b and a["b"] notation).
The concept and semantics of "property attribute" in ECMAScript are not separated from "key", array index, and method. The reading and writing of properties of all objects here must follow the same rules: check the prototype chain.
In the following Ruby example, we can see the semantic difference:
Object conversion
To convert an object into a primitive value, you can use the valueOf method. As we said, when the constructor of the function is called as a function (for some types), but if the new keyword is not used, the object is converted into a primitive value. , which is equivalent to the implicit valueOf method call:
The default value of valueOf will change according to the type of the object (if not overridden). For some objects, it returns this - for example: Object.prototype.valueOf(), and calculated values. : Date.prototype.valueOf() returns the date and time:
The toString method defined on Object.prototype has a special meaning. It returns the internal [[Class]] attribute value that we will discuss below.
Compared with converting to primitive values (ToPrimitive), converting values into object types also has a conversion specification (ToObject).
An explicit method is to use the built-in Object constructor as a function to call ToObject (somewhat similar to the new keyword):
There are no general rules about calling built-in constructors, whether to use the new operator or not, it depends on the constructor. For example, Array or Function produce the same result when used as a constructor using the new operator or a simple function that does not use the new operator:
Characteristics of attributes
All properties can have many attributes.
1.{ReadOnly} - Ignore the write operation of assigning a value to the property, but the read-only property can be changed by the behavior of the host environment - that is, it is not a "constant value";
2.{DontEnum}——Attributes cannot be enumerated by for..in loop
3.{DontDelete}——The behavior of the delete operator is ignored (that is, it cannot be deleted);
4. {Internal} - Internal attribute, no name (only used at the implementation level), such attributes cannot be accessed in ECMAScript.
Note that in ES5 {ReadOnly}, {DontEnum} and {DontDelete} are renamed to [[Writable]], [[Enumerable]] and [[Configurable]], which can be manually passed through Object.defineProperty or similar methods to manage these properties.
Internal properties and methods
Objects can also have internal properties (part of the implementation level) that are not directly accessible to ECMAScript programs (but as we will see below, some implementations allow access to some such properties). These properties are accessed through nested square brackets [[ ]]. Let's take a look at some of them. The description of these properties can be found in the specification.
Every object should implement the following internal properties and methods:
1.[[Prototype]] - the prototype of the object (will be introduced in detail below)
2.[[Class]] - a representation of a string object (for example, Object Array, Function Object, Function, etc.); used to distinguish objects
3.[[Get]]——Method to obtain attribute value
4.[[Put]]——Method to set attribute value
5.[[CanPut]]——Check whether the attribute is writable
6.[[HasProperty]]——Check whether the object already has this property
7.[[Delete]]——Delete the attribute from the object
8.[[DefaultValue]] returns the original value of the object (calling the valueOf method, some objects may throw a TypeError exception).
The value of the internal property [[Class]] can be obtained indirectly through the Object.prototype.toString() method, which should return the following string: "[object " [[Class]] "]" . For example:
Constructor
So, as we mentioned above, objects in ECMAScript are created through so-called constructors.
Constructor is a function that creates and initializes the newly created object.
A constructor is a function that creates and initializes a newly created object.
Object creation (memory allocation) is taken care of by the constructor's internal method [[Construct]]. The behavior of this internal method is well defined, and all constructors use this method to allocate memory for new objects.
The initialization is managed by calling this function up and down the new object, which is responsible for the internal method [[Call]] of the constructor.
Note that user code can only be accessed during the initialization phase, although during the initialization phase we can return a different object (ignoring the tihs object created in the first phase):
Referring to Chapter 15 Function - Algorithm for Creating Functions, we can see that the function is a native object, including [[Construct]] ] and [[Call]] ] attributes as well as the displayed prototype prototype attribute - the future The prototype of the object (Note: NativeObject is a convention for native objects and is used in the pseudocode below).
[[Call]] ] is the main way to distinguish objects other than the [[Class]] attribute (here equivalent to "Function"), so the internal [[Call]] attribute of the object is called as a function. Using the typeof operator on such an object returns "function". However, it is mainly related to native objects. In some cases, the implementation of using typeof to obtain the value is different. For example: the effect of window.alert (...) in IE:
The internal method [[Construct]] is activated by using the constructor with the new operator. As we said, this method is responsible for memory allocation and object creation. If there are no parameters, the parentheses for calling the constructor can also be omitted:
Let’s study the object creation algorithm.
Algorithm for object creation
The behavior of the internal method [[Construct]] can be described as follows:
Please note two main features:
1. First, the prototype of the newly created object is obtained from the prototype attribute of the function at the current moment (this means that the prototypes of two created objects created by the same constructor can be different because the prototype attribute of the function can also be different) .
2. Secondly, as we mentioned above, if [[Call]] returns an object when the object is initialized, this is exactly the result used for the entire new operator:
Prototype
Every object has a prototype (except some system objects). Prototype communication is carried out through the internal, implicit, and not directly accessible [[Prototype]] prototype property. The prototype can be an object or a null value.
Property constructor
There are two important knowledge points in the above example. The first one is about the prototype attribute of the constructor attribute of the function. In the function creation algorithm, we know that the constructor attribute is set to the prototype attribute of the function during the function creation phase. , the value of the constructor attribute is an important reference to the function itself:
Usually in this case, there is a misunderstanding: the constructor constructor property is wrong as a property of the newly created object itself, but, as we can see, this property belongs to the prototype and is accessed through inheritance.
By inheriting the instance of the constructor attribute, you can indirectly obtain a reference to the prototype object:
But please note that the constructor and prototype attributes of the function can be redefined after the object is created. In this case, the object loses the mechanism described above. If you edit the element's prototype through the function's prototype attribute (adding a new object or modifying an existing object), you will see the newly added attributes on the instance.
However, if we completely change the prototype property of the function (by allocating a new object), the reference to the original constructor is lost, because the object we create does not include the constructor property:
Note that although the constructor attribute has been restored manually, compared with the original lost prototype, the {DontEnum} feature is no longer available, which means that the for..in loop statement in A.prototype is not supported, but in the 5th edition of the specification , provides the ability to control the enumerable state enumerable through the [[Enumerable]] attribute.
Explicit prototype and implicit [[Prototype]] attributes
Generally, it is incorrect to explicitly reference the prototype of an object through the function's prototype attribute. It refers to the same object, the object's [[Prototype]] attribute:
a.[[Prototype]] ----> Prototype <---- A.prototype
In addition, the [[Prototype]] value of the instance is indeed obtained from the prototype attribute of the constructor.
However, submitting the prototype attribute will not affect the prototype of the already created object (it will only be affected when the prototype attribute of the constructor changes). That is to say, only newly created objects will have new prototypes, and already created objects will still have new prototypes. Reference to the original old prototype (this prototype can no longer be modified).
For example:
The main rule here is: the prototype of an object is created when the object is created, and cannot be modified to a new object after that. If it still refers to the same object, it can be referenced through the explicit prototype of the constructor. After the object is created, only the properties of the prototype can be added or modified.
Non-standard __proto__ attribute
However, some implementations, such as SpiderMonkey, provide the non-standard __proto__ explicit attribute to reference the object's prototype:
Object independent of constructor
Because the prototype of the instance is independent of the constructor and the prototype attribute of the constructor, the constructor can be deleted after completing its main work (creating the object). Prototype objects continue to exist by referencing the [[Prototype]] attribute:
Characteristics of instanceof operator
We display the reference prototype through the prototype attribute of the constructor, which is related to the instanceof operator. This operator works with the prototype chain, not the constructor. With this in mind, there are often misunderstandings when detecting objects:
Let’s take a look at this example:
On the other hand, an object can be created by a constructor, but if the [[Prototype]] attribute of the object and the value of the prototype attribute of the constructor are set to the same value, instanceof will return true when checked:
Prototypes can store methods and share properties
Prototypes are used in most programs to store object methods, default states, and shared object properties.
In fact, objects can have their own state, but the methods are usually the same. Therefore, methods are usually defined in prototypes for memory optimization. This means that all instances created by this constructor can share this method.
Read and write attributes
As we mentioned, reading and writing property values is through the internal [[Get]] and [[Put]] methods. These internal methods are activated through property accessors: dot notation or index notation:
[[Get]] method
[[Get]] will also query properties from the prototype chain, so properties in the prototype can also be accessed through the object.
O.[[Get]](P):
Note: The in operator can also be responsible for looking up properties (it will also look up the prototype chain):
[[Put]] method
The[[Put]] method can create and update the properties of the object itself, and mask the properties of the same name in the prototype.
O.[[Put]](P, V):
// For example, the attribute length is read-only, let’s try to mask the length
function SuperString() {
/* nothing */
}
SuperString.prototype = new String("abc");
var foo = new SuperString();
console.log(foo.length); // 3, the length of "abc"
// Attempt to mask
foo.length = 5;
console.log(foo.length); // Still 3
Property accessor
The internal methods [[Get]] and [[Put]] are activated through dot notation or indexing in ECMAScript. If the attribute identifier is a legal name, it can be accessed through ".", and indexing Party runs dynamically defined names.
There is a very important feature here - property accessors always use the ToObject specification to treat the value to the left of "." This implicit conversion is related to the saying "everything in JavaScript is an object" (however, as we already know, not all values in JavaScript are objects).
If the attribute accessor is used to access the original value, the original value will be wrapped by the object (including the original value) before accessing, and then the attribute will be accessed through the wrapped object. After the attribute is accessed, the wrapped object will be deleted.
For example:
The answer is simple:
First of all, as we said, after using the property accessor, it is no longer the original value, but a wrapped intermediate object (the entire example uses new Number(a)), and the toString method is passed at this time Found in the prototype chain:
We see that in step 3, the wrapped object is deleted, and the newly created property page is deleted - deleting the wrapping object itself.
When using [[Get]] to get the test value, the packaging object is created again, but this time the wrapped object no longer has the test attribute, so undefined is returned:
Inherit
We know that ECMAScript uses prototype-based delegated inheritance. Chains and prototypes have already been mentioned in the prototype chain. In fact, all the implementation of delegation and the search and analysis of the prototype chain are condensed into the [[Get]] method.
If you fully understand the [[Get]] method, then the question of inheritance in JavaScript will be self-explanatory.
When I often talk about inheritance in JavaScript on forums, I always use one line of code to show it. In fact, we don’t need to create any objects or functions because the language is already based on inheritance. The code is as follows:
1. First, create a packaging object from the original value 1 through new Number(1)
2. Then the toString method is inherited from this packaging object
Why is it inherited? Because objects in ECMAScript can have their own properties, the wrapper object in this case does not have a toString method. So it inherits from the principle, which is Number.prototype.
Note there is a subtlety, the two dots in the above example are not an error. The first point represents the decimal part, and the second point is an attribute accessor:
Prototype Chain
Let's show how to create a prototype chain for a user-defined object, it's very simple:
This method has two characteristics:
First, B.prototype will contain the x attribute. At first glance this may not seem right, you might think that the x property is defined in A and that the B constructor expects it as well. Although prototypal inheritance is no problem under normal circumstances, the B constructor may sometimes not need the x attribute. Compared with class-based inheritance, all attributes are copied to descendant subclasses.
However, if there is a need (to simulate class-based inheritance) to assign the x attribute to the object created by the B constructor, there are some ways, one of which we will show later.
Secondly, this is not a feature but a disadvantage - when the subclass prototype is created, the code of the constructor is also executed, and we can see that the message "A.[[Call]] activated" is displayed twice - When using the A constructor to create an object and assign it to the B.prototype property, the other scene is when the a object creates itself!
The following example is more critical, the exception thrown by the constructor of the parent class: Maybe it needs to be checked when the actual object is created, but obviously, the same case, that is, when using these parent objects as prototypes Something will go wrong.
In addition, having too much code in the constructor of the parent class is also a disadvantage.
To solve these "functions" and problems, programmers use the standard pattern of prototype chains (shown below). The main purpose is to wrap the creation of constructors in the middle. The chains of these wrapping constructors contain the required prototypes.