Пользователь
0,0
рейтинг
27 мая 2012 в 21:14

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

Здравствуйте программисты начинающие, законченные, а также все сочувствующие. Как известно, ничто не познается так хорошо, как на собственном опыте. Главное, чтобы опыт был полезный. И в продолжении этой простой мысли я хочу предложить заняться несколькими полезными делами сразу:
  • Побеседовать на тему «ООП с человеческим лицом».
  • Разобраться с прототипами в 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 за терпеливые консультации и содействие в написании статьи.
До новых встреч и приятного программирования.
Денис Орлов @denis_orlov
карма
31,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (51)

  • +1
    Здорово написано. Спасибо! :)
  • –2
    Статья отличная, я как раз вникаю в глубины Javascript и это многое прояснило! Но вот «bisy» просто режет глаз…
  • –1
    Однозначно в закладки, спасибо!
  • +3
    Статья хорошая: ясное изложение, понятные примеры, удачные сравнения. Однозначный плюс.
    От себя хотел бы посоветовать выносить константы или константные объекты (как в данном случае defaultOptions) за пределы функции (Timer), желательно перед ней, обернув всё это в анонимную функцию:

    (function(){
    	var defaultOptions = { /*константы*/ };
    	window.Timer = function Timer( options ){
    		//...
    		//здесь есть обращение к defaultOptions
    		//...
    	}
    	//...
    })();

    Такой подход хорош тем, что:
    1) когда вы захотите подправить значения констант, поиск по коду defaultOptions упрощается;
    2) объект не создаётся каждый раз и становится доступным для других функций, например, для какой-нибудь changeTimerOptions;
    3) появляется возможность динамически менять опции таймера по умолчанию.
    Ещё раз, спасибо.
    • 0
      Спасибо, учту.
  • +3
    С прототипами, учитывая уровень статьи, мне кажется, у вас не очень понятно. Для уверенности дал прочитать эту часть двум знакомым начинающим front-end-рам — они согласились.

    Я, помогая в понимании JS, всегда даю читать вот это — лучшего мануала по прототипам и наследованию не встречал.

    А статья написано просто замечательно. У вас, должно быть, гуманитарное образование, или явный талант к изложению.
    • –1
      ну вот, напишешь хорошую статью — обзовут гумном
    • –1
      >лучшего мануала по прототипам и наследованию не встречал.
      не советовал бы я читать это, так обычно появляются клоуны, которые потом везде кричат что вы все делаете неправильное ООП в жаваскрипте и есть только уан тру вэй — это вот этот примитив с прототипами, но как правило эти люди не работали ни над чем сложным.
      есть отличное видео по этой теме Class Inheritance and Composition Patterns in YUI
      • 0
        Клоуны появляются не от этого, а от попытки сделать полноценное ООП там, где его не было задумано. С каких это пор фундаментальные знания по языку стали губительными?

        Понимать и реализовывать парадигмы так, как хочется/привыкли — личное дело каждого. Нет никаких «тру-вейев» — все ограничивается фантазией.

        Но не зная основ прототипного наследования, никогда не напишешь Y.object() или Y.extend() — верно?
        • –1
          >Клоуны появляются не от этого
          Судя по общению с людьми, появляются именно от таких статей, в которых показывают что такое прототипы, самые базовые конструкции, которые можно с их помощью получить и произносятся слова вроде «Или имитацию «классического» ООП. Ещё чаще это выглядит надругательством над возможностями JS, которое, я уверен, даёт о себе знать с усложнением проектов.»

          >Но не зная основ прототипного наследования, никогда не напишешь Y.object() или Y.extend() — верно?
          верно, но на звание лучшего мануала по наследованию та статья как-то совсем не тянет.
          • +5
            Прекрасно! Вы настаиваете на ошибочной теории и поливаете грязью тех, кто старается вернуть новичков к правильному пути. Создание методов в конструкторе — это ересь. Она нарушает идеологию ооп в общем и идеологию JS в частности. В конструкторе должно происходить конструирование экземпляра (да, такая уж тафтология), а не всего класса, как у вас. Т.Е. очень грубо говоря — чем этот экземпляр отличается от класса в целом. А у подхода с перегруженным конструктором — всё смешано в кучу. Самое отвратительное, что люди даже не понимают логики такого подхода. Посмотрите то же самое на php и почувствуйте всю отвратительность:

            <?
            
            class Timer {
            
              function __construct ($options) {
                $defaultOptions = array(
                    'delay' => 20,
                    'stopFrame' => 0,
                    'loop' => true
                );
                foreach ($defaultOptions as $option => $values) $this[$option] = $option ? ($options[$option] || $defaultOptions[$option]) : $defaultOptions[$option];
            
            
                $this->start = function() {
                  // code
                };
                $this->setActiveFrameElement = function( frameNumber ){
                  // code
                };
                $this->toString = function() {
                  // code
                }
                $this->setTask = function( new_task ) {
                  // code
                }
                $this->getKeyFrames = function(){
                    return $keyFrames;
                }
                $this->getTask = function(){
                    return $task;
                }
              }
            
            }
            

            Так почему на php такое никто не напишет, а над JS все издеваются как хотят?
            • 0
              WUT? :)

              >Создание методов в конструкторе — это ересь
              Я где-то говорил про создание методов в конструкторе?

              >В конструкторе должно происходить конструирование экземпляра ..., а не всего класса, как у вас.
              У вас замечательные способности читать чужие мысли, но спешу вас разочаровать, ваша способность иногда обманывает вас :)

              Еслиб вы потрудились взглянуть на видео, которое я привёл первым комментарием, может тогда хоть немного дошло то о чём я говорил. Я же не говорю что прототипы — это зло, я просто говорю что в сложном проекте недостаточно Object.create
              • 0
                К сожалению, у меня закрыт доступ к ТыТрубе сейчас.
                Я так понял, что вы поддерживаете позицию автора на счёт использовния перегружённого конструктора. Если ошибся, то поясните свою позицию лично, пожалуйста, без видео. Можно в двух словах.
                • 0
                  Моя позиция в том что я заметил как вокруг бегает много интересных людей, которые узнали про Object.create и когда они видят нечто вроде YUI Base, то начинают говорить о том какая это Java и все кто это используют — полные идиоты и ничего не понимают в жаваскрипте.

                  Благо разработчики YUI отлично понимают плюсы и недостатки разных подходов и используют наиболее подходящие инструменты для решения различных задач.
                  Y.Object() // Object.create
                  Y.extend()
                  Y.augment()
                  Plugins
                  Class Extensions
  • +3
    Я пока только учу JS, но такая запись
    this[option] = options? (options[option] || defaultOptions[option]): defaultOptions[option];
    не позволит установить значения опций как false, 0, '' (пустая строка).
    • +1
      Вообще, немного «корявый» способ, лучше вынести эту логику в отдельную функцию:

      var extend = function(to, from) {
      	for (var key in from)
      		if (from.hasOwnProperty(key))
      			to[key] = from[key];
      	return to;
      };
      
      //........................................................................
      
      var options = {
          foo: 1,
          bar: 2
      }
      
      // Default options
      var params = extend({
      	foo: 0
      }, options);
      
      
      params.foo //1
      params.bar //2
      


      //public
      var defaultOptions
      



      Давайте лучше рассмотрим пример:

      var Foo = new function() {
      	//private
      	var private = 1;
      
      	var Foo = function() {
      		//...
      	};
      
      	//public
      	Foo.prototype = {
      		constructor: Foo,
      		foo: 1,
      		bar: function(param) {
      			return this.foo + param + private;
      		}
      	};
      
      	return Foo;
      };
      
      var object = new Foo;
      
      object.foo;    // 1
      object.bar(1); // 3
      
      
      • 0
        Относительно наследования:

        var Extend = function(child, parent) {
            for (var key in parent) {
                if (parent.hasOwnProperty(key))
                    child[key] = parent[key];
            }
        
            var __new__ = function() {
                this.constructor = child;
            }
        
            __new__.prototype = parent.prototype;
            child.prototype = new __new__();
            child.__super__ = parent.prototype;
        
            return child;
        };
        
        function A(value) {
           this.set = value;
        };
        
        A.prototype.get = function() {
            return this.set;
        };
         
        function B(value) {
            this.set = value;
        };
        
        Extend(B, A);
        
        alert(new A(1).get()); // 1
        alert(new B(2).get()); // 2
        
        function C(value) {
            this.set = value;
        };
        
        C.prototype.get = function(value) {
            return B.__super__.get.call(this, arguments);
        };
        
        alert(new C(3).get()); // 3
        
    • 0
      Верно, ошибка моя.
      Спасибо, что заметили.
    • 0
      исправил на
      for(var option in defaultOptions) this[option] = options && options[option]!==undefined ? options[option] : defaultOptions[option];
      
  • 0
    Отличная статья! Отдельное спасибо за юмор :)
  • +1
    Ужасная статья! Здесь показано, как нельзя программировать. Во-первых, нарушается SRP. Во-вторых, неправильно используется наследование (впрочем, я вообще за то, чтобы избегать наследования как такового), хотя тут же можно отослать и к SRP. В-третьих, нарушается инкапсуляция на каждом шагу (например, что произойдёт, если таймер уже работает, а у него поменяют свойства stopFrame, или что если написать timer.getTask()[0] = foo). В-четвёртых, странноватые имена для некоторых сущностей (например, task там, где логичнее было бы tasks или schedule).
    • 0
      Статья учебная, пример в ней — тоже.
      То о чем вы говорите, решается на стадии отладки. И еще миллион возможных багов…
      • 0
        Проблемы, о которых я говорю, не решаются на стадии отладки. Они решаются на стадии рефакторинга. Вот только рефакторинг может быть разным. Где-то намеренно оставляют технический долг, потому что здесь и сейчас времени нет. А где-то люди по незнанию городят вот такое. Если бы сразу не городили, было бы лучше. Если уж показывать, как реализовано ООП в javascript, то надо заодно и писать о том, где его уместно использовать и как это правильно делается.
  • +2
    Спасибо за труд. Только пример наследования и объяснение прототипов я бы делал без extend.

    Например, опишем родителя:

    /**
    * constructor
    */
    var Foo = function() {
    /** type {number} */
    this.param = 1;
    }

    /**
    * return {number}
    */
    Foo.prototype.getParam = function() {
    return this.param;
    }

    Функция, реализующая наследование:

    /**
    * Реализует прототипное наслдование.
    * Взял из Google Closure Library.
    * @param {Function} childCtor Конструктор потомка
    * @param {Function} parentCtor Конструктор родителя
    */
    var inherits = function(childCtor, parentCtor) {
    /**
    * Создаем пустой конструктор
    * constructor
    */
    function tempCtor() {};
    // Складываем прототип родителя в прототип временного
    // конструктора
    tempCtor.prototype = parentCtor.prototype;
    // В superClass кладем ссылку на прототип родителя.
    // Нужно чтобы вызывать методы родителя.
    childCtor.superClass = parentCtor.prototype;
    // В прототип ребенка кладем экземпляр пустого
    // конструтора. Не забываем, что все методы родитля
    // лежат в его прототипе, а значит передадутся
    // ребенку
    childCtor.prototype = new tempCtor();
    // Запоминаем конструктор
    childCtor.prototype.constructor = childCtor;
    };

    Теперь ребенка:

    /**
    * constructor
    * @extends
    */
    var Bar = function() {
    // в superClass лежит ссылка на прототип родителя,
    Foo.call(this);

    /** type {number} */
    this.otherParam = 2;
    }
    // Наследуемся от Foo, код наследования чуть выше.
    inherits(Bar, Foo);

    /**
    * return {number}
    */
    Bar.prototype.getOtherParam = function() {
    return this.otherParam;
    }

    А теперь посмотрим, что получилось:

    var foo = new Foo();
    var fooParam = foo.getParam(); // 1

    var bar = new Bar();
    var barParam = bar.getParam(); // 1
    var barOtherParam = bar.getOtherParam(); // 2

    Объявлять методы внутри других методов или конструтора с помощью

    this.myMethod = function() {/*… */};

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

    Я понимаю, подход плох тем, что неудобно скрывать методы, делать их по настоящему приватными. Я в коде использую соглашеие, что приватные методы начинаются с _. Спасает только то, что на боевом сервере весь код выполняется в замыкании. Зато не нужно перекладывать метода из родителя в ребянка for..in
    • 0
      Простите, парсер лох. Пойду вырву себе руки
      • НЛО прилетело и опубликовало эту надпись здесь
      • +2
        даешь код из хабраюзеров!)
  • +3
    Убедительная просьба к автору этой статьи и к другим авторам статей по JS: используйте правильную терминологию. Не вводите новичков в заблуждение. new — это оператор, а не директива. timer — это экземпляр конструктора Timer, а не объект класса (хорошо что хоть не типа) Timer.
    Ну и самый важный момент: настоящее ООП в JS есть! После этих строк читать дальше желание отпало.

    Не хочу быть занудой, но видеть такое почти в каждой статье уже порядком надоело.
    • +2
      Во, я тоже после пассажа про «настоящее ООП» читать перестал. Раздражает вот это непреодолимое желание сделать себе велосипедные заборы и коровники java-style в яваскрипте.
      • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Не хотите быть занудой — не нудите.
      Надоело не в каждой статье про «настоящее ООП» читать, а камменты к этим статьям в вашем стиле.
      • 0
        Моё чувство прекрасного не позволило промолчать.
    • 0
      Хорошо, new — это оператор, но в данном случае можно воспринимать и как директиву, предписывающую функции выполниться особым образом, это улучшает понимание.
      timer — не объект класса?
      Конечно. Я ведь несколько раз указал условность понятия «класс», даже в заголовке.
      На мой взгляд, такие «безупречные» словесные конструкции как «экземпляр конструктора» напрочь отбивают понимание сути. Уж извините, писал, как считал более понятным.

      С интересом буду ожидать и вашу статью на тему «ООП в JS есть»…
      • 0
        Понимание — вещь относительная.
        Мне, к примеру, понятнее будет знать, что оператор new вызывает функцию, а не предписывает ей что-то. Яркий тому пример:
        new functionName;
        

        Скобки необязательны так как оператор сам всё сделает.

        По поводу термина «Класс». Если вам удобнее использовать этот термин — никто не против, но необходимо явно оговориться что вы понимаете под этим, что бы дальнейшее чтение никого не сбивало с толку. Необходимо — потому что сам термин пришел из других языков. Кавычки каждый может понять по-своему.

        «Экземпляр конструктора» — это и есть суть. Если вы считаете такое определение непонятным — расскажите о нем более развернуто, а не ищите ему замену.

        Писать ещё одну статью про ООП в JS не вижу смысла, уже всё написано.
  • +1
    Статья очень плохая. Просто сборник дурных практик.
    Я понимаю, мы 10 лет назад долго писали такой пи**ец-код, пока не разобрались — просто потому что нормальной документации по теме вообще не было.
    Но сейчас-то?

    Почитайте MDN, загляните в реализацию наследования в node.js и в Google Closure Library.
    Не городите ужасов, пожалуйста.
    • +2
      Пожелание к 3-м последним комментаторам:

      С интересом буду ожидать ВАШУ статью на тему "ООП в JS есть!"…
      • 0
        Да, пожалуй, придется )))

        Денис, без обид, но Вы действительно не слишком хорошо разобрались в теме.

        А здесь очень много новичков, которые этого просто не видят.
        И мало JS-разработчиков с серьезным опытом в IT за пределами создания прикольной анимации и плагинов к jQuery. Таких вообще мало, к сожалению.

        Так что не принимайте восторженные отзывы слишком серьезно.
        Лучше поговорите с кем-нибудь, кто в теме давно, всерьез и надолго.
        • 0
          Я и не обижаюсь. И на безупречное знание темы не претендую.
          А Вы, в теме давно? Вам и слово.

          По сути то Вы правы: есть ООП! Потому, что в первую очередь оно есть… в душе программиста, а значит и везде где он пожелает ему следовать. Об этом же и статья.
          А вот в спецификации нет.

          Но душа то нам важнее. ;-)
          • 0
            А вот в спецификации нет.

            Как это нет?

            Each constructor is a function that has a property named “prototype” that is used to implement prototype-based inheritance and shared properties. Objects are created by using constructors in new expressions...

            4.2.1 Objects
            • 0
              Похоже, для Вас это вопрос веры, а не точности тех. реализации.
              В священные войны я не вступаю. :)
              • 0
                Вы не просто вступаете в священные войны. Вы их развязываете!
                • –1
                  Так это видят только те, кто хочет воевать. :)
                  • 0
                    То есть вы хотите воевать и потому вам кажется, что monolithed холиварит? Интересная логика.
                    • –1
                      Не я, вы так видите, ведь вы это утверждаете:
                      Вы не просто вступаете в священные войны. Вы их развязываете!
      • 0
        Денис, ну что за детский сад: «С интересом буду ожидать ВАШУ статью на тему «ООП в JS есть!»»? Не обязательно быть поваром, чтоб понять, что блюдо — не вкусное. Это критика, причем обоснованная, не нужно скатываться до «сперва добейся сам».
  • 0
    На момент публикации статьи очень не кстати выяснилось, что на сайте denis-or-love.narod.ru где пример, засел хулиганский js скрипт.
    Скрипт я «выпилил», но плашка с предупреждением от яндекс пока осталась.
  • –2
    Ужасная статья, к третьему абзацу понятно, что ООП в JavaScript мне не нужен и лучше пойти какао попить и не тратить время. Очень много воды, вернее весь текст — вода с кусочками кода, чтобы показать что статья техническая.

    Настоящего ООП в JS нет! Как выше заметили местами код ужасен.

    Писать свою статью не буду, ибо логика сначала сам сделай, а потом критикуй не для меня :)
  • 0
    Разумеется.
    Критикуют одни, а созидают другие. :)

    Статья написана не ради плюсиков или пустого тролле-трепа, а ради реальной пользы, которую она уже принесла и продолжает нести.

    Спасибо, что читаете.
    • 0
      Какую пользу она приносит? Практическую? Да, после вашей статьи появится на еще одного говнокодера больше, который не знает, что ООП в JS есть. Он будет держаться молодцом, зарабатывать деньги, но будет оставаться безграмотным, не знающим элементарнейших вещей из своей области. Не обольщайтесь хорошими оценками к посту, я так же, как и вы могу писать сатьи на тему квантовой механики, имея знания только «по верхам» и доставляя радость людям, которые в ней разбираются немногим хуже меня.
      • 0
        Пардон, вы уже отметились в комментариях к своей бывшей статье о том, что на ноль делить нельзя.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.