Pull to refresh

Javascript: ООП, прототипы, замыкания, «класс» Timer.js

Reading time 19 min
Views 96K
Здравствуйте программисты начинающие, законченные, а также все сочувствующие. Как известно, ничто не познается так хорошо, как на собственном опыте. Главное, чтобы опыт был полезный. И в продолжении этой простой мысли я хочу предложить заняться несколькими полезными делами сразу:
  • Побеседовать на тему «ООП с человеческим лицом».
  • Разобраться с прототипами в javascript, коротко и сердито!
  • Вспомнить, что «замыкание» это не только ценный мех… удар током.
  • Написать на javascript класс Timer — этакий планировщик событий для запуска анимаций, событий, любых функций.
  • Весело провести время!

Предупреждение! Если вы не ждете от статьи ничего веселого… то ошибаетесь. Людям с пониженным чувством юмора читать… еще более рекомендуется! Ну-с, приступим…

Замечу, что на момент написания статьи ( привет апокалиптично-гламурный 2012! )настоящего ООП в javascript нет, но есть приемы, которыми можно реализовать основные принципы объектно ориентированного программирования. Для тех, кто только открывает для себя эту безумно интересную тему, я своими словами поясню характерность данного метода.

Часть 1. ООП с человеческим лицом.


Кратенько: ООП это вовсе не священная мантра, а по сути, просто методика организации приложений, структурирования кода, централизации методов и объединения сущностей в единое иерархическое семейство. Подобно тому, как строились субмарины и самолеты, перенимая опыт плавучести и летучести из живой природы, ООП приложения также используют восприятие программных сущностей, как неких «живых» объектов, перенимая известные нам из реального(offline — помните о таком?) мира характеристики и свойства.

Другими словами, создается некая сущность, которая не только имеет свои свойства и методы, но умеет порождать потомков и эволюционировать! Это называется расширением – extending. Словно бережный родитель, объект передает имущество по наследству, либо получает опыт поколений, будучи потомком другой родительской сущности – parent. Таким образом, создается единое древо поколений, в котором удобно ориентироваться и массово управлять в отличие от разрозненных библиотек — процедурный метод.

Как видите, — все как у людей! С той разницей, что разработчик являет собой бога этой системы и может переноситься по поколениям, внося изменения в самом корне, либо в отдельных ветвях развития. Войны и конфликты устраняем! Новые знания — добавляем! Или наоборот, все ломаем… Трудно быть богом! Однако сам принцип ООП обязывает разработчика структурировать приложение по правилам, а не как приспичит, что облегчает и систематизирует его поддержку, и что, впрочем, вовсе не мешает при желании запутать код даже в этом случае… :)

На мой взгляд, освоению ООП очень помогает именно такое «человеческое» восприятие принципов. Например, как и в жизни существуют строгие родители, которые заставляют детей уметь что-либо, что сами считают нужным! Их называют Абстрактные классы — abstract. Помните, как родители заставляли вас играть на фортепиано или учить стихи?.. Так вот, Абстрактные классы также как и многие родители вовсе и знать не знают зачем ребенку-потомку это будет нужно, и как он это будет использовать, но уверены, что так НАДО! Т.е. такие классы содержат абстрактные методы, которые являют собой объявление метода без самой реализации, как фантик без конфетки, тем самым обязывая потомка, этот метод реализовать. Как и в жизни, где родители нередко перекладывают на детей свои нереализованные мечты…

Вот в такой шутливо-серьезной форме, мы затронули тему абстрактных классов и семейных отношений, как способ понять… и то и другое?.. А если серьезно, то разумеется, в программировании не должно быть случайных методов, и любые методы и свойства являются частью продуманной иерархии классов, которая как генеалогическое дерево, может давать возможности расширять функционал от поколения к поколению. А абстрактные классы, и еще более абстрактные – интерфейсы ( interface — вообще не содержит реализаций ), помогают программисту не потерять, не забыть реализовать общие необходимые для всех потомков умения в жизни, без которых особь умрет, а с ней и приложение.

Кроме шуток, с практической стороны, при проектировании элементов приложения таких как например товары в магазине, объектное представление позволяет приблизить их к свойствам реальных объектов, которые как и в жизни инкапсулируют (т.е. содержат в себе) все необходимые аттрибуты: цену, количество, массу, срок хранения и прочие необходимые качества. Но так как и товары могут быть разными, то их классовая модель может ветвиться и развиваться, наследуя или переопределяя общие свойства.

Наследование — пожалуй, важнейшая особенность ООП. Если требуется новый виток эволюции, программист создает новый класс, расширяющий умения его родителя, а иногда и реализующий по новому, т.е. перекрывающий методы родителя — override. Ведь у каждого поколения свои понятия в жизни… Если же программисту нужен «опыт и понятия» прежних поколений — он обращается к ним. Ничто не потеряно в этой структуре, поэтому крайне важно уметь ею пользоваться.

И хотя пока для javascript нет полноценной ООП спецификации, возможность следовать принципам ООП есть, и этим удобством мы сейчас будем пользоваться. Конечно, в рамках данной статьи мы лишь коснемся основ понимания, но как известно – лиха беда начало, главное зацепиться…

Итак, наша цель сейчас написать некую управляемую сущность, которая по таймеру будет запускать нужные нам процессы. «Управляемую» – это значит такая сущность, или условно класс, будет заключать в себе – как говорят инкапсулировать, методы для управления и свойства, содержащие необходимые данные. Пример из жизни:
• свойства – это то, что объект знает( имя, цвет глаз, таблица умножения ),
• методы – это то, что объект умеет( спать, есть, строить синхрофазотрон ).

Важно! Если читатель еще не знает, что из себя представляет объект в javascript, — то рекомендую предварительно почитать об этом в любом справочнике, иначе будут возникать трудности понимания.
Создание объекта будет происходить через функцию, которая вызывается с директивой new. Именно директива new определяет, что функция эта не обычная вовсе, а специальная функция – Конструктор, которая создает и возвращает некий объект. До того же как она его возвратит, мы можем этому объекту присвоить все что душа пожелает: и знания и умения.

function Timer() {
	/* тут будем кодить дальше…  */
};
var timer = new Timer();


Итак, мы создали объект timer класса Timer. Но как творческие люди, мы можем захотеть уже при создании наделить свой объект некоторыми свойствами, или не наделять… Т.е. мы хотим универсальности, чтобы при желании можно было задать «знания и умения», или нет, но объект при этом не умер бы в муках, не умея например дышать… Мы же не звери, но как это сделать?
Для этого, первое, что мы поместим в нашем классе, — свойства по умолчанию, которые объект принимал бы от природы. И блок обработки.

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];
	
	/*тут будем кодить дальше…  */
};


Также в сигнатуре функции у нас появился параметр options, это объект. Вы ведь знаете что такое объект?.. После комментария
//public
у нас тоже объект defaultOptions, который содержит необходимые для жизнедеятельности свойства, и за ним блок кода, который перебирая все свойства defaultOptions по именам, проверяет переданы ли они через options, и если нет — ставит значение из defaultOptions.
Для присваивания объекту значения мы пользуемся this, который указывает на текущий, созданный внутри функции — Конструктора( помните про new ?). Таким образом, мы можем создать наш объект так:

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


… и указанные свойства будут записаны, а пропущенные — взяты из defaultOptions. Объект спасен! Универсальность и гибкость получена!

Обратим внимание и на комментарии //public
разумеется, он имеет тут условное значение (как и все ООП условности в javascript, включая понятие класс), но суть его в пометке Публичных свойств, т.е. доступных из вне. К таким свойствам можно обратиться напрямую через объект:
alert( timer.delay );


Как пример из жизни, это очевидные свойства объекта, которые не нуждаются в сокрытии: длина хвоста у кота.

Бывают также Личные свойства — private, и Защищенные — protected. Для доступа к Личным, если это позволительно вообще, нужно использовать специальные методы, в которых программист определяет, что и как и кому можно возвращать. Защищенные — protected, это чуть менее личные, т.к. доступны и для самого класса и для его наследников, в кругу «семьи» так сказать. Делается это для стабильности приложения, ведь не весь сор лучше выносить их избы…

Давайте и мы добавим Личные свойства — private, делается это созданием внутренних переменных внутри функции, таким образом, область их видимости ограничивается (или иначе — замыкается) самой функцией, — «никому не скажу, если не захочу»! О замыканиях мы еще поговорим… А пока, вставляем дальше в функцию Timer:

	//private
	var busy = false, // флаг "занят" или "процесс идет!"
	currentTime = 0 // текущее время
	,frame = 1	// текущий кадр
	,task = {}	// о! важно! в этот объект будем помещать список задач для выполнения, по кадрам
	,keyFrames=[]	// массив индексов ключевых кадров, т.е. тех в которые помещены задачи
	;
	/*тут будем кодить дальше…  */

к таким свойствам, если захотим, мы позволим обращаться через публичные методы как:
	
	this.getKeyFrames = function( ) {
		return keyFrames;
	}

Обратим внимание, что этот метод именно публичный, т.к. через this присвоен свойству объекта, к которому можно потом обратится через точку (не забудем про скобки на конце, если вызываем именно действие ):

timer.getKeyFrames();


если же нам нужен приватный метод, то он подобно приватным переменным, также создается «обычным» объявлением внутренней функции:

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


получилась служебная функция, которую Конструктор может вызвать для личных целей. Но из созданного конструктором объекта эта функция будет недоступна, т.к. не является его свойством.

Повторюсь, в javascript все это условности, которые помогают следовать принципам ООП, но не всегда обеспечивают точную реализацию. Например, с реализацией protected в javascript совсем туго! Дело в том, что protected частично сочетает в себе свойства и private- для недоступности из объекта, и public- для доступа из других классов, что в javascript противоречит друг другу, так как обеспечивается областью видимости — замыканием. Как вариант, можно создать public метод и внутри него проверять, является ли вызывающий его объект наследником хозяина метода и т.п. Идеального решения тут нет, все в рамках условностей.

Ну вроде, с организацией доступа чуток разобрались. А как насчет наследования, ведь это наиболее важное качество в ООП — способность перенимать и развивать умения и знания своих родителей? И тут мы подходим к важной особенности javascript — прототипному наследованию. Эта тема часто вызывает трудности понимания, и далее я сделаю свою попытку «объяснить все раз и навсегда» простым, человечеким языком.

Часть 2. Прототипы в javascript.


Итак, в javascript существует понятие прототип, скрытая ссылка [[prototype]] объекта, она же __proto__, и свойство prototype функции. Чтобы перестать путаться в этих понятиях, разберем их по одному:

  • прототипом текущего объекта называют некий другой объект, из которого текущий объект черпает недостающие методы и свойства.
  • скрытая ссылка [[prototype]] указывает на прототип текущего объекта, и по спецификации недоступна для разработки. Но в некоторых браузерах, нарущающих спецификацию, она открыта как свойство объекта __proto__, что впрочем не дает смысла ей пользоваться на прямую. т.к. это не кроссбраузерно и не корректно.
  • prototype — это свойство функции! повторяю, именно функции, которое используют для передачи значения ссылке [[prototype]] создаваемого объекта.


Любой, уважающий себя, javascript-объект имеет скрытую ссылку [[prototype]], которая связывает его с родительским по замыслу объектом, который в свою очередь со своим и т.д. Наверху всей этой цепочки заседает встроенный объект javascript, этакий верховный прародитель, объектный адам, имеющий все необходимые встроенные методы, такие как toString, valueOf, hasOwnProperty и т.д. Благодаря этому, все объекты потомки тоже имеют этот минимально необходимый набор методов, позволяющий выжить в непростой среде javascript.

объект_потомок2--[[prototype]]--> объект_потомок1--[[prototype]]-->… { toString: ..., valueOf: ..., hasOwnProperty: ...,… }

Т.е. даже если просто создать пустой объект var obj = {}, не имеющий методов и свойств, и обратиться к стандарному методу, то по цепочке ссылок [[prototype]] ( в данном случае минимально короткой цепочке ) он возьмет этот метод из встроенный объекта javascript:

var obj = {}; //пустой объект
obj.toString(); // вызываем метод и получаем строку "[object Object]"


Так реализуется прототипное наследование в javascript, — через цепочку ссылок [[prototype]]. Все свойства, доступные по цепочке прототипов будут открыты потомкам, как кладезь знаний и умений, что позволяет выстраивать настоящее эволюционирующее древо классов!

Обратим внимание, что прототипные свойства каждого объекта хранятся не в нем самом, а как бы в промежуточном звене цепочки прототипов, между текущим объектом и встроенным объектом javascript «в начале времен». Этот корневой объект требует уважения, и нарушать его покой не совсем прилично, поэтому для создания собственных прототипных свойств лучше создавать и встраивать в цепочку собственные же объекты с ссылкой [[prototype]].

Но, как мы помним, [[prototype]] — ссылка закрытая, как же нам выстроить свою цепочку, не имея к ней доступа? Тут нам помогает уже знакомая функция-Конструктор, с ключевым словом new, и свойство prototype, которое вполне себе открытое. Дело в том, что объект, создаваемый через Конструктор, получает ссылку [[prototype]] со значением, указанным в свойстве prototype этого Конструктора! Изначально, любая функция имеет в своем свойстве prototype ссылку на почти пустой объект(с единственным свойством constructor указывающим обратно на саму функцию),

сама_функция.prototype --> { constructor: --> сама_функция }

но мы можем заменить свойство prototype, передавая свой родительский класс. Т.е. создавая функцию-Конструктор, мы просто присвоим в ее свойство prototype ссылку на объект с нужными нам свойствами, и новый создаваемый объект получит ссылку [[prototype]] на этот объект с нужными нам свойствами.

// функция Конструктор "класса"
var Foo = function() {};
//передаем свойству prototype объект со свойством и методом
Foo.prototype = { hi: 'Hello!',  sayHi: function(){ alert( this.hi ) } }; 
// создаем экземпляр "класса"
var obj = new Foo(); 
// вызываем метод, наследованный из прототипа
obj.sayHi();


В примере выше скрытая ссылка [[prototype]] объекта obj получала указатель на некий объект имеющий свойство hi и метод sayHi. Таким образом объект obj наследовал это знание и умение.
Для упрощения этой процедуры придумана функция

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


Пример ее использования смотрите на javascript.ru
javascript.ru/tutorial/object/inheritance#svoystvo-prototype-i-prototip

Она принимает в агрументах две функции-Конструктора — Потомка и Родителя, и делает то, что мы уже затронули:
  • создает служебную функцию, для передачи прототипа
  • записывает в ее prototype-свойство prototype функции Родителя
  • передает свойству prototype Потомка промежуточный объект, новое звено цепочки, с ссылкой [[prototype]] на prototype Родителя
  • записывает в constructor функцию Child( вместо исходного конструктора служебной F )
  • записывает в свойство superclass ссылку на Родителя, на случай возможности обращения к его конструктору и другим исходным методам, если они будут переопределены в потомке.


Может возникнуть вопрос, зачем нужны первые три строки, почему бы сразу не сделать присвоение Child.prototype = Parent.prototype, безо всякого new F(), и дело с концом?!

Дело в том, что при таком присвоении не будет создано новое промежуточное звено в цепочке наследования! В Child.prototype запишется Parent.prototype, а не промежуточный объект -хранилище с дальнейшей ссылкой на Parent.prototype, и при попытке записать что либо в Child.prototype мы грубо ворвемся на территорию Parent, нарушая уважение к старшим и приемственность поколений. Вызывая конструктор new F(), мы создаем для Child свою собственную область хранения прототипных знаний, которые он сможет передать потомкам.

Добавлять отдельные свойства в прототип Конструктора можно еще и так:

Child.prototype.someProperty = "someProperty";


И кстати, не нужно пытаться обращаться к prototype как к свойству объекта — экземпляра класса.
У объекта нет свойства prototype, есть скрытая ссылка [[prototype]], а свойства prototype — нет!
Его конечно можно создать, но толку от него в наследовании никакого. Толк есть только от свойства prototype функции-Конструктора, благодаря ее способности передавать указатель в ссылку [[prototype]] создаваемого объекта.

Вот и все, что касается прототипного наследования. Правда просто?
Но прототипное наследование не единственно возможная схема. Хочу упомянуть также и метод вызова конструктора суперкласса, т.е. класса родителя, не даром же мы позаботились о его записи в свойства прототипа ( см. function extend ).

В конструкторе Timer нашего забытого примера, мы присваиваем объекту некоторые свойства через this. Чтобы передать эти свойства последующим поколениям, надо в конструкторе потомка сделать вызов родительского конструктора в контексте потомка т.е.:

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


Здесь важно помнить, что нельзя вызывать через this.superclass.constructor.apply, а именно через имя текущего конструктора( тут TimerPlayer), потому что иначе, если родительский конструктор тоже использует this, и вызывает this.superclass.constructor.apply(this, arguments), то это превратится в замкнутый вызов apply в контексте this как потомка, что вызовет ошибку.

Вызов родительского конструктора в контексте потомка создаст и присвоит потомку все его свойства и методы. Причем приватные свойства родителя, объявленные через var, а не через this, могут быть доступны только при наличии позволяющих их прочитать родительских публичных методов.
Именно этим путем мы и продолжаем строить наш Timer.

Часть 3. Javascript-класс Timer и его наследие.


Итак, у нас уже есть класс, что-то знающий, но ничего не умеющий, так что пора добавить ему умений! Чему мы желаем научить наш класс? Сделаем что-то вроде плеера:
• start
• pause
• stop
• rewind
• setToFrame
И некоторые менее важные методы. Представим, что мы их уже написали… Итак, вставляем дальше в функцию Timer:

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
		);
		
	}


Все с конструктором покончили.
В целом, думаю, все понятно: нужен метод старт? Пишем публичный метод старт! Где…

this.start = function(){ 
		if( busy ) return; /* выходим если уже стартовали, флаг стоит! */
			/* это для отладки, вывод информации в консоль */
			if( window.console ) console.log ('start: .currentTime='+currentTime+'; frame='+frame);
		busy = true; /* ставим флаг, что стартуем */
		timer.call( this ); /* вызываем приватный метод */
	}


Саму функцию timer я подробно прокомментировал. В целом идея простая:

function timer(){ 
	var this_  = this; 
	this.intervalId = setInterval(function() { /* тут все и делаем, используем this_ а не this! */ }, this.delay );
}


Сперва, сохраняем ссылку на контекст нашего объекта в переменную, т.к. внутри функции вызываемой в setInterval контекст будет потерян, а переменная останется в замыкании, т.е в локальной области видимости. Возможно для понимания, следует повторить(или узнать) про замыкания, а мы о них еще поговорим ниже… Далее присваиваем нашему объекту свойство intervalId, которое возвращается методом setInterval, этот идентификатор позволит нам останавливать выполнение setInterval при паузе или стоп, смотрите эти методы.

Отдельного разбора требует свойство task, ведь именно там мы в некоем виде храним задачи для выполнения. Структура его такая:

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


Объект массивов объектов. Ой, лучше бы не говорил, а то сам запутался…
Но все просто, в объекте task под нужным номером кадра содержится массив заданий-объектов со свойством run. Этому свойству надо присвоить функцию, которая и вызовется при нужном кадре. При необходимости каждому заданию-объекту, можно добавить еще свойство, на то он и объект.

Также, по надобности, можно в массив добавлять новый объект-задание, пользуясь стандартными методами массивов push, unshift, splice.
Ну и разумеется самому объекту task можно присваивать свойство по номеру нужного кадра!

Таким образом, заполняя task и присваивая нашему классу методом setTask, мы определяем, что и когда ему делать. Как это можно использовать? Выполнять различные динамические сценарии на сайте или на клиенте оffline, создавать анимацию, создавать «живые» учебные пособия или тесты завязанные на времени, напоминать о важных событиях(чайник вкипел!), доставать пользователей всплывающей рекламой( мерзкая и гадкая шутка!). Или просто выводить часики в углу страницы!

Более того, у нас уже организовался простейший интерфейс, некое маленькое API для управления нашим таймером и визуализацией, и сейчас мы его используем, построив панель управления на подобие плеера! Вставляем на страницу html код, любимый и родной:

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


на событие onclick кнопочкам повешены методы объекта timer. Разумеется, перед вызовом которых объект следует не забыть создать. Помните как? — через функцию конструктор:
var timer = new Timer();


Однако, не плохо бы теперь и сценарий создать, чем управлять, а иначе — за что боролись?..
Попробуем создать простую анимацию, будем перемещать картинку по странице, ну своеобразный «hello world» в мире анимации. Перемещать будем картинку
<img id="ball" src="http://www.smayli.ru/data/smiles/transporta-854.gif" >



Напомню, что задача нашего таймера вызывать действие, какое нам угодно действие, при этом сам он за это действие не в ответе. Поэтому ЧТО именно делать — это наша задача, и мы ее сейчас решим, написав простенькую функцию перемещения элемента, которой передается сам элемент по ID и две его координаты:

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


Итак, теперь эту функцию нужно присвоить свойству run объекта-задания в массивах под номерами нужных кадров объекта task, следите за мыслью? Итак, создаем объект-сценарий, и первый его кадр определяем как массив, в этот кадр мы положим начальное положение элемента-картинки:

var frames = {};
/* начальное положение */
frames[1]=[];
frames[1].push( 
	{ 
		run: function(){ 
			moveElem( ball, 600, 600 );
		}	
	} 
);


Почему нельзя написать run: moveElem( ball, 600, 600 )? Это неправильно, потому что синтаксис…
moveElem();
… означает вызов функции, а нам ее не надо вызывать тут и сейчас, а надо поместить в тело свойства-функции run, которая вызов и сделает. А иначе мы бы в run запихали результат выполненной moveElem() — undefined, поскольку она ничего не возвращает, и картинку нашу почем зря дернули бы.

И вуаля! Первому кадру мы добавили действие, которое помещает нашу картинку(воздушный шарик) в некую нижнюю позицию страницы. Теперь, чтобы начать подниматься, нам нужно покадрово изменять это состояние, т.е. уменьшать координату top, ну и left — с поправкой на ветер. :) Для заполнения нужных кадров используем цикл. При желании, кстати, можно написать собственный метод класса Timer — который бы добавлял кадры, и действия, и распределял бы изменяющиеся параметры действий по кадрам… А пока, для примера, заполним циклом кадры со 2 по 600-й.

/* действие */
for(var i=2; i<601; i++) frames[i] = [ 
	{ 
		run: function(i){ 
			return function(){
				moveElem( ball, 600-i, 600-i );
			} 
		}(i)  
	} 
];

Здесь надо также обратить внимание на использование замыканий. Дело в том, что для передачи динамического i тут используется обертывание в функциональное выражение, которое вызывается на месте:

function(i){  /* сюда передался текущий i как аргумент функции */ }(i) ;


Если бы мы просто использовали:

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


То при вызов moveElem() i бы бралась из глобальной области видимости, т.е. та, которая у нас объявлена и отработана в цикле for(var i=2; i<601; i++), т.е. вызывалась бы «отработанная» переменная i равная 600, а вовсе не динамическая i, которая должна постепенно нарастать, изменяя координаты взлетающего шарика.
Поэтому мы используем ЗАМЫКАНИЕ на javascript, которое определяет область видимости, т.е. при выполнении функции

function(i){  /* сюда передался текущий i как аргумент функции */ }(i) ;


… внутри него создалась своя переменная i как аргумент этой самой функции, а вызов на месте( повторите если забыли ), выполняет тело функции, где происходит возврат ( return ) уже нашей функции. И опять же:
не

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


а

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


потому что, в первом случае вернется не вызов функции, а результат вызова moveElem( ball, 600-i, 600-i ), которая выполнится тут же!

И вот уже у нас есть сценарий. Теперь можно его присвоить и запустить:

var timer = new Timer( );
timer.setTask( frames );
timer.start(); /* или кнопочкой start */


В целом, покадровое управление дает обширные возможности для создания интересных и сложных сценариев.
На демо странице denis-or-love.narod.ru/portf/timer я также реализовал пример покадровой линейки наподобие TimeLine в Adobe Flash :) — кнопочка drawFrameLine.

Надо заметить, что большое количество элементов на странице может сильно тормозить браузер при перерисовке их позиций, это хорошо заметно в ненаглядном IE, если нажать drawFrameLine с параметром 1 — каждый кадр.
При желании, можно написать целый интерфейс для создания сценариев, с расширяемыми возможностями и прочими приятностями. Тут уж как говорится, кто во что горазд…

image

А теперь, займемся НАСЛЕДОВАНИЕМ!
Построив базовый класс Timer, мы расширим его, созданием более продвинутого класса TimerPlayer. Ограничим пример продвинутости простым примером, — наш дочерний класс, принимая навыки родительского, будет уметь создавать панель управления нашим таймером наподобие плеера. Для этого делаем три вещи:
  1. вызов родительского конструктора
  2. добавление новых методов
  3. передачу наследования через функцию extend


//Дочерний класс
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);


И хотя, здесь у нас не происходит прототипного наследования, extend нам нужна для выстраивания цепочки на будущее( а вдруг захотим добавить прототипных свойств родителю...), и для записи superclass и constructor. Новый метод drawPanel принимает строку id элемента внутри которого помещать кнопочки, и строку имя объекта для подстановки в шаблон HTML.

//используем дочерний класс
var timerPlayer = new TimerPlayer();
timerPlayer.setTask( frames2 );
timerPlayer.drawPanel( 'controlPanel', 'timerPlayer' );


Вот мы и закончили, а может только начали наш класс Timer и его потомка.
Я хочу поблагодарить devote за терпеливые консультации и содействие в написании статьи.
До новых встреч и приятного программирования.
Tags:
Hubs:
+73
Comments 51
Comments Comments 51

Articles