Время прочтения: 13 мин.

Привет! Предлагаю рассмотреть пример объектно-ориентированного программирования (ООП) на JavaScript с использованием jQuery. На практике покажу, как инициализировать объекты, создавать методы и структуру класса. Данный материал поможет сделать рефакторинг вашего старого кода и перейти с процедурного кода JS на JS ООП, а также решить некоторые проблемы использования jQuery в классах.

В материале использовался jQuery 3.4.1 и браузеры ЯндексБраузер 19.3.1.828, Microsoft Edge 98.0.1108.43

За последнее время JavaScript, как клиентский язык, хорошо развился. Теперь JavaScript стал поддерживать стиль объектно-ориентированного программирования. Но, несмотря на это, до сих пор замечаю, что большинство web-приложений используют процедурный код на JS. Я думаю, что для этого есть несколько причин, которые тормозят процесс апгрейда клиентского кода фронтенда:

  1. Неравномерный переход на более свежие версии браузеров «пользователей».
  2. Неравномерная поддержка новой версии JS самими браузерами.
  3. Старые привычки — например, использование jQuery и некоторые сложности в правильном использовании данной библиотеки внутри классов.

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

Я не буду рассказывать про стандарты ECMAScript (ES) JavaScript и в чем разница между ES5, ES6, ES8 (и т.д.) в ООП JS – на эту тему материала достаточно много. Рассмотрю «живой» пример ООП на JavaScript с использованием jQuery. Расскажу, как инициализировать объекты и создавать методы класса. Создам структуру класса, на примере формы регистрации. Описанные приёмы помогут сделать первый шаг к рефакторингу вашего старого кода и перейти с процедурного кода JS на JS ООП, а также решить некоторые проблемы использования jQuery в классах.

Прежде чем приступить к разработке JS скрипта, проведите предварительную работу:

Создайте каталог c:/workspace/form_es6. В каталоге «form_es6» создайте форму регистрации:

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Форма регистрации ES6</title>
<link rel="stylesheet" href="css/form_new.css">
<script type="text/javascript" src="lib/jquery/jquery-3.4.1.min.js"></script>
<script type="text/javascript" src="js/form1ES6.js"></script>

</head>
<body>
<form class="registrity" action="" method="post" id="registrity">
<fieldset class="f_form1">
<table id="tab_form">
<tr><th colspan="2"> Форма регистрации ES6 </th></tr>
<tr><td class="td_left top_pos">ФИО*:</td><td class="td_right top_pos"><input id="fio" type="text" placeholder="ФИО" required="required"></td></tr>
<tr><td class="td_left">Регион*:</td><td class="td_right">
	<select id="region" type="select" class="set_change" required="required">
	    <option value="">-</option>
		<option value="moscow_mo">Москва и МО</option>
		<option value="other_reg">Другие регионы</option>
	</select>
	<span id="cur_time"></span>
</td></tr>
<tr style="display:none;" id="np_tr"><td class="td_left">н/п*:</td><td class="td_right">
	<select id="np" type="select" class="set_change">
	    <option value="">-</option>
		<option value="Архангельск" data-tmz="0" >Архангельск</option>
		<option value="Астрахань" data-tmz="1" >Астрахань</option>
		<option value="Белгород" data-tmz="-1" >Белгород</option>
		<option value="Брянск" data-tmz="0" >Брянск</option>
		<option value="Великий Новгород" data-tmz="0" >Великий Новгород</option>
		<option value="Владивосток" data-tmz="7" >Владивосток</option>
	</select>
</td></tr>
<tr><td class="td_left">email*:</td><td class="td_right"><input id="email" type="email" placeholder="Эл. адрес" required="required"></td></tr>
<tr><td class="td_left">Пароль*:</td><td class="td_right"><input id="pass" type="password" placeholder="*****" required="required"></td></tr>
<tr><td class="td_left">Дата рождения:</td><td class="td_right"><input id="birthdate" type="date"></td></tr>
<tr>
	<td class="td_left">Пол:</td>
	<td class="td_right">
		<input id="s_w" type="radio" name="sex[]" value="ж"><span class="t_sex">ж</span> 
		<input id="s_m" type="radio" name="sex[]" value="м"><span class="t_sex">м</span>
	</td>
</tr>
<tr><td class="td_left">О себе:</td><td class="td_right">
<textarea id="about" placeholder="Введите текст"></textarea></td></tr>
<tr>
	<td colspan="2" class="td_left"> 
		<button type="reset" id="bt_cleen">Очистить</button>
		<button type="submit" id="bt_send">Отправить</button>
	</td>
</tr>
</table>
</fieldset>
</form>
<div id="data_submit">
<p id="p_fio" class="result_submit"></p>
<p id="p_address" class="result_submit"></p>
<p id="p_email" class="result_submit"></p>
<p id="p_birthdate" class="result_submit"></p>
<p id="p_sex" class="result_submit"></p>
<p id="p_about" class="result_submit"></p>
</div>
</body>
</html>

Для используемого мной примера необходимо скачать библиотеку jQuery с официального сайта и поместить его в каталог c:/workspace/form_es6/lib/jquery. Главное – не забыть в index.html включить библиотеку jQuery:

<script type="text/javascript" src="lib/jquery/jquery-3.4.1.min.js"></script>

Создайте файл со стилями в каталоге c:/workspace/form_es6/css:

css/form_new.css

/*
* Оформление формы
*/

#tab_form,.registrity {
	border:0;
	width:420px;
	margin:0 auto;
}


#tab_form .td_left{
	color: #333399;
	padding-left:0.5em;
	padding-top:0.3em;
	padding-bottom:0.3em;
	padding-right:0.5em;
	text-align: right;
	vertical-align: center;
}

#tab_form .td_right{
	padding-left:0.5em;
	padding-top:0.3em;
	padding-bottom:0.3em;
	padding-right:0.1em;
	text-align: left;

}
#tab_form th {
   height:50px;
   color: #333399;
   vertical-align:middle;
   text-align: center;
   font-size: 16px;
   border-bottom: 1px solid #9999CC;
}

#bt_send, #bt_cleen{
	margin-right: 5px;
	margin-top: 10px;
	padding-left:10px;
	padding-right:10px;
	color:#333399;
	border: 1px solid #666699;
	height: 26px;
	border-radius: 4px;
}
.f_form1{
	border: 1px solid #666699;
	background-color: #CCCCFF;
}
#tab_form .top_pos{
	padding-top: 20px;
}

#about {
	width:250px;
	min-height: 72px;
	line-height: 72px;
	height:72px;
	padding: 0 10px;
}

#birthdate {
	color: #666666;
	min-height: 20px;
	line-height: 25px;
	height:25px;
	padding: 0 10px;	
}

#tab_form .t_sex {
	color: #666666;
	margin-bottom: 8px;
	margin-left: 3px;	
}

#tab_form input, textarea {
	border: 1px solid #666699;	
}

#tab_form input[type="text"],
	input[type="email"],
	input[type="password"]
	
{
	width:250px;
	min-height: 20px;
	line-height: 25px;
	height:25px;
	padding: 0 10px;
}


#tab_form 
	input[type="password"]::-webkit-input-placeholder	
{
	transform:translate3d(0,3px,0);
	font-size: 15px;
}

#s_w, #s_m {
	border: 1px solid #666699;
}

#data_submit {
	width: 400px;
	margin:0 auto;
	border: 1px solid #666699;
	margin-top:40px;
	padding-left:5px;
}

.result_submit{
	color: #333399;
	font-weight: bold;
}

Страничка с простейшей формой подготовлена, теперь форма должна выглядеть вот так:

Далее, необходимо создать form1ES6.js файл и поместить его в каталог c:/workspace/form_es6/js.

Откройте form1ES6.js в редакторе, чтобы начать основную работу. Создайте класс:

js/form1ES6.js

class form1 {
	
}

Вызов класса можно прописать в том же файле form1ES6.js после закрывающей фигурной скобки класса:

class form1 {
//переменные и метода класса
…	
} 
//создать объект класса при загрузке страницы.
//вы можете поместить этот кусок в отдельный файл
$(document).ready(function() {
	const objform1 = new form1();
})

А можно прописать вызов класса в отдельном файле (например: в callDocReady.js) и включить его на странице index.html

(<script type="text/javascript" src="js/callDocReady.js"></script>)

Внутри класса «form1» создайте конструктор — специальный метод, который вызывается автоматически при вызове класса. В конструкторе будут инициализироваться переменные класса и метод:

class form1 {
	constructor(){
		//Форма регистрации
		this.form1 = $("#registrity");
		//ФИО
		this.fio = $("#fio");
		//Регион
		this.region = $("#region");
		//Населенный пункт
		this.np = $("#np");
		//Эл адрес
		this.email = $("#email");
		//Пароль
		this.pass = $("#pass");
		//День рождения
		this.birthdate = $("#birthdate");
		//Пол
		this.t_sex = $("input[name='sex[]']");
		//О себе
		this.about = $("#about");
		//Скрытая строка с полем городов
		this.np_tr = $("#np_tr");
		//Тег span в который помещаем дату-время
		this.cur_time = $("#cur_time");
		//Элемента формы с классом set_change, чтобы отслеживать событие onchange
		this.sortOption = $(this.form1).find('.set_change');
		//Строка с приветствием
		this.hello = $("#p_hello");
		//Кнопка очистки формы
		this.bt_cleen = $("#bt_cleen");
}
}

И, как вы уже заметили, в примере используется такая любимая и привычная библиотека jQuery.

Теперь я хочу связать два элемента. А именно: по выбору значения пункта «Другие регионы» списка <select id=«region»> — чтобы показывалась скрытая строка <tr id=«np_tr»>  с другим списком <select id=«np»>. Для этого создайте в классе метод «trigger_select» с входным параметром «element«:

class form1 {
     constructor(){
            …
     }     
     trigger_select(element){
          //при изменении region показываем или скрываем список np
          if($(element)[0].id =='region'){
               if($(element)[0].value =='other_reg'){
                    this.np_tr.show();
                        
               }else{
                    this.np_tr.hide();
               }
          }
     }
}

Теперь необходимо вызвать этот метод при событии формы – «onchange». Создайте следующий метод внутри класса:

class form1 {
constructor(){
	…
}	
trigger_select(element){
…
}
init(){
	this.sortOption.on('change',(e)=>{
//console.log(e);
		this.trigger_select(e.currentTarget);

	});
}

}

Внутри этого метода вызван метод trigger_select. В качестве параметра, передайте «currrentTarget» – свойство объекта «Event», который содержит элемент, привязанный к обработчику события. В console.log(e) это выглядит так:

Теперь вызовите метод init в конструкторе класса:

class form1 {
constructor(){
	//Форма регистрации
	this.form1 = $("#registrity");
…
	//Кнопка очистки формы
	this.bt_cleen = $("#bt_cleen");

	this.init();
}
…

}

Поздравляю, первый шаг сделан! Теперь проверьте работу в Браузере (Браузер должен поддерживать JS стандарта ECMAScript 2015 (ES6) и выше. И, конечно же, поддерживать HTML5. Необходимо убедиться, что исполнение JS в настройках браузера включено). Откройте index.html в браузере, обновите страницу Ctrl+F5 и пробуйте выбрать пункты регионов. Если все сделано правильно, то должен открыться второй список с населенными пунктами по выбору значения «Другие регионы»

Теперь необходимо расширить возможности формы: при выборе населенного пункта выставлялось его текущее время.

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

class form1 {
constructor(){
	…
}

addHoursDate(dh){
	const inDate = new Date();
	
	inDate.setHours(inDate.getHours()+(1*dh));
	
	const d = inDate.getDate();
	const m = inDate.getMonth() + 1;
	const y = inDate.getFullYear();
	const hh = inDate.getHours();
	const mm = inDate.getMinutes();
	const ss = inDate.getSeconds();	
	
	return '' + (d<=9 ? '0'+d : d) + '.' + (m<=9 ? '0'+m : m) + '.' + y + ' ' + (hh<=9 ? '0'+hh : hh)+ ':' + (mm<=9 ? '0'+mm : mm) + ':' + (ss<=9 ? '0'+ss : ss);

}
	
trigger_select(element){
…
}

init(){
…
}

}

В качестве входного параметра будет переменная dh, которая будет принимать число со знаком. А возвращать дату и время в формате dd.mm.yyyy hh24:min:ss типа string.

Осталось придумать откуда принимать значения во входную переменную. Для этого можно в «option» со списком населенных пунктов добавить атрибут «data-tmz» со значением часового сдвига от московского времени:

В index.html:

<select id="np" type="select" class="set_change">
<option value="">-</option>
<option value="Архангельск" data-tmz="0" >Архангельск</option>
<option value="Астрахань" data-tmz="1" >Астрахань</option>
<option value="Белгород" data-tmz="1" >Белгород</option>
<option value="Брянск" data-tmz="0" >Брянск</option>
<option value="Великий Новгород" data-tmz="0" >Великий Новгород</option>
<option value="Владивосток" data-tmz="7" >Владивосток</option>
</select>

И теперь добавьте в класс метод, который будет получать атрибут выбранного пункта списка населенных пунктов и возвращать полученное значение:

class form1 {
constructor(){
	…
}

addHoursDate(dh){
	…
}

getSortOptionAttr(element){
		const sortDataOpt = $(element)[0].options[$(element)[0].selectedIndex].dataset.tmz;
		return sortDataOpt;
}

	
trigger_select(element){
…
}

init(){
…
}

}

Поясню для тех, кто еще не знает – почему dataset.tmz, если в примере атрибут полностью называется «data-tmz».

В HTML5 есть поддержка расширения данных элементов с помощью атрибута data-*. Можно добавить в элемент свой собственный атрибут начинающийся с data, а после дефиса именовать своими наименованиями. Например, «data-parent» или «data-go-city», и т.д. В таких атрибутах можно хранить дополнительные значения. В JS доступ к таким атрибутам осуществляется с помощью метода getAttribute(), либо, как в моём случае, – с помощью объекта «dataset». Получить значение свойства объекта «dataset», можно обратившись по части имени, которая стоит после «data-«. В примере  – это «tmz«. Продолжим. Входной параметр придуман, теперь осталось придумать: где показывать полученные дату и время. Я прописала тег

<span id="cur_time"></span> 

на страницу с формой рядом со списком Регионов, но его можно разместить по своему вкусу, где больше нравится:

В index.html:

<tr><td class="td_left">Регион*:</td><td class="td_right">
	<select id="region" type="select" class="set_change" required="required">
	    <option value="">-</option>
		<option value="moscow_mo">Москва и МО</option>
		<option value="other_reg">Другие регионы</option>
	</select>
	<span id="cur_time"></span>
</td></tr>

Чтобы разместить текст в этом теге, сначала создайте метод, который будет размещать любой контент в указанный элемент:

class form1 {
constructor(){
	…
}

addHoursDate(dh){
	…

}

getSortOptionAttr(element){
	…
}

setHTMLElement(element, value){
		if(value != ''){
			$(element).html(value);
		}else{
			$(element).html('');
		}
}
	
trigger_select(element){
…
}

init(){
…
}

}

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

Сопутствующие методы написаны, теперь в уже имеющийся метод «trigger_select» добавьте строки:

trigger_select(element){
if($(element)[0].id =='region'){
	if($(element)[0].value =='other_reg'){
		this.np_tr.show();
	}else{
		this.np_tr.hide();
		this.setHTMLElement(this.cur_time,'');
	}
//при изменении np устанавливаем текущее время выбранного города
}else if($(element)[0].id == 'np'){
	if($(element)[0].value !=''){
this.setHTMLElement(this.cur_time,this.addHoursDate(this.getSortOptionAttr(element)));
	}
}
}

Обновите браузер и проверьте результат, выбирая пункты списка «населенные пункты». Должно получиться так:

Теперь создайте «бантики» на форму.  Как уже известно, в HTML5 существует встроенная валидация полей. Например, логический атрибут «required» в тегах форм будет проверять заполнение полей. Если поля формы статичные, то такого механизма вполне хватает для простой проверки заполнения и указания обязательных полей формы. А как быть, если необходимо выставлять статус обязательного заполнения, в зависимости от выбора другого поля формы? В этом поможет JS.

Добавьте в метод «trigger_select« строки this.np.attr(‘required’, true) и this.np.attr(‘required’, false):

trigger_select(element){
	//при изменении region показываем или скрываем список np
	if($(element)[0].id =='region'){
		if($(element)[0].value =='other_reg'){
			this.np_tr.show();
			this.np.attr('required', true);//становится обязательным
		}else{
			this.np_tr.hide();
			this.np.attr('required', false);//теперь необязательное
			this.setHTMLElement(this.cur_time,'');
		}
	//при изменении np устанавливаем текущее время выбранного города
	}else if($(element)[0].id == 'np'){
		if($(element)[0].value !=''){
			this.setHTMLElement(this.cur_time,this.addHoursDate(this.getSortOptionAttr(element)));
		}
	}
}

Из примера видно, что если выбрать «Другие регионы» и не выбрать населенный пункт, то при нажатии на кнопку «Отправить» выдается ошибка. Но если выбрать «Москва и МО», то ошибки не будет:

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

class form1 {
constructor(){
	…
}

addHoursDate(dh){
	…

}

getSortOptionAttr(element){
	…
}

setHTMLElement(element, value){
		…
}
	
trigger_select(element){
…
}

trigger_click(element){
		if($(element)[0].id =='bt_cleen'){
			this.np_tr.hide();
			this.setHTMLElement(this.cur_time,'');
		}
}

init(){
…
}

}

Теперь необходимо вызвать этот метод в методе init() по событию onclick:

init(){
…
this.bt_cleen.on('click',(e)=>{
			
		this.trigger_click(e.currentTarget);

	});
}

По аналогии с событием «Onclick», можно перехватывать событие Onsubmit:

init(){
…	
       this.form1.on('submit',(e)=>{
       	this.setHTMLElement($("#p_fio"),this.fio.val());
       	this.setHTMLElement($("#p_address"),"Адрес: " + this.region.find("option:selected").text() +', '+ this.np.val());
       	this.setHTMLElement($("#p_email"),"Эл адрес: " + this.email.val());
       	this.setHTMLElement($("#p_birthdate"),"д/р: " + this.birthdate.val());
       	this.setHTMLElement($("#p_sex"),"Пол: " + this.t_sex.val());
       	this.setHTMLElement($("#p_about"),"О себе: " + this.about.val());
       	return false;

	});
}

В указанные элементы страницы вернутся значения полей, а «return false» запретит перегружать страницу при «submit«. Это удобно, если необходимо отправлять данные с помощью ajax на сервер.

Теперь рассмотрим сеттеры и геттеры — это специальные методы класса, которые позволяют, соответственно, получить и присвоить значение определенных свойств объектов. Добавьте в класс сеттер (set) и геттер (get), метод «hello_fio(fio)», и новую переменную в конструктор класса «this.hfio«, которая по умолчанию будет принимать значение входной переменной «helloWorld».

class form1 {
constructor(helloWorld){
	…
this.hfio = helloWorld;
}

addHoursDate(dh){
	…

}

getSortOptionAttr(element){
	…
}

setHTMLElement(element, value){
		…
}
	
trigger_select(element){
…
}

trigger_click(element){
…
}

hello_fio(fio){
console.log('hello_fio: '+`${this.hfio} ${fio}`);
		return `${this.hfio} ${fio}`;
	}
	
	get HelloFIO(){
		console.log('getter: '+this.hfio);
		return this.hfio;
	}
		
	set HelloFIO(helloWorld){
		console.log('setter: '+helloWorld);
		this.hfio = helloWorld;	
}

init(){
…
}

}

Теперь измените строку, которая будет возвращать в тег <p id=«p_fio»> итоговый текст:

init(){
 …
		
        this.form1.on('submit',(e)=>{
        	
        	this.setHTMLElement($("#p_fio"),this.hello_fio(this.fio.val()));
        	this.setHTMLElement($("#p_address"),"Адрес: " + this.region.find("option:selected").text() +', '+ this.np.val());
        	this.setHTMLElement($("#p_email"),"Эл адрес: " + this.email.val());
        	this.setHTMLElement($("#p_birthdate"),"д/р: " + this.birthdate.val());
        	this.setHTMLElement($("#p_sex"),"Пол: " + this.t_sex.val());
        	this.setHTMLElement($("#p_about"),"О себе: " + this.about.val());
        	return false;

		});
	}

В части скрипта с вызовом класса добавьте строки:

$(document).ready(function() {
	const objform1 = new form1('Доброе утро'); //Передаем значение 'Доброе утро' в переменную класса "helloWorld" 
objform1.HelloFIO; //это строка для эксперимента, ее можно удалить
	objform1.HelloFIO= 'Добрый день'; //Присваиваем новое значение 'Добрый день' переменной "hfio" объекта
})

Из примера видно, что в переменную класса «helloWorld» передается строка: «Доброе утро», а затем, вызывается сначала геттер (без параметра), а потом сеттер (с параметром и новым значением).

В console.log можно проследить вызовы геттера и сеттера, и значение, которое хранится в переменной «this.hfio» в момент вызовов. А метод «hello_fio(fio)» вернет итоговый текст «Добрый день Olga».

Теперь самостоятельно добавьте любые события, дополнительные проверки, динамику, чтобы сделать форму регистрации еще красивее и динамичнее.

В этом материале показано, как можно переделать свои старые скрипты JS на ООП. Конечно, это не все возможности JS ES6 и выше, здесь описаны только основные понятия и принципы работы ООП без более сложных конструкций. В следующей части вы познакомитесь с наследованием, инкапсуляцией, полиморфизмом.