Introduction
In this article, we consider various aspects of object-oriented programming in ECMAScript (although this topic has been discussed in many articles before). We will look at these issues more from a theoretical perspective. In particular, we will consider object creation algorithms, how objects are related (including basic relationships - inheritance), which can also be used in discussions (which I hope will dispel some previous conceptual ambiguities about OOP in JavaScript).
English original text:http://dmitrysoshnikov.com/ecmascript/chapter-7-1-oop-general-theory/
Introduction, paradigm and ideas
Before conducting the technical analysis of OOP in ECMAScript, it is necessary for us to master some basic characteristics of OOP and clarify the main concepts in the introduction.
ECMAScript supports a variety of programming methods including structured, object-oriented, functional, imperative, etc. In some cases, it also supports aspect-oriented programming; but this article discusses object-oriented programming, so here is the object-oriented programming in ECMAScript Definition:
ECMAScript is an object-oriented programming language based on prototype implementation.
There are many differences between prototype-based OOP and static class-based approaches. Let’s take a look at their differences in immediate detail.
Based on class attributes and based on prototype
Note that an important point has been pointed out in the previous sentence - completely based on static classes. With the word "static" we understand static objects and static classes, strongly typed (although not required).
Concerning this situation, many documents on the forum have emphasized that this is the main reason why they object to comparing "classes with prototypes" in JavaScript, although their implementations are different (such as based on dynamic classes) Python and Ruby) are not too opposed to the focus (some conditions are written, although there are certain differences in thinking, JavaScript has not become so alternative), but the focus of their opposition is statics classes vs. dynamics prototypes ), to be precise, the mechanism of a static class (for example: C, JAVA) and its subordinates and method definitions allows us to see the exact difference between it and a prototype-based implementation.
But let’s list them one by one. Let us consider general principles and the main concepts of these paradigms.
Based on static class
In the class-based model, there is a concept of classes and instances. Instances of classes are also often named objects or instances.
Classes and Objects
A class represents an abstraction of an instance (that is, an object). It's a bit like mathematics in this regard, but we call it a type or classification.
For example (the examples here and below are pseudocode):
Hierarchical inheritance
To improve code reuse, classes can be extended from one to another, adding additional information. This mechanism is called (hierarchical) inheritance.
When calling a method on an instance of a class, you will usually search for the method in the native class. If not found, go to the direct parent class to search. If not found yet, go to the parent class of the parent class to search. (For example, in a strict inheritance chain), if the top of the inheritance is found but not yet found, the result is: the object has no similar behavior and there is no way to obtain the result.
Key concepts based on classes
Thus, we have the following key concepts:
1. Before creating an object, the class must be declared. First, it is necessary to define its class
2. Therefore, the object will be created from the class abstracted into its own "iconogram and similarity" (structure and behavior)
3. Methods are processed through a strict, direct, and immutable inheritance chain
4. The subclass contains all the attributes in the inheritance chain (even if some of the attributes are not needed by the subclass);
5. Create a class instance. A class cannot (because of the static model) change the characteristics (properties or methods) of its instance;
6. Instances (because of the strict static model) cannot have additional behaviors or attributes other than those declared in the class corresponding to the instance.
Let’s look at how to replace the OOP model in JavaScript, which is what we propose based on prototype OOP.
Based on prototype
The basic concept here is dynamic mutable objects. Transformations (complete transformations, including not only values but also attributes) are directly related to dynamic languages. Objects like the following can store all their properties (properties, methods) independently without the need for a class.
Furthermore, being dynamic, they can easily change (add, delete, modify) their characteristics:
That is, at the time of assignment, if some attribute does not exist, create it and initialize the assignment with it, and if it exists, just update it.
In this case, code reuse is not achieved by extending the class, (please note that we did not say that the class cannot be changed, because there is no concept of class here), but by prototype.
A prototype is an object that is used as a primitive copy of other objects, or if some objects do not have the necessary properties of their own, the prototype can be used as a delegate for these objects and serve as an auxiliary object.
Delegated based
Any object can be used as a prototype object for another object, because an object can easily change its prototype dynamically at runtime.
Note that we are currently considering an overview rather than a specific implementation. When we discuss specific implementations in ECMAScript, we will see some of their own characteristics.
Example (pseudocode):
This example shows the important function and mechanism of prototype as an auxiliary object attribute, just like asking for its own attributes. Compared with its own attributes, these attributes are delegate attributes. This mechanism is called a delegate, and a prototype model based on it is a delegate prototype (or delegate-based prototype). The mechanism of reference here is called sending a message to an object. If the object does not get a response, it will be delegated to the prototype to find it (requiring it to try to respond to the message).
Code reuse in this case is called delegate-based inheritance or prototype-based inheritance. Since any object can be used as a prototype, that means a prototype can also have its own prototype. These prototypes are linked together to form a so-called prototype chain. Chains are also hierarchical like static classes, but they can be easily rearranged to change the hierarchy and structure.
If an object and its prototype chain cannot respond to message sending, the object can activate the corresponding system signal, possibly handled by other delegates on the prototype chain.
This system signal is available in many implementations, including systems based on bracketed dynamic classes: #doesNotUnderstand in Smalltalk, method_missing in Ruby; __getattr__ in Python, __call in PHP; and __noSuchMethod__ implementation in ECMAScript, etc.
Example (SpiderMonkey’s ECMAScript implementation):
In other words, if the implementation based on the static class cannot respond to the message, the conclusion is that the current object does not have the required characteristics, but if you try to obtain it from the prototype chain, you may still get the result. , or the object possesses this characteristic after a series of changes.
Regarding ECMAScript, the specific implementation is: using delegate-based prototypes. However, as we will see from the specification and implementation, they also have their own characteristics.
Concatenative Model
Honestly, it is necessary to say something about another situation (as soon as it is not used in ECMASCript): the situation when the prototype replaces the native object from other objects. Code reuse in this case is a true copy (clone) of an object during the object creation phase rather than delegation. This kind of prototype is called concatenative prototype. Copying all prototype properties of an object can further completely change its properties and methods, and the prototype can also change itself (in a delegate-based model, this change will not change the existing object behavior, but change its prototype properties) . The advantage of this method is that it can reduce the time of scheduling and delegation, but the disadvantage is that the memory usage is high.
Duck Type
Returning objects that dynamically change weak types. Compared with models based on static classes, testing whether it can do these things has nothing to do with the type (class) of the object, but whether it can respond to the message (that is, after checking whether The ability to do it is a must).
For example:
This is the so-called Dock type. That is, objects can be identified by their own characteristics when checking, rather than the object's position in the hierarchy or their belonging to any specific type.
Key concepts based on prototypes
Let’s take a look at the main features of this approach:
1. The basic concept is object
2. The object is completely dynamic and variable (theoretically it can be converted from one type to another)
3. Objects do not have strict classes that describe their own structure and behavior. Objects do not need classes
4. Objects do not have classes but can have prototypes. If they cannot respond to messages, they can be delegated to the prototype
5. The prototype of the object can be changed at any time during runtime;
6. In the delegate-based model, changing the characteristics of the prototype will affect all objects related to the prototype;
7. In the concatenative prototype model, the prototype is an original copy cloned from other objects, and further becomes a completely independent copy original. The transformation of the prototype characteristics will not affect the objects cloned from it
8. If the message cannot be responded to, its caller can take additional measures (e.g., change scheduling)
9. The failure of objects can not be determined by their level and which class they belong to, but by the current characteristics
However, there is another model that we should also consider.
Based on dynamic classes
We believe that the distinction "class VS prototype" shown in the above example is not so important in this model based on dynamic classes, (especially if the prototype chain is immutable, for a more accurate distinction, it is still necessary to consider a static class). As an example, it could also use Python or Ruby (or other similar languages). These languages all use a dynamic class-based paradigm. However, in some aspects we can see some functionality implemented based on the prototype.
In the following example, we can see that based only on delegation, we can enlarge a class (prototype), thus affecting all objects related to this class. We can also dynamically change this object at runtime. class (providing a new object for the delegate) and so on.
The implementation in Ruby is similar: fully dynamic classes are also used (by the way in the current version of Python, in contrast to Ruby and ECMAScript, enlarging classes (prototypes) does not work), we can completely change the object (or class) characteristics (adding methods/properties to the class, and these changes will affect existing objects), however, it cannot dynamically change the class of an object.
However, this article is not specifically about Python and Ruby, so we won’t say more and let’s continue discussing ECMAScript itself.
But before that, we have to take another look at the "syntactic sugar" found in some OOPs, because many previous articles about JavaScript often cover these issues.
The only incorrect sentence to note in this section is: "JavaScript is not a class, it has prototypes, which can replace classes." It is important to know that not all class-based implementations are completely different. Even though we might say "JavaScript is different", it is also necessary to consider that (in addition to the concept of "classes") there are other related characteristics.
Other features of various OOP implementations
In this section we briefly introduce other features and methods of code reuse in various OOP implementations, including OOP implementations in ECMAScript. The reason is that there are some habitual thinking restrictions on the implementation of OOP in JavaScript. The only main requirement is that it should be proven technically and ideologically. It cannot be said that we have not discovered the syntactic sugar function in other OOP implementations, and we have hastily assumed that JavaScript is not a pure OOP language. This is wrong.
Polymorphic
Objects have several meanings of polymorphism in ECMAScript.
For example, a function can be applied to different objects, just like the properties of the native object (because the value is determined when entering the execution context):
The so-called parameter polymorphism when defining a function is equivalent to all data types, except that it accepts polymorphic parameters (such as the .sort sorting method of the array and its parameters - polymorphic sorting function). By the way, the above example can also be considered a kind of parametric polymorphism.
Methods in the prototype can be defined as empty, and all created objects should redefine (implement) this method (i.e. "one interface (signature), multiple implementations").
Polymorphism is related to the Duck type we mentioned above: i.e. the type and position of the object in the hierarchy are not that important, but if it has all the necessary characteristics, it can be easily accepted (i.e. Common interfaces are important, implementations can be diverse).
Encapsulation
There are often misconceptions about encapsulation. In this section we discuss some syntactic sugars in OOP implementations - also known as modifiers: In this case, we will discuss some convenience "sugar" in OOP implementations - well-known modifiers: private, protected and public ( Alternatively known as an object's access level or access modifier).
Here I would like to remind you of the main purpose of encapsulation: encapsulation is an abstract addition, not a hidden "malicious hacker" who writes something directly into your class.
This is a big mistake: use hide for the sake of hiding.
Access levels (private, protected and public) have been implemented in many object-oriented programs to facilitate programming (really very convenient syntax sugar), describing and building systems more abstractly.
This can be seen in some implementations (such as Python and Ruby already mentioned). On the one hand (in Python), these __private_protected properties (named via the underscore convention) are not accessible from the outside. Python, on the other hand, can be accessed from outside with special rules (_ClassName__field_name).
In Ruby: On the one hand, it has the ability to define private and protected characteristics. On the other hand, there are also special methods (such as instance_variable_get, instance_variable_set, send, etc.) to obtain encapsulated data.
The main reason is that the programmer himself wants to get the encapsulated (note that I specifically don't use "hidden") data. If this data changes incorrectly in some way or has any errors, the full responsibility lies with the programmer, but not simply "typing errors" or "just changing some fields". But if this happens frequently, it is a very bad programming habit and style, because it is usually worth using the public API to "talk" to the object.
To repeat, the basic purpose of encapsulation is to abstract away the user of auxiliary data, not to prevent hackers from hiding the data. More seriously, encapsulation does not use private to modify data to achieve software security.
Encapsulate auxiliary objects (partial). We use minimal cost, localization and predictive changes to provide feasibility for behavioral changes in public interfaces. This is the purpose of encapsulation.
In addition, the important purpose of the setter method is to abstract complex calculations. For example, the element.innerHTML setter - the abstract statement - "The HTML inside this element now is the following content", and the setter function in the innerHTML property will be difficult to calculate and check. In this case, the problem mostly involves abstraction, but encapsulation also occurs.
The concept of encapsulation is not only related to OOP. For example, it can be a simple function that only encapsulates various calculations, making it abstract (there is no need for the user to know, for example, how the function Math.round(...) is implemented, the user simply calls it). It is a kind of encapsulation. Note that I did not say it is "private, protected and public".
The current version of the ECMAScript specification does not define the private, protected and public modifiers.
However, in practice it is possible to see something named "Mock JS Encapsulation". Generally this context is intended to be used (as a rule, the constructor itself). Unfortunately, this "mimicry" is often implemented and programmers can produce pseudo-absolutely non-abstract entity setting "getter/setter methods" (I repeat, it is wrong):
So, everyone understands that for every object created, the getA/setA methods are also created, which is also the reason for the increase in memory (compared to the prototype definition). Although, in theory the object can be optimized in the first case.
In addition, some JavaScript articles often mention the concept of "private methods". Note: The ECMA-262-3 standard does not define any concept of "private methods".
However, in some cases it can be created in the constructor, because JS is an ideological language - objects are completely mutable and have unique characteristics (under certain conditions in the constructor, some objects can get extra methods while others don't).
In addition, in JavaScript, if encapsulation is still misinterpreted as an understanding that prevents malicious hackers from automatically writing certain values instead of using the setter method, then the so-called "hidden" and " "private" is actually not very "hidden", some implementations can get the value on the relevant scope chain (and corresponding all variable objects) by calling the context to the eval function (can be tested on SpiderMonkey1.7).
Alternatively, the implementation allows direct access to the active object (such as Rhino), and the value of the internal variable can be changed by accessing the corresponding property of the object:
var _myPrivateData = 'testString';
It is often used to enclose execution context in parentheses, but for real auxiliary data, it is not directly related to the object, but is just convenient for abstracting from external API:
Multiple inheritance
Multiple inheritance is a very convenient syntactic sugar to improve code reuse (if we can inherit one class at a time, why can't we inherit 10 at a time?). However, due to some shortcomings of multiple inheritance, it has not become popular in implementation.
ECMAScript does not support multiple inheritance (i.e. only one object can be used as a direct prototype), although its ancestor programming language has such capability. But in some implementations (such as SpiderMonkey) using __noSuchMethod__ can be used to manage scheduling and delegation instead of the prototype chain.
Mixins
Mixins are a convenient way to reuse code. Mixins have been suggested as alternatives to multiple inheritance. Each of these individual elements can be mixed with any object to extend their functionality (so objects can also be mixed with multiple Mixins). The ECMA-262-3 specification does not define the concept of "Mixins", but according to the definition of Mixins and ECMAScript has dynamically mutable objects, there is no obstacle to simply extending features using Mixins.
Typical example:
Please note that I use these definitions ("mixin", "mix") in quotation marks mentioned in ECMA-262-3. There is no such concept in the specification, and it is not mix but commonly used to extend objects with new features. (The concept of mixins in Ruby is officially defined. Mixins create a reference to a containing module instead of simply copying all the properties of the module to another module - in fact: creating an additional object (prototype) for the delegate. ).
Traits
Traits are similar in concept to mixins, but they have a lot of functionality (by definition, since mixins can be applied they cannot contain state, as it may cause naming conflicts). According to ECMAScript, Traits and mixins follow the same principles, so the specification does not define the concept of "Traits".
Interface
The interfaces implemented in some OOP are similar to mixins and traits. However, in contrast to mixins and traits, interfaces force implementing classes to implement the behavior of their method signatures.
Interfaces can be completely regarded as abstract classes. However, compared with abstract classes (methods in abstract classes can only implement part of the method, and the other part is still defined as a signature), inheritance can only inherit a single base class, but can inherit multiple interfaces. For this reason, interfaces (multiple Mixed) can be seen as an alternative to multiple inheritance.
The ECMA-262-3 standard neither defines the concept of "interface" nor the concept of "abstract class". However, as imitation, it is possible to implement an object with an "empty" method (or an exception thrown in an empty method to tell the developer that this method needs to be implemented).
Object combination
Object composition is also one of the dynamic code reuse technologies. Object composition differs from highly flexible inheritance in that it implements a dynamically mutable delegate. And this is also based on the basis of commissioned prototypes. In addition to dynamically mutable prototypes, the object can aggregate objects for delegates (create a combination as a result - an aggregation) and further send messages to objects that delegate to the delegate. This can be done with more than two delegates, as its dynamic nature means it can change at runtime.
The already mentioned __noSuchMethod__ example does this, but let's also show how to use delegates explicitly:
For example:
This object relationship is called "has-a", and integration is an "is-a" relationship.
Due to the lack of explicit composition (flexibility compared to inheritance), adding intermediate code is also okay.
AOP features
As an aspect-oriented function, you can use function decorators. The ECMA-262-3 specification does not clearly define the concept of "function decorators" (as opposed to Python, where this term is officially defined). However, functions with functional parameters can be decorated and activated in certain aspects (by applying so-called suggestions):
The simplest decorator example:
Conclusion
In this article, we have clarified the introduction of OOP (I hope this information has been useful to you), and in the next chapter we will continue with the implementation of ECMAScript for object-oriented programming.