📜 ⬆️ ⬇️

CoffeeScript: Classes

CoffeeScript: Classes

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 # 1 

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 # 1 property # undefined 

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() # TypeError: has no method '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 # 1 

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(); //1 

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() # 1 

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 # 1 

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() # 1 

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 # [] Object::toString.call(object) # [object Array] object.method() # TypeError: Object has no method 'method' 

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() #1 

Of course, it doesn't matter how the object reference will be returned:

 A = (param) -> method: -> param object = new A 1 object.method() #1 

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 # 3 

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()() # global 


It is worth noting that when using strict mode ( use strict ), this will indicate undefined :

 A = -> 'use strict' -> @ new A()() # undefined 


In order for this to point to the newly created object, you need to explicitly add the new operator:

 A = -> new -> @ new A() # object 

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 # 3 

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:: # object object.constructor::constructor 2 # 3 

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 ( .. ):

 @..:: # this.constructor.prototype 



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; //1 

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(); //1 

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 # 3 

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 # 4 

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 #  .coffee   JavaScript     . coffee -p file.coffee #      coffee -e 'console.log i for i in [0..5]' #    coffee -t #   

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, //    hasOwnProperty __hasProp = {}.hasOwnProperty; //  __extends     , // , , .. child    parent __extends = function(child, parent) { //    parent for (var key in parent) { //    ( ) if (__hasProp.call(parent, key)) { //    child,   parent. //       , //   c   child[key] = parent[key]; } } //   - function ctor() { //    constructor    child this.constructor = child; } //   . //        parent ctor.prototype = parent.prototype; //        child.prototype = new ctor(); //      //     super child.__super__ = parent.prototype; //    return child; }; //   A = (function() { function A() {}; return A; })(); //   B = (function(_super) { //   __extends. //       B, //  -     A __extends(B, _super); function B() { //    A    B //  A.apply(this, arguments); return B.__super__.constructor.apply(this, arguments); } return B; })(A); 


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

-


Encapsulation


PS: , , CoffeeScript — .

Source: https://habr.com/ru/post/146405/


All Articles