introduce
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 the creation algorithm of the object, how the relationship between objects (including basic relationships - inheritance) is also available in the discussion (I hope that some of the conceptual ambiguity of OOP in JavaScript will be removed).
Original English text: http://dmitrysoshnikov.com/ecmascript/chapter-7-1-oop-general-theory/
Introduction, Paradigm and Thoughts
Before conducting OOP technical analysis in ECMAScript, it is necessary to master some basic characteristics of OOP and clarify the main concepts in the introduction.
ECMAScript supports various programming methods including structured, object-oriented, functional, imperative, etc., and in some cases it also supports aspect-oriented programming; but this article discusses object-oriented programming, so we will give the definition of object-oriented programming in ECMAScript:
ECMAScript is an object-oriented programming language based on prototype implementation.
There are many differences between prototype-based OOP and static class-based methods. Let's take a look at their direct detailed differences.
Class-based and prototype-based
Note that the important point in the previous sentence has already pointed out - it is based entirely on static classes. With the word "static", we understand static objects and static classes, strongly typed (although not required).
Regarding this situation, many forum documents emphasize that this is the main reason why they oppose comparing "classes to prototypes" in JavaScript. Although their implementation differences (such as Python and Ruby based on dynamic classes) are not too opposed to the point (some conditions are written, although there are some differences in ideas, JavaScript is not so alternative), but their opposition is static classes and dynamic prototypes (statics + classes vs. dynamics + prototypes). To be precise, a static class (such as C++, JAVA) and its subordinates and method definition mechanisms can let us see the exact difference between it and prototype implementation.
But let's list them one by one. Let us consider the general principles and the main concepts of these paradigms.
Based on static classes
In a class-based model, there is a concept about classes and instances. Instances of classes are also often named objects or paradigms.
Classes and Objects
A class represents an abstraction of an instance (i.e. an object). This is a bit like mathematics, but we call it type or classification.
For example (the examples here and below are pseudo-code):
The code copy is as follows:
C = Class {a, b, c} // Class C, including features a, b, c
The characteristics of the instance are: attributes (object description) and methods (object activity). The characteristics themselves can also be regarded as objects: that is, whether the attribute is writable, configurable, settable (getter/setter), etc. Therefore, objects store state (i.e., the specific values of all attributes described in a class), and classes define strictly invariant structures (properties) and strictly invariant behaviors (methods) for their instances.
The code copy is as follows:
C = Class {a, b, c, method1, method2}
c1 = {a: 10, b: 20, c: 30} // Class C is an instance: object с1
c2 = {a: 50, b: 60, c: 70} // Class C is an instance: object с2, which has its own state (that is, attribute value)
Hierarchical inheritance
To improve code reuse, classes can be extended from one to another, adding additional information. This mechanism is called (hierarchical) inheritance.
The code copy is as follows:
D = Class extends C = {d, e} // {a, b, c, d, e}
d1 = {a: 10, b: 20, c: 30, d: 40, e: 50}
When calling the party on the instance of the class, the native class will usually look for the method now. If it is not found, go to the parent class to search directly. If it is not found, go to the parent class of the parent class to search (for example, on the strict inheritance chain). If it is found that the top of the inheritance has not been found, the result is: the object has no similar behavior and cannot get the result.
The code copy is as follows:
d1.method1() // D.method1 (no) -> C.method1 (yes)
d1.method5() // D.method5 (no) -> C.method5 (no) -> no result
Compared to inheritance methods that are not copied to a subclass, properties are always complicated into subclasses. We can see that subclass D inherits from the parent class C: attributes a, b, c are copied, and the structure of D is {a, b, c, d, e} }. However, the method {method1, method2} does not copy the past, but inherits the past. Therefore, that is, if a very deep class has some properties that objects do not need at all, then the subclass also has these properties.
Key concepts based on classes
Therefore, 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 a class abstracted into its own "pictographic and similarity" (structure and behavior)
3. The method is handled through a strict, direct, and unchanging inheritance chain.
4. The subclass contains all attributes in the inheritance chain (even if some of the attributes are not required by the subclass);
5. Create a class instance. The class cannot (because of a static model) to change the characteristics (attributes or methods) of its instance;
6. Instances (because of strict static models) cannot have additional behaviors or attributes except for the behaviors and attributes declared in the corresponding class to the instance.
Let's see how to replace the OOP model in JavaScript, which is the prototype-based OOP we recommend.
Based on prototype
The basic concept here is dynamic mutable objects. Transformation (full conversion, including not only values, but also features) is directly related to dynamic language. Objects like the following can independently store all their properties (properties, methods) without classes.
The code copy is as follows:
object = {a: 10, b: 20, c: 30, method: fn};
object.a; // 10
object.c; // 30
object.method();
Additionally, due to dynamic, they can easily change (add, delete, modify) their own features:
The code copy is as follows:
object.method5 = function () {...}; // Add a new method
object.d = 40; // Add new attribute "d"
delete object.c; // Delete attribute "с"
object.a = 100; // Modify the attribute "а"
// The result is: object: {a: 100, b: 20, d: 40, method: fn, method5: fn};
That is, when assigning, if some feature does not exist, it is created and the assignment is initialized with it, and if it exists, it is just updated.
In this case, code reuse is not implemented by extending the class (note that we did not say that the class cannot be changed because there is no concept of class here), but is implemented by prototypes.
A prototype is an object that is used as a primitive copy of other objects, or if some objects do not have their own necessary characteristics, the prototype can be used as a delegate to these objects and act as a auxiliary object.
Based on delegation
Any object can be used as a prototype object for another object, because the object can easily change its prototype dynamics at runtime.
Note that we are currently considering an introduction rather than a concrete implementation, and when we discuss concrete implementation in ECMAScript, we will see some of their own characteristics.
Example (pseudocode):
The code copy is as follows:
x = {a: 10, b: 20};
y = {a: 40, c: 50};
y.[[Prototype]] = x; // x is the prototype of y
ya; // 40, its own characteristics
yc; // 50, it is also its own characteristic
yb; // 20 Get from the prototype: yb (no) -> y.[[Prototype]].b (yes): 20
delete ya; // Delete your own "а"
ya; // 10 Get it from the prototype
z = {a: 100, e: 50}
y.[[Prototype]] = z; // Modify the prototype of y to z
ya; // 100 Get from prototype z
ye // 50, also obtained from prototype z
zq = 200 // Add new attribute to the prototype
yq // Modification also applies to y
This example shows the important functions and mechanisms of the prototype as auxiliary object attributes, just like you need your own attributes. Compared with your own attributes, these attributes are delegate attributes. This mechanism is called a delegate, and the prototype model based on it is a delegate prototype (or a delegate based prototype). The reference mechanism is called sending information 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 regarded as a prototype, that is, the prototype can also have its own prototype. These prototypes are connected together to form a so-called prototype chain. Chains are also hierarchical in static classes, but it can be easily rearranged, changing hierarchies and structures.
The code copy is as follows:
x = {a: 10}
y = {b: 20}
y.[[Prototype]] = x
z = {c: 30}
z.[[Prototype]] = y
za // 10
// za found in the prototype chain:
// za (no) ->
// z.[[Prototype]].a (no) ->
// z.[[Prototype]].[[Prototype]].a (yes): 10
If an object and its prototype chain cannot respond to message sending, the object can activate the corresponding system signal, which may be processed by other delegates on the prototype chain.
This system signal is available in many implementations, including systems based on 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):
The code copy is as follows:
var object = {
// catch the system signal that cannot respond to messages
__noSuchMethod__: function (name, args) {
alert([name, args]);
if (name == 'test') {
return '.test() method is handled';
}
return delegate[name].apply(this, args);
}
};
var delegate = {
square: function (a) {
return a * a;
}
};
alert(object.square(10)); // 100
alert(object.test()); // .test() method is handled
That is to say, based on the implementation of static classes, when the message cannot be responded to, 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 has the characteristic after a series of changes.
Regarding ECMAScript, the specific implementation is: using a delegate-based prototype. However, as we will see from specifications and implementations, they also have their own characteristics.
Concatenative Model
Honestly, it is necessary to say something about another situation (not used in ECMASCript as soon as possible): this situation when the prototype is complex from other objects to original replacement of native objects. In this case code reuse is a real copy (clone) of an object rather than a delegate during the object creation stage. This prototype is called the concatenative prototype. Copying the properties of all prototypes of an object can further completely change its properties and methods, and as a prototype, you can also change yourself (in a delegate-based model, this change will not change the behavior of existing objects, but will change its prototype characteristics). The advantage of this method is that it can reduce the time of scheduling and delegating, while the disadvantage is that it uses memory.
Duck type
Back to the dynamically weak type change object, compared with the static class-based model, it is necessary to check whether it can do these things and what type (class) the object has, but whether it can be related to the corresponding message (i.e. whether it has the ability to do it after checking is necessary).
For example:
The code copy is as follows:
// In a static-based model
if (object instanceof SomeClass) {
// Some behaviors are running
}
// In dynamic implementation
// It doesn't matter what type of the object is at this time
// Because mutations, types, and characteristics can be changed freely.
// Whether important objects can respond to test messages
if (isFunction(object.test)) // ECMAScript
if object.respond_to?(:test) // Ruby
if hasattr(object, 'test'): // Python
This is called the Dock type. That is, objects can be identified by their own characteristics when checking, rather than the location of objects in the hierarchy or they belong to any specific type.
Key concepts based on prototypes
Let's take a look at the main features of this method:
1. The basic concept is the object
2. Objects are completely dynamic and mutable (in theory, it can be completely transformed from one type to another)
3. Objects do not have strict classes that describe their own structure and behavior, and objects do not need classes.
4. Objects do not have class classes but can have prototypes. If they cannot respond to messages, they can delegate to prototypes.
5. The prototype of the object can be changed at any time during runtime;
6. In a 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 the original copy cloned from other objects and further becomes a completely independent copy original. The transformation of the prototype's characteristics will not affect the object cloned from it.
8. If the message cannot be responded to, its caller can take additional measures (e.g., change the scheduling)
9. Object failure can be determined not by their level and which class they belong to, but by the current characteristics.
However, there is another model that we should consider as well.
Based on dynamic classes
We believe that the difference "class VS prototype" shown in the above example is not that important in this dynamic class-based model (especially if the prototype chain is unchanged, it is still necessary to consider a static class for more accurate distinction). As an example, it can also be used in Python or Ruby (or other similar languages). These languages all use dynamic class-based paradigms. However, in some aspects, we can see certain functions implemented based on prototypes.
In the following example, we can see that just based on the delegate prototype, we can amplify a class (prototype), thereby affecting all objects related to this class, we can also dynamically change the class of this object at runtime (providing a new object for the delegate), etc.
The code copy is as follows:
# Python
class A(object):
def __init__(self, a):
self.a = a
def square(self):
return self.a * self.a
a = A(10) # Create an instance
print(aa) # 10
Ab = 20 # Provide a new property for the class
print(ab) # 20 You can access it in the "a" instance
a = 30 # Create a property of its own
print(ab) # 30
del ab # Delete its own attributes
print(ab) # 20 - Get it from the class again (prototype)
# Like a prototype-based model
# Can change the prototype of an object at runtime
class B(object): # empty class B
pass
b = B() # B instance
b.__class__ = A # Dynamically change class (prototype)
ba = 10 # Create new attribute
print(b.square()) # 100 - Class A method is available at this time
# Can display references on deleted classes
del A
del B
# But the object still has implicit references, and these methods are still available
print(b.square()) # 100
# But you can't change the class at this time
# This is the feature of implementation
b.__class__ = dict # error
The implementation in Ruby is similar: it also uses completely dynamic classes (by the way, in the current version of Python, compared with Ruby and ECMAScript, amplifying classes (prototypes) cannot be done), we can completely change the characteristics of an object (or class) (add methods/attributes to the class, and these changes will affect the existing objects), but it cannot dynamically change the class of an object.
However, this article is not specifically for Python and Ruby, so we won't say much, let's continue to discuss ECMAScript itself.
But before this, we have to look at the "synthetic sugar" that is found in some OOPs, because many previous articles about JavaScript often cover these issues.
The only wrong sentence that needs to be noted in this section is: "JavaScript is not a class, it has a prototype and can replace a class." It is very necessary to know that not all class-based implementations are completely different. Even if we may say "JavaScript is different", it is also necessary to consider (besides the concept of "classes") that there are other related features.
Other features of various OOP implementations
In this section, we briefly introduce other features and various OOP implementations about code reuse, including OOP implementations in ECMAScript. The reason is that the previous appearance of OOP implementation in JavaScript has some habitual thinking restrictions. The only main requirement is that it should be proved technically and ideologically. It cannot be said that you have not discovered the syntax sugar function in other OOP implementations, and you have no idea that JavaScript is not a pure OOP language, which is wrong.
Polymorphic
In ECMAScript, there are several polymorphisms in which objects mean.
For example, a function can be applied to different objects, just like the properties of a native object (because this value is determined when entering the execution context):
The code copy is as follows:
function test() {
alert([this.a, this.b]);
}
test.call({a: 10, b: 20}); // 10, 20
test.call({a: 100, b: 200}); // 100, 200
var a = 1;
var b = 2;
test(); // 1, 2
However, there are exceptions: Date.prototype.getTime() method, according to the standard, there should always be a date object, otherwise an exception will be thrown.
The code copy is as follows:
alert(Date.prototype.getTime.call(new Date())); // time
alert(Date.prototype.getTime.call(new String(''))); // TypeError
The so-called parameter polymorphism when defining a function is equivalent to all data types, but it only accepts polymorphic parameters (such as the .sort sorting method of an array and its parameters - the sorting function of polymorphism). By the way, the above example can also be considered as a parameter polymorphism.
The method in the prototype can be defined as empty, and all created objects should be redefined (implemented) the method (i.e. "One interface (signature), multiple implementations").
Polymorphism is related to the Duck type we mentioned above: i.e. the type of the object and its position in the hierarchy are not that important, but it can be easily accepted if it has all the necessary features (i.e. the general interface is important, and the implementation can be varied).
Package
There are often wrong views about encapsulation. In this section, we will discuss some syntactic sugars in OOP implementations - that is, well-known modifiers: in this case, we will discuss some OOP implementations convenient "sugars" - well-known modifiers: private, protected and public (or objects' access level or access modifiers).
Here I would like to remind you about the main purpose of encapsulation: encapsulation is an abstract addition, rather than selecting a hidden "malicious hacker" who writes something directly into your class.
This is a big mistake: use hidden in order to hide.
Access levels (private, protected and public), for the convenience of programming, have been implemented in many object-oriented (really very convenient syntax sugar), and describe and build the system more abstractly.
These can be seen in some implementations (such as Python and Ruby already mentioned). On the one hand (in Python), these __private _protected properties (through the underscore name specification) are not accessible from the outside. Python, on the other hand, can be accessed from the outside through special rules (_ClassName__field_name).
The code copy is as follows:
class A(object):
def __init__(self):
self.public = 10
self.__private = 20
def get_private(self):
return self.__private
# outside:
a = A() # Example of A
print(a.public) # OK, 30
print(a.get_private()) # OK, 20
print(a.__private) # Failed because it can only be available in A
# But in Python, special rules can be accessed
print(a._A__private) # OK, 20
In Ruby: On the one hand, it has the ability to define the characteristics of private and protected, and on the other hand, there are also special methods (such as instance_variable_get, instance_variable_set, send, etc.) to obtain encapsulated data.
The code copy is as follows:
class A
def initialize
@a = 10
end
def public_method
private_method(20)
end
Private
def private_method(b)
return @a + b
end
end
a = A.new # New instance
a.public_method # OK, 30
aa # failed, @a - is a private instance variable
# "private_method" is private and can only be accessed in Class A
a.private_method # Error
# But there is a special metadata method name that can get the data
a.send(:private_method, 20) # OK, 30
a.instance_variable_get(:@a) # OK, 10
The main reason is the encapsulation (note that I don't use "hidden") data that the programmer wants to obtain. If this data is altered incorrectly in some way or there are any errors, then the entire responsibility is the programmer, but not simply "spellow" or "change certain fields casually." But if this happens frequently, it is a bad programming habit and style, because it is usually a common API to "talk" with objects.
Repeat, the basic purpose of encapsulation is to abstract it from the user of auxiliary data, rather than to prevent hackers from hiding data. More serious, encapsulation is not to use private to modify data to achieve the purpose of software security.
Encapsulation of helper objects (local), we provide feasibility for behavior changes of public interfaces with minimal cost, localization and predictive changes, which is exactly the purpose of encapsulation.
In addition, the important purpose of the setter method is to abstract complex calculations. For example, element.innerHTML setter - abstract statement - "The HTML inside this element is now as follows", and the setter function in the innerHTML attribute will be difficult to calculate and check. In this case, the problem mostly involves abstraction, but encapsulation can happen.
The concept of encapsulation is not only related to OOP. For example, it could be a simple function that encapsulates all kinds of calculations so that it is abstract (no need to let the user know, for example how the function Math.round(......) is implemented, the user simply calls it). It's an encapsulation, note that I didn't say it is "private, protected and public".
The current version of the ECMAScript specification does not define private, protected and public modifiers.
However, in practice it is possible to see something named "imitate JS encapsulation". Generally the purpose of this context is to be used (as a rule, the constructor itself). Unfortunately, often implementing this "imitation" can be done, and programmers can produce pseudo-absolutely non-abstract entities to set the "getter/setter method" (I say it again, it's wrong):
The code copy is as follows:
function A() {
var _a; // "private" a
this.getA = function _getA() {
return _a;
};
this.setA = function _setA(a) {
_a = a;
};
}
var a = new A();
a.setA(10);
alert(a._a); // undefined, "private"
alert(a.getA()); // 10
Therefore, everyone understands that for each created object, the getA/setA method is also created, which is also the reason for the memory increase (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 method". Note: ECMA-262-3 standard does not define any concept of "private method".
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 additional methods, while others cannot).
In addition, in JavaScript, if the encapsulation is still misinterpreted as a way to prevent malicious hackers from writing certain values automatically in place of using the setter method, the so-called "hidden" and "private" are not "hidden". Some implementations can obtain values on the relevant scope chain (and all corresponding variable objects) by calling context to the eval function (which can be tested on SpiderMonkey 1.7).
The code copy is as follows:
eval('_a = 100', a.getA); // or a.setA, because "_a" methods on [[Scope]]
a.getA(); // 100
Alternatively, in the implementation, allow direct access to the active object (such as Rhino), by accessing the corresponding properties of the object, the value of the internal variable can be changed:
The code copy is as follows:
// Rhino
var foo = (function () {
var x = 10; // "private"
return function () {
print(x);
};
})();
foo(); // 10
foo.__parent__.x = 20;
foo(); // 20
Sometimes, in JavaScript, the purpose of "private" and "protected" data is achieved by underscoreing variables (but compared to Python, this is just a naming specification):
var _myPrivateData = 'testString';
It is often used to enclose the execution context with brackets, but for real auxiliary data, it has no direct association with objects, and it is just convenient to abstract from external APIs:
The code copy is as follows:
(function () {
// Initialize the context
})();
Multiple inheritance
Multi-inheritance is a very convenient syntactic sugar for code reuse improvements (if we can inherit one class at a time, why can't we inherit 10 at a time?). However, due to some shortcomings in 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 ancestors' self-programming languages have such capabilities. But in some implementations (such as SpiderMonkey) using __noSuchMethod__ can be used to manage scheduling and delegate to replace prototype chains.
Mixins
Mixins is a convenient way to reuse code. Mixins has been recommended as a replacement for multiple inheritance. These independent elements can all be mixed with any object to extend their functionality (so objects can also mix multiple Mixins). The ECMA-262-3 specification does not define the concept of "Mixins", but according to the Mixins definition and ECMAScript has dynamic mutable objects, so there is no obstacle to simply expanding features using Mixins.
Typical examples:
The code copy is as follows:
// helper for augmentation
Object.extend = function (destination, source) {
for (property in source) if (source.hasOwnProperty(property)) {
destination[property] = source[property];
}
return destination;
};
var X = {a: 10, b: 20};
var Y = {c: 30, d: 40};
Object.extend(X, Y); // mix Y into X
alert([Xa, Xb, Xc, Xd]); 10, 20, 30, 40
Please note that I took these definitions ("mixin", "mix") in quotes mentioned in ECMA-262-3. There is no such concept in the specification, and it is not a mix but a commonly used way to extend objects through new features. (The concept of mixins in Ruby is officially defined. mixin creates a reference to the module instead of simply copying all attributes of the module to another module - in fact: creating an additional object (prototype) for the delegate).
Traits
Traits and mixins have similar concepts, but it has many functions (by definition, because mixins can be applied so it cannot contain states because it has the potential to 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 OOPs are similar to mixins and traits. However, compared with mixins and traits, an interface enforces the implementation class to implement its method signature behavior.
Interfaces can be regarded as abstract classes. However, compared with abstract classes (the methods in abstract classes can only implement part of them, and the other part is still defined as signatures), inheritance can only be a single inheritance base class, but it can inherit multiple interfaces. This is why interfaces (mixed multiple) can be regarded as an alternative to multiple inheritance.
The ECMA-262-3 standard does not define the concept of "interface" nor the concept of "abstract class". However, as imitation, it can be implemented by an object that "empty" method (or an exception thrown in an empty method, telling the developer that this method needs to be implemented).
Object combination
Object combination is also one of the dynamic code reuse techniques. Object combinations differ from high flexibility inheritance, which implements a dynamic and variable delegate. And this is also based on the principal prototype. In addition to dynamic mutable prototypes, the object can be a delegated aggregate object (create a combination as the result - the aggregation), and further send messages to the object and delegate to the delegate. This can be more than two delegates, because its dynamic nature determines that it can be changed at runtime.
The __noSuchMethod__ example mentioned is like this, but also lets us show how to use delegates explicitly:
For example:
The code copy is as follows:
var _delegate = {
foo: function () {
alert('_delegate.foo');
}
};
var aggregate = {
delegate: _delegate,
foo: function () {
return this.delegate.foo.call(this);
}
};
aggregate.foo(); // delegate.foo
aggregate.delegate = {
foo: function () {
alert('foo from new delegate');
}
};
aggregate.foo(); // foo from new delegate
This object relationship is called "has-a", while integration is the relationship of "is-a".
Due to the lack of display combinations (flexibility compared to inheritance), it is also OK to add intermediate code.
AOP Features
As an aspect-oriented function, function decorators can be used. The ECMA-262-3 specification does not have a clearly defined concept of "function decorators" (as opposed to Python, the word is officially defined in Python). However, functions with functional parameters are decorative and activated in some ways (by applying so-called suggestions):
The simplest example of a decorator:
The code copy is as follows:
function checkDecorator(originalFunction) {
return function () {
if (fooBar != 'test') {
alert('wrong parameter');
return false;
}
return originalFunction();
};
}
function test() {
alert('test function');
}
var testWithCheck = checkDecorator(test);
var fooBar = false;
test(); // 'test function'
testWithCheck(); // 'wrong parameter'
fooBar = 'test';
test(); // 'test function'
testWithCheck(); // 'test function'
in conclusion
In this article, we have sorted out the introduction to OOP (I hope this information has been useful to you), and in the next chapter we will continue to implement ECMAScript in object-oriented programming.