📜 ⬆️ ⬇️

Javascript: OOP, prototypes, closures, Timer.js "class"

Hello, programmers are beginners, complete, and all sympathizers. As you know, nothing is cognized as well as on personal experience. The main thing that experience was useful. And in the continuation of this simple thought, I want to suggest doing several useful things at once:

A warning! If you do not expect anything fun from the article ... then you are mistaken. People with a reduced sense of humor to read ... even more recommended! Well, let's get started ...

I note that at the time of writing this article (hello, apocalyptic-glamorous 2012!), There is no real OOP in javascript, but there are tricks with which you can implement the basic principles of object-oriented programming. For those who are just discovering this insanely interesting topic, I will explain in my own words the specificity of this method.

Part 1. OOP with a human face.


Briefly: OOP is not at all a sacred mantra, but in fact, simply a method of organizing applications, structuring code, centralizing methods, and combining entities into a single hierarchical family. Just as submarines and airplanes were built, adopting buoyancy and volatility experience from nature, OOP applications also use the perception of software entities as certain “living” objects, adopting characteristics and properties known to us from the real world. .

In other words, a certain entity is created, which not only has its own properties and methods, but also knows how to generate descendants and evolve! This is called an extension. As if a careful parent, an object passes the property by inheritance, or gains the experience of generations, being a descendant of another parent entity - parent. Thus, a single tree of generations is created, in which it is convenient to navigate and massively manage, unlike disparate libraries, a procedural method.
')
As you can see, everything is just like that of people! With the difference that the developer is the god of this system and can be transferred by generations, making changes in the root, or in individual branches of development. War and conflict we eliminate! New knowledge - add! Or on the contrary, we break everything ... It's hard to be a god! However, the principle of OOP obliges the developer to structure the application according to the rules, and not as it is necessary, which facilitates and systematizes its support, and that, however, does not interfere if you want to confuse the code even in this case ... :)

In my opinion, it is precisely such a “human” perception of principles that helps the development of the PLO. For example, as in life, there are strict parents who force children to be able to do anything that they themselves consider necessary! They are called Abstract classes - abstract. Remember how parents forced you to play the piano or teach poetry? .. So, the Abstract classes as well as many parents do not even know what this offspring child will need, and how he will use it, but we are sure that MUST! Those. such classes contain abstract methods, which are a declaration of a method without the implementation itself, like a candy wrapper without candy, thereby obliging the descendant to implement this method. As in life, where parents often lay their unrealized dreams on children ...

Here, in such a jokingly serious form, we touched on the topic of abstract classes and family relationships, as a way to understand ... both of them? .. But seriously, it goes without saying that there should be no random methods in programming, and any methods and properties are part of A well-thought class hierarchy, which as a family tree, can provide opportunities to extend functionality from generation to generation. And abstract classes, and even more abstract ones — interfaces (the interface — contains no implementations at all), help the programmer not to lose, not to forget to realize the common skills necessary for all descendants in life, without which the individual will die, and with it the application.

In addition to jokes, from the practical side, when designing application elements such as goods in a store, an object representation allows them to be brought closer to the properties of real objects, which, like in life, encapsulate (i.e. contain in themselves) all the necessary attributes: price, quantity , weight, shelf life and other necessary qualities. But since goods can be different, their class model can branch and develop, inheriting or redefining common properties.

Inheritance is perhaps the most important feature of OOP. If a new stage of evolution is required, the programmer creates a new class that expands the skills of his parent, and sometimes implements in a new way, i.e. overlapping parent methods - override. After all, each generation has its own concepts in life ... If a programmer needs “experience and concepts” of previous generations, he appeals to them. Nothing is lost in this structure, so it is extremely important to be able to use it.

And while there is not a full-fledged OOP specification for javascript, the ability to follow the principles of OOP is, and now we will use this convenience. Of course, in this article we will only touch on the basics of understanding, but as you know - the first step was the first step, the main thing is to catch on ...

So, our goal now is to write some managed entity, which will trigger the processes we need by timer. “Managed” - it means such an entity, or conventionally, a class will contain - as they say encapsulate, methods for management and properties containing the necessary data. Life example:
• properties are what the object knows (name, eye color, multiplication table),
• methods - this is what the object can (sleep, eat, build a synchrophasotron).

Important! If the reader does not yet know what the object in javascript is, then I recommend to read about it in any directory beforehand, otherwise there will be difficulties in understanding.
The creation of the object will occur through a function that is called with the new directive. It is the new directive that determines that this function is not ordinary at all, but a special function - the Constructor, which creates and returns an object. Before she returns it, we can assign to this object everything that the soul wishes: knowledge and skills.

function Timer() { /*    … */ }; var timer = new Timer(); 


So, we created a timer object of class Timer. But as creative people, we may want to give our object some properties when creating it, or not to give it ... Ie we want universality, so that if we wish we could set “knowledge and skills” or not, but the object would not die in agony without knowing how to breathe for example ... We are not animals, but how can we do it?
For this, the first thing we put in our class are the default properties that the object would accept from nature. And the processing unit.

 function Timer( options ) { //public var defaultOptions = { delay: 20 //     ,   -   ,stopFrame: 0 //   ,loop: true //    ,frameElementPrefixId: 'timer_' //   ID ,  -  } for(var option in defaultOptions) this[option] = options && options[option]!==undefined ? options[option] : defaultOptions[option]; /*   … */ }; 


Also in the function signature, we have the options parameter, this is an object. You know what an object is? .. After the comment
//public
we also have a defaultOptions object, which contains properties necessary for life, and a block of code behind it, which goes through all the defaultOptions properties by name, checks whether they are passed through options, and if not, sets the value from defaultOptions.
To assign a value to an object, we use this, which points to the current one created inside the function - the Constructor (remember new?). Thus, we can create our object like this:

 var timer = new Timer( {delay: 500, loop: false } ); 


... and the specified properties will be recorded, and the missing ones will be taken from defaultOptions. The object is saved! Versatility and flexibility obtained!

Pay attention to the comments //public
of course, it has here a conditional meaning (like all OOP conventions in javascript, including the notion of a class), but its essence is in the mark of Public properties, i.e. available from outside. These properties can be accessed directly through the object:
 alert( timer.delay ); 


As an example from life, these are obvious properties of an object that do not need concealment: the length of a cat's tail.

There are also Personal properties - private, and Protected - protected. To access Personal, if it is allowed at all, you need to use special methods in which the programmer determines what and how and to whom you can return. Protected - protected, it is a little less personal, because available for both the class itself and its heirs, in the “family” circle, so to speak. This is done for the stability of the application, because not all the rubbish is better to endure their huts ...

Let us and we add Personal properties - private, this is done by creating internal variables inside the function, so their scope is limited (or otherwise closed) by the function itself - “I won’t tell anyone if I don’t want it”! We'll talk more about closures ... In the meantime, insert further into the Timer function:

  //private var busy = false, //  ""  " !" currentTime = 0 //   ,frame = 1 //   ,task = {} // ! !         ,   ,keyFrames=[] //    , ..      ; /*   … */ 

to such properties, if we want, we will allow to access through public methods as:
 this.getKeyFrames = function( ) { return keyFrames; } 

Note that this method is public, because through this it is assigned to the property of the object, which can then be accessed through a point (let's not forget about the brackets at the end, if we call the action):

 timer.getKeyFrames(); 


if we need a private method, then it is similar to private variables, it is also created by a “normal” declaration of an internal function:

 function somePrivateMethod() { /* some code... */ } 


It turned out a service function that the Constructor can call for personal purposes. But from the object created by the constructor, this function will be unavailable, since is not his property.

Again, in javascript all these conventions that help to follow the principles of OOP, but do not always provide an exact implementation. For example, with the implementation of protected in javascript is very tight! The fact is that protected partially combines the properties and private- for inaccessibility from an object, and public- for access from other classes, which in javascript contradicts each other, as it is provided by the scope - the closure. Alternatively, you can create a public method and check inside it if the object that is calling it is a successor of the method owner, etc. There is no perfect solution, everything is within the framework of conventions.

Well, sort of, with the organization of access a bit sorted out. What about inheritance, because this is the most important quality in the PLO - the ability to learn and develop the skills and knowledge of their parents? And here we come to an important feature of javascript - prototype inheritance. This topic often causes difficulties of understanding, and then I will make my attempt to “explain everything once and for all” in a simple, human language.

Part 2. Prototypes in javascript.


So, in javascript there is a concept prototype, a hidden link [[prototype]] of an object, it is __proto__, and a property of the prototype function. To stop being confused in these concepts, let's sort them one by one:



Any self-respecting javascript object has a hidden [[prototype]] link that links it to the parent object, which in turn is with its own, etc. At the top of this whole chain is a built-in javascript object, a sort of supreme progenitor, object adam, which has all the necessary built-in methods, such as toString, valueOf, hasOwnProperty, etc. Because of this, all descendant objects also have this minimally necessary set of methods, which allows them to survive in an uneasy javascript environment.

flow_object2 - [[prototype]] -> flow_object1 - [[prototype]] -> ... {toString: ..., valueOf: ..., hasOwnProperty: ..., ...}

Those. even if you simply create an empty var obj = {} object that does not have methods and properties and refer to the standard method, then by the [[prototype]] reference chain (in this case the minimum short chain), it will take this method from the built-in javascript object:

 var obj = {}; //  obj.toString(); //      "[object Object]" 


This is how prototype inheritance in javascript is implemented, via the [[prototype]] link chain. All properties available through the prototype chain will be discovered by descendants, as a storehouse of knowledge and skills, which allows you to build a real evolving class tree!

Note that the prototype properties of each object are not stored in it, but as if in an intermediate link in the chain of prototypes, between the current object and the embedded javascript object "at the beginning of time". This root object requires respect, and disturbing its peace is not quite decent, so to create your own prototypical properties, it is better to create and embed your own objects in the chain with the [[prototype]] link.

But, as we remember, [[prototype]] is a closed link, how can we build our chain without access to it? Here we are helped by the familiar constructor function, with the new keyword, and the prototype property, which is quite open to itself. The fact is that an object created through the Constructor receives a [[prototype]] reference with the value specified in the prototype property of this Constructor! Initially, any function has in its prototype property a reference to an almost empty object (with a single constructor property pointing back to the function itself),

self_function.prototype -> {constructor: -> self_function}

but we can replace the prototype property by passing our parent class. Those. creating a Constructor function, we simply assign a reference to the object with the properties we need in its prototype property, and the new object being created will receive a [[prototype]] reference to this object with the properties we need.

 //   "" var Foo = function() {}; //  prototype      Foo.prototype = { hi: 'Hello!', sayHi: function(){ alert( this.hi ) } }; //   "" var obj = new Foo(); //  ,    obj.sayHi(); 


In the example above, the hidden reference [[prototype]] of obj received a pointer to an object having the property hi and the method sayHi. Thus the object obj inherits this knowledge and skill.
To simplify this procedure, a function is invented.

 function extend(Child, Parent) { var F = function() { } F.prototype = Parent.prototype Child.prototype = new F() Child.prototype.constructor = Child Child.superclass = Parent.prototype } 


See javascript.ru for an example of its use.
javascript.ru/tutorial/object/inheritance#svoystvo-prototype-i-prototip

It takes in the arguments two constructor functions, Descendant and Parent, and does what we have already mentioned:


It may be asked why we need the first three lines, why not immediately make the assignment to Child.prototype = Parent.prototype, without any new F (), and that’s the end ?!

The fact is that with such an assignment a new intermediate link in the chain of inheritance will not be created! In Child.prototype, Parent.prototype is recorded, not an intermediate storage object with further reference to Parent.prototype, and when we try to write anything in Child.prototype, we will rush into Parent territory, violating respect for elders and succession of generations. By calling the constructor new F (), we create our own area for storing prototype knowledge for Child, which it can pass on to its descendants.

It is possible to add separate properties to the prototype of the Constructor as follows:

 Child.prototype.someProperty = "someProperty"; 


And by the way, do not try to access prototype as a property of an object - an instance of a class.
The object has no prototype property, there is a hidden link [[prototype]], and the prototype property is not!
Of course, it can be created, but there is no sense in it from inheritance. The sense is only from the prototype property of the Constructor function, due to its ability to pass a pointer to the [[prototype]] reference of the object being created.

That's all about prototype inheritance. Really simple?
But prototype inheritance is not the only possible scheme. I also want to mention the method of calling the superclass constructor, i.e. parent class, it’s not for nothing that we took care of its entry in the properties of the prototype (see function extend).

In the Timer constructor of our forgotten example, we assign some properties to the object through this. To pass these properties to subsequent generations, it is necessary to make a call to the parent constructor in the context of the descendant, ie:

 function TimerPlayer() { TimerPlayer.superclass.constructor.apply( this, arguments ); } 


Here it is important to remember that you cannot call through this.superclass.constructor.apply, namely through the name of the current constructor (here TimerPlayer), because otherwise, if the parent constructor also uses this, it calls this.superclass.constructor.apply (this, arguments), this will turn into a closed call to apply in the context of this as a descendant, which will cause an error.

Calling the parent constructor in the context of a descendant will create and assign all its properties and methods to the descendant. Moreover, the private properties of the parent, declared through var, and not through this, can be accessed only if there are parent public methods that allow them to be read.
This is the way we continue to build our Timer.

Part 3. Javascript class Timer and its legacy.


So, we already have a class, something knowing, but not knowing how, so it's time to add skills to it! What do we want to teach our class? Let's do something like a player:
• start
• pause
• stop
• rewind
• setToFrame
And some less important methods. Imagine that we have already written them ... So, we insert further into the Timer function:

 this.start = function(){ /*  */ if( busy ) return; if( window.console ) console.log ('start: .currentTime='+currentTime+'; frame='+frame); busy = true; timer.call( this ); } this.pause = function() { /*  */ if( window.console ) console.log ('pause: currentTime='+currentTime+'; frame='+frame); clearInterval( this.intervalId ); busy = false; } this.stop = function() { /*  */ if( window.console ) console.log ('stop: currentTime='+currentTime+'; frame='+frame); clearInterval( this.intervalId ); busy = false; currentTime = 0; frame = 1; this.clearFrameLine(); } /* highlighting -   */ this.clearFrameLine = function() { /*    */ for(var i=1, str=''; i<this.stopFrame+1; i++) if( elFr = document.getElementById( this.frameElementPrefixId+i ) ) removeClass( elFr, 'active'); } this.setActiveFrameElement = function( frameNumber ){ /*    */ if( elFr = document.getElementById( this.frameElementPrefixId+frameNumber ) ) addClass(elFr, 'active'); } this.toString = function() { /*  ,   alert(),    */ var str = ''; for(var option in this ) str+= option+': '+( (typeof this[option]=='function') ? 'function' : this[option] )+'\n'; return '{\n'+str+'}'; } this.setTask = function( new_task ) { /*   ,       */ task = new_task; this.stopFrame = 0; keyFrames.length = 0; for(var frInd in task) { if( (+this.stopFrame)< (+frInd) ) this.stopFrame=(+frInd); keyFrames.push( +frInd ); } } this.getKeyFrames = function( ) { /*    keyFrames */ return keyFrames; } this.getTask = function() { /*    task */ return task; } this.setToFrame = function( toFrame ) { /*     */ if(toFrame>this.stopFrame) return; frame=toFrame; currentTime=(frame-1)*this.delay; for(var frInd in task) { if( (+frInd)>(+toFrame) ) break; var taskList = task[ frInd ]; for(var i=0; i<taskList.length; i++ ){ var taskItem; if( taskItem = taskList[i] )taskItem.run(); } } this.clearFrameLine(); this.setActiveFrameElement( toFrame ); } this.rewind = function( amount ) { /* !  !      :))))))))) */ if( amount<0 && this.intervalId ) amount--;/*    setInterval */ var toFrame = frame+amount; toFrame = Math.max( Math.min( toFrame, this.stopFrame), 1); this.setToFrame(toFrame); } function timer(){ /*  ,  setInterval      */ var this_ = this; /*         */ this.intervalId = setInterval( function() { /*    setInterval    this.delay */ //console.log ('currentTime='+currentTime+'; frame='+frame+';'+task); if( task[ frame ] ) { /*       ,  ... */ var taskList = task[ frame ] /* ...   -   */ for(var i=0; i<taskList.length; i++ ){ /*     -    - run... */ var taskItem; if( taskItem = taskList[i] ) taskItem.run(); /* ...     */ } } /* highlighting */ this_.setActiveFrameElement( frame ); /*   */ currentTime+=this_.delay; /*      */ frame++; if( this_.stopFrame && frame>this_.stopFrame ) { /*  stopFrame       ... */ if( this_.loop ) this_.setToFrame( 1 ); /*    - ,    ,   ,  , */ else this_.stop(); /*   ! */ } }, this.delay ); } 


All done with the designer.
In general, I think everything is clear: do you need a start method? Writing a public start method! Where…

 this.start = function(){ if( busy ) return; /*    ,  ! */ /*   ,     */ if( window.console ) console.log ('start: .currentTime='+currentTime+'; frame='+frame); busy = true; /*  ,   */ timer.call( this ); /*    */ } 


I commented on the timer function in detail. In general, the idea is simple:

 function timer(){ var this_ = this; this.intervalId = setInterval(function() { /*    ,  this_   this! */ }, this.delay ); } 


First, we save the reference to the context of our object into a variable, since inside the function called in setInterval, the context will be lost, and the variable will remain in the closure, that is, in the local scope. Perhaps for understanding, you should repeat (or find out) about closures, and we'll talk about them below ... Next, we assign the intervalId property to our object, which is returned by the setInterval method, this identifier will allow us to stop the execution of setInterval during pause or stop, see these methods.

Separate analysis requires task property, because it is there that we in some form store tasks for execution. Its structure is as follows:

 { 1:[ { run: function(){} } ], 5:[ {},{},{} /*...*/ ], /*...*/ } 


Object of arrays of objects. Oh, it would be better not to say, but then he himself was confused ...
But everything is simple, in the task object under the necessary frame number there is an array of tasks-objects with the run property. This property must be assigned a function, which will be called when the desired frame. If necessary, each task object, you can add another property, and then he and the object.

Also, if necessary, you can add a new object to the array using the standard methods of the arrays push, unshift, splice.
Well, of course, the task object itself can be assigned a property by the desired frame number!

Thus, filling in the task and assigning our class to the setTask method, we determine what and when to do it. How can this be used? Perform various dynamic scenarios on the website or on the client off-line, create animations, create “live” tutorials or time-bound tests, remind of important events (the kettle has boiled!), Get users pop-up advertisements (nasty and nasty joke!). Or just display the watch in the corner of the page!

Moreover, we have already organized the simplest interface, a kind of small API to manage our timer and visualization, and now we use it by building a control panel similar to the player! We paste into the html page the code, beloved and dear:

 <button onclick="timer.rewind(-50);">rewind -50</button> <button onclick="timer.start();">start</button> <button onclick="timer.pause();">pause</button> <button onclick="timer.stop();">stop</button> <button onclick="timer.rewind(+50);">rewind +50</button> 


on the onclick event, buttons of the timer object are hung on the buttons. Of course, before calling which object should not forget to create. Remember how? - through the constructor function:
 var timer = new Timer(); 


However, it would not be bad now to create a script, what to manage, and otherwise - what did you fight for? ..
Let's try to create a simple animation, we will move the image on the page, well, a kind of “hello world” in the animation world. We will move the picture
 <img id="ball" src="http://www.smayli.ru/data/smiles/transporta-854.gif" > 



Let me remind you that the task of our timer to cause an action, what action we want, while he himself is not responsible for this action. Therefore, WHAT exactly to do is our task, and we will solve it now by writing a simple function of moving an element, by which the element itself is transmitted by ID and its two coordinates:

 function moveElem( elem, top, left ){ elem.style.cssText = 'position:relative;top:'+top+'px;left:'+left+'px'; } 


So, now this function needs to be assigned to the run property of the task object in arrays with the numbers of the necessary frames of the task object, watch your thought? So, we create a script object, and define its first frame as an array, in this frame we put the initial position of the image element:

 var frames = {}; /*   */ frames[1]=[]; frames[1].push( { run: function(){ moveElem( ball, 600, 600 ); } } ); 


Why it is impossible to write run: moveElem (ball, 600, 600)? This is wrong, because the syntax ...
moveElem ();
... means calling a function, but we don’t need to call it here and now, but rather we need to put it in the body of the property of the run function that will make the call. Otherwise, we would have crammed the result of moveElem () - undefined into run, because it returns nothing, and we would have torn our picture for nothing.

And voila! To the first frame we added an action that places our picture (balloon) in a certain lower position of the page. Now, to begin to rise, we need to change this state frame by frame, i.e. reduce the top coordinate, well, the left - adjusted for the wind. :) To fill the necessary frames use the cycle. If you wish, by the way, you can write your own Timer class method - which would add frames, and actions, and distribute the changing parameters of actions across frames ... In the meantime, for example, fill the loop with frames 2 through 600.

 /*  */ for(var i=2; i<601; i++) frames[i] = [ { run: function(i){ return function(){ moveElem( ball, 600-i, 600-i ); } }(i) } ]; 

Here we must also pay attention to the use of closures. The fact is that for the transfer of dynamic i, wrapping in a functional expression is used here, which is called on the spot:

 function(i){ /*    i    */ }(i) ; 


If we just used:

 run: function(){ moveElem( ball, 600-i, 600-i ); } 


Then, when calling moveElem () i would be taken from the global scope, i.e. the one that is declared and worked out in the for loop (var i = 2; i <601; i ++), i.e. a “waste” variable i equal to 600 would be called, and not at all a dynamic i, which should gradually increase, changing the coordinates of the ball soaring.
Therefore, we use the CLOSING on javascript, which defines the scope, i.e. when performing a function

 function(i){ /*    i    */ }(i) ; 


... inside it, its own variable i was created as an argument to this function itself, and the call is in place (repeat if forgotten), performs the function body, where the return (return) of our function already takes place. And again:
not

 return moveElem( ball, 600-i, 600-i ); 


but

 return function(){ moveElem( ball, 600-i, 600-i ); } 


because, in the first case, it is not the function call that returns, but the result of calling moveElem (ball, 600-i, 600-i), which will be executed right there!

And now we have a script. Now you can assign it and run it:

 var timer = new Timer( ); timer.setTask( frames ); timer.start(); /*   start */ 


In general, frame-by-frame management provides ample opportunities for creating interesting and complex scenarios.
On the demo page denis-or-love.narod.ru/portf/timer, I also implemented an example of a time-frame line like TimeLine in Adobe Flash :) - the drawFrameLine button.

It should be noted that a large number of elements on the page can greatly slow down the browser when redrawing their positions, this is clearly seen in the visual IE, if you click drawFrameLine with parameter 1 - each frame.
If desired, you can write a whole interface for creating scripts, with expandable features and other amenities. Here, as they say, who is in that much ...

image

And now, let's do the Inheritance !
Having built the base class Timer, we will expand it by creating a more advanced class TimerPlayer. We will limit the example of advancement to a simple example - our child class, taking the skills of the parent, will be able to create a control panel of our timer, like a player. To do this, we do three things:
  1. call parent constructor
  2. adding new methods
  3. passing inheritance through the extend function


 //  function TimerPlayer( options ) { //    TimerPlayer.superclass.constructor.apply(this, arguments); //   this.drawPanel = function( panelId, objName ) { var objName = objName || 'timer'; var template ='<button onclick="'+objName+'.rewind(-50);">rewind -50</button>'+ '<button onclick="'+objName+'.start();">start</button>'+ '<button onclick="'+objName+'.pause();">pause</button>'+ '<button onclick="'+objName+'.stop();">stop</button>'+ '<button onclick="'+objName+'.rewind(+50);">rewind +50</button>'; document.getElementById( panelId ).innerHTML = template; } } // extend extend(TimerPlayer, Timer); 


And although here we do not have prototype inheritance, extend we need to build a chain for the future (and suddenly we want to add prototype properties to the parent ...), and to write the superclass and constructor. The new drawPanel method takes the string id of the element inside which to place the buttons, and the string the name of the object to substitute into the HTML template.

 //   var timerPlayer = new TimerPlayer(); timerPlayer.setTask( frames2 ); timerPlayer.drawPanel( 'controlPanel', 'timerPlayer' ); 


So we finished, and maybe we just started our Timer class and its descendant.
I want to thank devote for the patient advice and assistance in writing the article.
Until new meetings and pleasant programming.

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


All Articles