
In
ECMAScript, the concept of “class”
is still missing, in the classical understanding of this term, however,
CoffeeScript has such a concept, so today we will look at this question in great detail.
Content:
1. Basic concepts
2. Class members
2.1.
Constructor method
2.2. Public class members
2.3. Closed class members
2.4. Protected class members
2.5. Static class members
2.6. Operator => (fat arrow)
3. Inheritance
4. Additional literature
')
Basic concepts
A class is a special syntactic construct represented by a set of generalized logically related entities.
An object is an unordered set of properties that has a prototype object, the value of which can be an object or
null .
A constructor is an object of type
Function that creates and initializes objects.
Prototype — Used to implement the inheritance of structure, state, and behavior.
The
prototype property is automatically created for each function to ensure that the function can be used as a constructor.
Instantiation is the creation of an instance of a class.
General theory
CoffeeScript is a dynamic language with a prototype-class paradigm in which work with classes is implemented at the level of delegating prototyping.
More precisely, a class in
CoffeeScript is an abstract data type that defines an alternative way to work with objects.
In order to define a class, you need to use the
class specifier:
class
The result of the broadcast:
(function() { function Class() {}; return Class; })();
As you can see, a class is just a wrapper (
syntactic sugar ) represented by a constructor function:
Object::toString.call class # [object Function]
Creating an instance of a class (instantiation) is done using the operator
new class A instance = new A
After applying the
new operator to the constructor function, the internal method
[[Construct]] is activated, which is responsible for creating the object:
class A Object::toString.call new A # [object Object]
In other words, the definition of a class is not the creation of an object, until instantiation occurs:
class A constructor: (@variable) -> A 1 variable # 1
Notice that the
variable has become available in the global namespace.
Without instantiation, the definition of a class is similar to the following code:
A = (variable) -> @variable = variable A 1 variable
After we initialize the creation of an object,
this inside the constructor will point to the object being created:
class A constructor: (@property) -> object = new A 1 object.property # 1 property # undefined
As you can see,
this no longer indicates a global object, and the
property variable is not defined. The same applies to code without a
class specifier:
A = (@property) -> object = new A 1 object.property
In fact, there is no difference between these options.
However, it is worth noting that the syntax with the
class specifier is more preferable for creating such objects and now I will tell you why ...
If you remember, I already mentioned the implicit addition of a
return statement in functions. Such behavior can play a very bad joke with us:
A = (variable) -> method = -> variable
It seems to be quite harmless code?
Well, let's try to create an object:
A = (param) -> method = -> param object = new A 1 object.method()
What the heck?
To understand why this is happening enough to see the result of the broadcast:
var A = function(param) { return this.method = function() { return param; }; };
Unfortunately, there is no normal way to avoid such surprises (I now mean implicit addition of a
return statement ).
First, we can preface a parameter with the
@ symbol and define a function as an initialization parameter:
A = (@method, @param) -> object = new A (-> @param), 1 do object.method
This solution is possible due to the fact that
this is determined at the time of creation of the object.
Let's look at the result of the broadcast:
var A, object; A = function(method, param) { this.method = method; this.param = param; }; object = new A((function() { return this.param; }), 1); object.method();
Note: Functions that take other functions as parameters or return other functions as a result are called higher-order functions ( first-class functions ). In this case, the function parameter is called a function parameter or fungarg .
The next way that allows us to determine the members of an object is to use
prototype objects :
A = (@param) -> A::method = -> @param object = new A 1 object.method()
It is worth noting that the definition of object properties directly, and through a prototype object is not the same:
A = (@param) -> A::param = 0 object = new A 1 object.param
The intrinsic properties have a higher priority, therefore, they are first analyzed, and then the search is performed in a chain of prototypes.
Let me remind you that we are now considering the issue of implicitly adding
return statements in constructor functions and ways to solve this problem.
A = (@param) -> @method = -> @param @ object = new A 1 object.method()
this will point to the newly created object.
It should be noted that if the return value is an object, then it will be the result of the expression
new :
A = (@param) -> @method = -> @param [] object = new A 1 object
Accordingly, in order to have access to the
method property, you must explicitly return the object to which this property belongs:
A = (param) -> object = {} object.method = -> param object object = new A 1 object.method()
Of course, it doesn't matter how the object reference will be returned:
A = (param) -> method: -> param object = new A 1 object.method()
In this case, in the constructor function
A , a reference to the
anonymous object is returned.
All the same applies to the return of functions:
A = (one) -> (two) -> one + two object = new A 1 object 2
As you understand, the use of the operator
new in this case is optional. And
this inside the nested function will point to the global object:
A = -> -> @ new A()()
It is worth noting that when using strict mode ( use strict ), this will indicate undefined :
A = -> 'use strict' -> @ new A()()
In order for
this to point to the newly created object, you need to explicitly add the
new operator:
A = -> new -> @ new A()
Then you should have a counter question about how to pass the parameters of the nested function:
A = (one) -> new (two) -> one + two object = new A 1 object.constructor 2
Please do not confuse the internal
CoffeeScript constructor () method and the link to the
constructor function, through which you can get a reference to the prototype of the object:
A = -> new -> object = new A object.constructor::
This behavior is carried out by dispatching calls, when an object, following a chain of delegating pointers, cannot find the corresponding property; it refers to its prototype. Such a chain of calls from an object to a prototype is called a
prototype chain .
This behavior is well known to programmers writing in
Ruby ,
Python ,
SmallTalk, and
Self .
Note: unfortunately, there is no pseudonym for the constructor as for ptototype , but maybe in the near future this will be taken into account, since In some CoffeeScript dialects, this is already implemented. For example, in coco, the word constructor can be replaced with a colon ( .. ):
@..::
Class members
Constructor () method
The class constructor is a special
constructor () method defined in the class body and intended to initialize the members of the object.
class A A::property = 1 object = new A object.property # 1
In this example, we initialized the creation of a class named
A.The only member of the class is the property
property , which is formally located in the prototype of the object
A.Because
This will always point to
A (and translated too), it makes sense to rewrite the class definition:
class A @::property = 1
Despite the compactness of the record, such a definition of class members is not accepted in
CoffeScript . In addition, there is a more elegant solution:
class A property: 1
In order to determine the members of a class outside of it, use the first notation:
class A A::property = 1
As noted earlier, each class instance has a link to its own constructor, to which the
prototype property is available.
Having thus obtained a link to the original prototype of the object, you can define new members of the class:
class A object1 = new A object1.constructor::property = 1 object2 = new A object2.property # 1 object2.constructor is A # true
If you need to add several properties to the prototype, then it makes sense to make it “en masse”:
class A A:: = property: 1 method: -> @property object2 = new A object2.method() # 1
However, in this case, the
constructor property will point to another object:
class A A:: = {} object = new A object.constructor is A # false
Despite the fact that the link to the original prototype will be lost, few people will notice:
class A A:: = {} object = new A object.constructor::property = 1 # ! object.property # 1
You need to be especially careful in such situations, because by adding a new property through an instance of the class, we will add this property to all objects!
We look at an example carefully:
class A A:: = property: 1 method: -> @property object = new A object.constructor::property = 'Oh my god!' object.method() # 1 object.property # 1 list = [] list.property # 'Oh my god!'
This behavior has become possible due to the fact that the
constructor property now points to
Object :
class A A:: = {} object = new A object.constructor # [Function: Object] object.constructor is Object # true
In other words, without knowing it, we did the following:
Object::property = 'Oh my god!'
Of course, few people will like to see third-party methods in their objects.
In order to have a valid reference to the original constructor, you should explicitly recreate it:
class A A:: = constructor: A object = new A object.constructor::property = 1 object.property # 1 object.constructor is A # true
Now the reference to the prototype object is correct.
If you remember, we started to consider the
constructor () class method. So, the only purpose of this method is to initialize the parameters:
class A constructor: (param) -> @param = param object = new A 1 object.param # 1
Accordingly, if there is no need to pass parameters to the class constructor, it will be reasonable to omit the
constructor () method.
Ideologically, it so happened that the constructor should not return any values and cannot be overloaded (there is no operator and function overload in
CoffeeScipt ).
Since passing parameters to a constructor function is quite a frequent operation,
CoffeeScript has a special syntax:
class A constructor: (@param) -> object = new A 1 object.param # 1
Let's look at the result of the broadcast:
var A, object; A = (function() { function A(param) { this.param = param; } return A; })(); object = new A(1); object.param;
As you can see,
param is a direct property of property
A , i.e. at any time it can be modified and even deleted.
class A constructor: (@param) -> object = new A 1 object.param = 2 object.param # 2
In this case, we redefined the property value of a specific instance of the class, which does not affect other instances.
Next, we will look at the implementation of access level modifiers that are not part of the
CoffeeSript language (although many people think so).
Public class members (public)
In many object-oriented languages,
encapsulation is defined by the use of modifiers such as
public ,
private ,
protected, and to some extent
static . In the
CoffeeScript for these purposes, a slightly different approach is proposed, I propose to begin the consideration of this issue with public members.
All public members of the class are written in associative notation, without the leading
@ and / or
this :
class A constructor: (@param) -> method: -> @param object = new A 1 object.method() # 1
The result of the broadcast:
var A, object; A = (function() { function A(param) { this.param = param; } A.prototype.method = function() { return this.param; }; return A; })(); object = new A(1); object.method();
From the result of the translation it is clear that the public members of the class are added to the prototype of the object
A.Therefore, the treatment to the members of the class is technically carried out as in any other object:
class A property: 1 method: -> @property object = new A object.method() # 1
Private class members (private)
In the private members of the class, calls to the member are allowed only from the methods of the class in which this member is defined. Class heirs do not have access to private members.
Private class members are written in literal notation:
class A constructor: (@param) -> property = 1 # private method: -> property + @param object = new A 1 object.method() # 2 object.property # undefined
Technically, the private members of a class are ordinary local variables:
var A, object; A = (function() { var property; function A(param) { this.param = param; } property = 1; A.prototype.method = function() { return property + this.param; }; return A; })(); object = new A(1); object.method(); object.property; # 2
Currently, the implementation of closed members is very limited. In particular, private members of a class are not accessible to members defined outside the class:
class Foo __private = 1 Foo::method = -> try __private catch error 'undefined' object = new Foo object.method() #undefined
To understand why this is happening it is worth looking at the result of the broadcast:
var A; A = (function() { var __private; function A() {} __private = 1; return Foo; })(); A.prototype.method = function() { try { return __private; } catch (error) { return 'undefined'; } };
Surely you already have a question: why can't the definition of external members be placed in a constructor function?
In fact, this will not solve the problem, because the definition of class members may be in different files!
Partially solve this problem in a very simple way:
class A constructor: (@value) -> privated = (param) -> @value + param __private__: (name, param...) -> eval(name).apply @, param if !@constructor.__super__ A::method = -> @__private__ 'privated', 2 class B extends A B::method = -> @__private__ 'privated', 2 object = new A 1 object.method() # 3 object = new B 1 object.method() # undefuned object.privated # undefuned
As you can see, the
privated class
member is available only to members of the base class.
All we needed was to define the following method in the base class:
__private__: (name, param...) -> eval(name).apply @, param if !@constructor.__super__
But there is one problem, our private property is available directly through the
__pri__ method:
object.__private__ 'privated', 2
Taking into account minor changes (thanks to
nayjest for
paying attention to this and proposing a solution), you can close this question:
__private__: (name, param...) -> parent = @constructor.__super__ for key, value of @constructor:: allow = on if arguments.callee.caller is value and not parent eval(name).apply @, param if alllow
The advantages of this implementation include:
+ simplicity and efficiency
+ easy embedding in existing code
Of the disadvantages:
- use the
eval function and
arguments.callee.caller- extra “layer”
__private__- lack of real practical value
- in the
__private __ method, the descriptor attributes controlling the enumeration of this method, modification and deletion are not set.
The last point related to the shortcomings of the implementation can be corrected as follows:
Object.defineProperty @::, '__private__' value: (name, param...) -> eval(name).apply @, param if !@constructor.__super__
Now the
__private __ method
will not be listed by the
for-of loop, it cannot be modified or deleted. Let's look at the final example:
class A constructor: (@value) -> privated = (param) -> @value + param Object.defineProperty @::, '__private__' value: (name, param...) -> parent = @constructor.__super__ for key, value of @constructor:: allow = on if arguments.callee.caller is value and not parent eval(name).apply @, param if allow A::method = -> @__private__ 'privated', 2 class B extends A B::method = -> @__private__ 'privated', 2 object = new A 1 object.method() # 3 object = new B 1 object.method() # undefuned object.privated # undefuned i for i of object # 3, value, method
Protected class members (protected)
The protected members of a class are accessible only within the methods of the base class and its heirs.
Formally,
CoffeeScript does not have a special syntax for defining protected class members. Nevertheless, we can independently implement this functionality:
class A constructor: (@value) -> protected = (param) -> @value + param __protected__: (name, param...) -> parent = @constructor.__super__ for key, value of @constructor:: allow = on if arguments.callee.caller is value eval(name).apply @, param if allow A::method = -> @__protected__ 'privated', 2 class B extends A B::method = -> @__protected__ 'privated', 2 object = new A 1 object.method() # 3 object = new B 1 object.method() # 3 object.protected # undefuned
As you can see, the architecture of this solution is almost identical to the solution of the problem with closed class members, and therefore has the same problem with the attributes of the descriptor. The final decision will be the following:
Object.defineProperty @::, '__private__' value: (name, param...) -> parent = @constructor.__super__ for key, value of @constructor:: allow = on if arguments.callee.caller is value eval(name).apply @, param if allow
It is worth noting that this is not the only solution to this problem, it is simply the most universal for understanding the implementation.
Static class members (static)
Static class member:
- preceded by
@ or
this- can only exist in a single copy
- for non-static members of the class is available only through the prototype object of the base class
Consider an example of determining a static member of a class:
class A @method = (param) -> param A.method 1 # 1
Now let's consider the possible (most appropriate) forms for writing a static member of a class:
@property: @ @property = @ this.property = @ this.constructor::property = 1 @constructor::property = 1 Class.constructor::property = 1
If you notice, the use of the
@ symbol is more universal.
Access to other non-static members of a class is possible only through the prototype object of the base class:
class A property: 1 @method: -> @::property do A.method # 1
Access to other static members of the class is available through the symbol @ or
this or the name of the class:
class A @property: 1 @method: -> @property + A.property do A.method # 2
Another point worth paying special attention to is the use of the
new operator:
class A @property: 1 @method: -> @property object = new A object.method() # TypeError: Object # <A> has no method 'method'
As you can see, a call to a nonexistent method resulted in a
TypeError error
type !
Now, let's consider the correct way to call a static member of a class through an instance of a class:
class A @property: 1 @method: -> @property object = new A object.constructor.method() # 1
=> (fat arrow)
Another important point when working with class members is the ability to use the
=> (fat arrows) operator , which allows you to keep the context of the call.
For example, this can be useful for creating callback functions.
class A constructor: (@one, @two) -> method: (three) => @one + @two + three instance = new A 1, 2 object = (callback) -> callback 3 object instance.method # 6
We could achieve the same result using the
call () method:
class A constructor: (@one, @two) -> method: (three) -> @one + @two + three instance = new A 1, 2 object = (callback) -> callback.call instance, 3 object instance.method # 6
Now, let's consider the situation with the use of a
predicate :
class A constructor: (@splat...) -> method: (three) => @splat instance = new A 1, 2, 3, 4, 5 object = (callback, predicate) -> predicate callback() object instance.method, (callback) -> callback.filter (item) -> item % 2 # [1, 3, 5]
In this example, we initialized the creation of class
A , with the
nth number type parameters.
Next, a member of the
method class returned an array with the parameters passed to the constructor. After that, the resulting array was passed to the predicate, which filtered the values of the array modulo 2, with the result that a new array was obtained.
Inheritance
Inheritance in
coffeScript is done using the
extends operator.
Consider an example:
class A constructor: (@property) -> method: -> @property class B extends A object = new B 1 object.method() # 1
Although class
B does not define its own class members, it inherits them from
A.Now, notice that the property
property is also available inside class
B :
class A constructor: (@property) -> class B extends A method: -> @property object = new B 1 object.method() # 1
As you can see, the essence of the
extends operator is very simple - the establishment of a relationship between two objects.
It is not difficult to guess that the
extends operator can be used not only with classes:
A = (@param) -> A::method = (x) -> @param * x B = (@param) -> B extends A (new B 2).method 2
Unfortunately, the official
CoffeeScript documentation is rather “poor” in examples, but you can always use the translator to watch the implementation of a particular piece of code:
The most important options for analyzing software code:
coffee -c file.coffee
The
-n (--nodes) parameter has a very important value for analyzing the structure of the program; it returns a
syntax tree :
class A @method: @ class B extends A do (new B).method
coffee -n
Block Class Value "A" Block Value Obj Assign Value "this" Access "method" Value "this" Class Value "B" Value "A" Block Call Value Parens Block Op new Value "B" Access "method"
For the most complete information about the syntax of
CoffeeScript, see the
nodes.coffee section of official documentation.
If you define methods in classes with the same name, then the native method will override the inherited method:
class A constructor: -> method: -> 'A' class B extends A method: -> 'B' object = new B object.method() # B
Although this behavior is quite expected, we have the opportunity to call a class
A method from
B :
class A A::method = -> 'A' class B extends A B::method = -> super object = new B object.method() # A
This code is not much different from the previous one, with a few exceptions, that in the class
B method a certain
super operator is returned.
The task of the
super operator is to call the properties defined in the parent class and initialize the call parameters.
In this case, the inheritance structure has no value, the method of the closest class in the hierarchical inheritance chain is called:
class A A::method = -> 'A' class B extends A B::method = -> 'B' class C extends B C::method = -> super object = new C object.method() # B, method B
If the method is not defined in the direct parent, then the search continues by following the chain of
delegating pointers :
class A A::method = -> 'A' class B extends A class C extends B C::method = -> super object = new C 1 object.method() # 'A', method A
The
super operator can also take parameters:
class A constructor: (@param) -> A::method = (x) -> x + @param class B extends A B::method = -> super 3 object = new B 1 object.method 2 # 4 (3 + 1)
Despite the fact that the method was called with parameter 2, we later redefined this value by 3.
If you define the
super operator in the
constructor method, you can override the parameters with which the class constructor is initialized:
class A constructor: (@param) -> A::method = (x) -> x + @param class B extends A constructor: (@param) -> super 3 object = new B 1 object.method 2 # 5 (2 + 3)
Of course, simultaneous use of the super operator in class members and constructor is permissible : class A constructor: (@param) -> A::method = (x) -> x + @param class B extends A constructor: (@param) -> super 3 B::method = (x) -> super 4 object = new B 1 object.method 2 # 7 (3 + 4)
Now, I propose to consider in detail the internal implementation of the pattern that implements the inheritance interface: var A, B, object,
Let's sum up:
- Classes allow you to more clearly visualize the structure of generalized logically related entities;- The presence of classes allows some misunderstanding when working with objects and inheritance, especially for those who worked with classes before;- Despite the fact that access level modifiers ( public , private and protected ) are not defined in ECMAScript , they can be implemented independently (albeit through one place). - The internal implementation of the classes is very simple;ECMA-262-3. 7.1. :ECMA-262-3. 7.2. : ECMAScript-EncapsulationPS: , , CoffeeScript — .