Классы в JavaScript появились только после выхода стандарта ES6 (EcmaScript 2015), привнесшего массу нововведений в синтаксис языка, хотя ключевое слово class было зарезервировано в JavaScript с момента его создания. С точки зрения подхода к созданию однотипных объектов, создание классов очень облегчило подход к записи кода, с точки же зрения работы самого языка JavaScript поменялось немного, т.к. классы все равно базируются на прототипах.
Именно по последней причине классы еще называют синтаксическим сахаром - т.е. красивой оберткой над тем, что было заложено в ядро JavaScript с самого начала, а именно - создание функций, которые являлись конструктором для экземпляров определенного класса и основывались на использовании определенных внутри этой функции свойств и методов. Тем не менее создание классов сделало этот язык понятней для программистов на Java или C# и упростило сам синтаксис создания объектов как экземпляров класса.
Синтаксис класса в JavaScript
Во-первых, как для любой переменной или функции, для класса нужно задать имя после ключевого слова class
. Оно записывается с большой буквы и после него идут фигурные скобки (тело класса), в которых будут находится все функции (методы) этого класса.
1 2 3 4 5 6 7 8 |
class NameOfClass { // методы класса constructor() { ... } method1() { ... } method2() { ... } method3() { ... } ... } |
Основной функцией в классе является конструктор (constructor()
). Именно он вызывается при создании экземпляра класса строкой вида:
1 |
let firstExample = new NameOfClass(); |
В классе может быть только один конструктор, т.е. метод с именем constructor вы можете объявить однократно.
Достаточно часто внутрь функции-конструктора передаются некоторые переменные, которые в становятся свойствами этого класса и затем используются в различных его методах. Например, нам необходимо создать класс, который будет выводить часы, минуты и секунды. Мы не будем использовать в нем методы объекта Date, но он позволит нам манипулировать временем. Назовем этот класс Clock и используем в качестве свойств то, что нам надо вывести.
1 2 3 4 5 6 7 8 9 10 11 12 |
class Clock { constructor(hour, min, sec) { this.hour = hour; this.min = min; this.sec = sec; } showClock(){ document.write(`<h4> ${this.hour} : ${this.min} : ${this.sec}<h4>`); } } let clock1 = new Clock(12, 17, 36); clock1.showClock(); |
В конструктор класса мы передаем начальные значения часов, минут и секунд. А функция showClock()
выведет нам все переданные значения в виде строки с разделением с помощью двоеточия.
Обратите внимание на то, что внутри класса все свойства записываются с помощью ключевого слова this
, которое указывает на текущий объект класса Clock. Запись функции-конструктора в классе можно заменить такой функцией:
1 2 3 4 5 6 7 8 9 10 |
function Clock(hour, min, sec) { // this = {}; (неявным образом создается пустой объект) // добавляет свойства к this this.hour = hour; this.min = min; this.sec = sec; // return this; (объект из функции неявно возвращается с уже назначенными свойствами) } |
Обычно конструкторы ничего не возвращают явно. Их задача – записать все необходимые свойства в this
, а иногда и вызвать методы, чтобы мы могли пользоваться экземпляром класса.
Обратите внимание на то, что классы всегда нужно объявлять ДО их использования, в отличие от функций. Объявление функции (function declaration) совершает подъём (hoisted), т.е. интерпретатор знает о ее существовании до момента вызова, где бы она не была объявлена, в то время как объявление класса (class declaration) — нет. Поэтому сначала нужно объявить класс и только потом создавать его экземпляры во избежание ошибок типа ReferenceError
:
Поэтому мы объявляем внизу (строка 11) переменную clock1
, которая является экземпляром созданного нами класса Clock
. Результат:
Мы можем вывести с помощью нашего класса текущее время, воспользовавшись встроенным в JavaScript классом Date и его методами.
1 2 3 |
let now = new Date(); let clock2 = new Clock(now.getHours(), now.getMinutes(), now.getSeconds()); clock2.showClock(); |
Результат:
Что же будет, если мы создадим еще один экземпляр класса Clock с цифрами, большими, чем в привычном нам формате часов.
1 2 |
let clock3 = new Clock(27, 122, 368); clock3.showClock(); |
В результате выведутся все цифры, которые мы передали в функцию-конструктор, т.к. она сейчас не предполагает каких-либо изменений, связанных с форматом привычных нам часов.
Добавление методов класса Clock
Поскольку предыдущий пример показал, что класс требует доработки, изменим его таким образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
class Clock { constructor(hour, min, sec) { this.hour = hour; this.min = min; this.sec = sec; this.checkMin(); this.checkSec(); this.checkHours(); this.showClock(); } format(num) { return num < 10 ? '0' + num : num; } showClock() { document.write(`<h4>${this.format(this.hour)} : ${this.format(this.min)} : ${this.format(this.sec)}<h4>`); } checkSec() { if (this.sec >59) { this.min += Math.trunc(this.sec / 60); this.sec %= 60; } return this.sec; } checkMin() { if (this.min > 59) { this.hour += Math.trunc(this.min / 60); this.min %= 60; } return this.min; } checkHours() { return this.hour > 23 ? this.hour %= 24 : this.hour; } } let clock4 = new Clock(27, 122, 368); |
В функции-конструкторе мы вызываем 3 функции для проверки корректности переданных параметров для реальных часов. Например, функция checkHours
проверяет значение this.hour
и в том случае, если оно больше 23, записывает остаток от деления на 24 в свойство this.hour
и возвращает его в качестве значения. Для минут и секунд функции будут похожи и в их задачу будет входить определение того, а превышает ли значение минут (секунд) цифру 59, и если это так, то остаток от деления мы будем записывать в this.min
(this.sec
), а целочисленное значение от деления на 60 добавлять к часам или минутам.
Результат создания экземпляра класса с параметрами new Clock(27, 122, 368)
вернет нам уже совсем другие значения:
Надеюсь, вы обратили внимание на то, что все функции класса внутри него также вызываются с помощью ключевого слова this
, которое в момент объявления переменной-экземпляра класса Clock
, связано именно с этой переменной. Можно сказать, что this
- это указание на переменную (объект), которая находится слева от точки, с помощью которой мы вызываем методы класса.
В классе Clock мы также добавили метод format
, который выводит ноль перед числами до 10 в привычном нам формате часов : минут : секунд.
Использование внешней функции и собственных методов
Допустим, нам нужно добавить функционал к нашему классу, который бы позволял добавлять часы, минуты, секунды и выводить скорректированное время. Поскольку нам нужно понимать, где начальное значение нашей переменной, а где - измененное, мы преобразуем код класса, добавив в вывод стиль, меняющий цвет текста выводимого заголовка h4
и опишем это во внешней функции:
1 2 3 4 5 6 |
function randomColor() { let r = Math.floor(Math.random() * 256); let g = Math.floor(Math.random() * 256); let b = Math.floor(Math.random() * 256); return `rgb(${r}, ${g}, ${b})`; } |
Сам код класса также станет другим. Обратите внимание, что в том месте в функции showClock()
, где мы вызываем внешнюю функцию randomColor()
, мы уже не пишем this.randomColor()
, т.к. эта функция не входит в методы класса Clock.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
class Clock { constructor(hour, min, sec) { this.hour = hour; this.min = min; this.sec = sec; this.checkMin(); this.checkSec(); this.checkHours(); this.showClock(); } format(num) { return num < 10 ? '0' + num : num; } showClock(text = 'Начальное значение') { document.write(`<h4 style="color: ${randomColor()}">${text}: ${this.format(this.hour)} : ${this.format(this.min)} : ${this.format(this.sec)}<h4>`); } checkSec() { if (this.sec > 59) { this.min += Math.trunc(this.sec / 60); this.sec %= 60; } return this.sec; } checkMin() { if (this.min > 59) { this.hour += Math.trunc(this.min / 60); this.min %= 60; } return this.min; } checkHours() { return this.hour > 23 ? this.hour %= 24 : this.hour; } addHours(someHours) { this.hour += someHours; this.checkHours(); } addMin(someMin) { this.min += someMin; this.checkMin(); } addSec(someSec) { this.sec += someSec; this.checkSec(); } } let clock5 = new Clock(21, 12, 23); clock5.addHours(6); clock5.showClock('Добавили 6 часов'); clock5.addMin(48); clock5.showClock('Добавили 48 минут'); clock5.addSec(200); clock5.showClock('Добавили 200 секунд'); |
Мы добавили функции addHours()
, addMin()
и addSec()
, каждая из которых принимает некоторое числовое значение, добавляет это значение к соответствующему свойству нашего класса, а затем проверяет его на правильность. Мы также поменяли вывод информации, добавив к времени поясняющий текст в функции showClock()
. Смотрим на результат:
Добавляем интерактивности в класс
Наш класс все больше расширяется, но пока мы можем только изменять значения, передавая параметры в функцию. Намного интереснее было бы протестировать его, вводя данные в поля формы. Для начала нам нужно сделать html-разметку формы с полями типа number
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<form name="addClockData" id="addClockData"> <p> <label for="hours">Добавьте часы</label> <input type="number" name="hours" id="hours" value="5" min="0"> <button type="button" class="add" data-func="addHours" data-option="часов">Ок</button> </p> <p> <label for="minutes">Добавьте минуты</label> <input type="number" name="minutes" id="minutes" value="80" min="0" step="10"> <button type="button" class="add" data-func="addMin" data-option="минут">Ок</button> </p> <p> <label for="seconds">Добавьте секунды</label> <input type="number" name="seconds" id="seconds" value="30" min="0" step="10"> <button type="button" class="add" data-func="addSec" data-option="секунд">Ок</button> </p> </form> <p id="outputClock"></p> |
Измененный класс Clock с новой функцией showHTMLClock()
для вывода сообщений в нужный html-элемент и функцией checkInputData(some)
, которая не позволяет отнимать время, передавая отрицательные значения и проверяет правильность ввода чисел, если вдруг вы захотите получить данные от пользователя методом prompt()
или использовать текстовые поля вида <input type="text">
. Кроме того, мы убрали из конструктора вызов функции showClock()
, так как теперь мы будем добавлять информацию в нужный html-элемент с помощью showHTMLClock()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
class Clock { constructor(hour, min, sec) { this.hour = hour; this.min = min; this.sec = sec; this.checkMin(); this.checkSec(); this.checkHours(); } format(num) { return num < 10 ? '0' + num : num; } showClock(text = 'Начальное значение') { document.write(`<h4 style="color: ${randomColor()}">${text}: ${this.format(this.hour)} : ${this.format(this.min)} : ${this.format(this.sec)}<h4>`); } showHTMLClock(elem, text = 'Начальное значение') { elem.insertAdjacentHTML('beforeend',`<h4 style="color: ${randomColor()}">${text}: ${this.format(this.hour)} : ${this.format(this.min)} : ${this.format(this.sec)}<h4>`); } checkSec() { if (this.sec > 59) { this.min += Math.trunc(this.sec / 60); this.sec %= 60; } return this.sec; } addSec(someSec) { someSec = this.checkInputData(someSec); this.sec += someSec; this.checkSec(); } checkMin() { if (this.min > 59) { this.hour += Math.trunc(this.min / 60); this.min %= 60; } return this.min; } addMin(someMin) { someMin = this.checkInputData(someMin); this.min += someMin; this.checkMin(); } checkHours() { return this.hour > 23 ? this.hour %= 24 : this.hour; } addHours(someHours) { someHours = this.checkInputData(someHours); this.hour += someHours; this.checkHours(); } checkInputData(some) { if (isNaN(some)) { alert('Недопустимый ввод. Значение не учитывается'); return 0; } some = some < 0 ? some *= -1 : some; return some; } } let outputClock = document.getElementById('outputClock'), addBtns = document.querySelectorAll('.add'); let clock6 = new Clock(12, 0, 16); clock6.showHTMLClock(outputClock); addBtns.forEach(btn => btn.addEventListener('click', function() { let option = this.dataset.option; let addValue = +this.previousElementSibling.value; switch(this.dataset.func){ case 'addSec': clock1.addSec(addValue); break; case 'addMin': clock1.addMin(addValue); break; default : clock1.addHours(addValue); } clock6.showHTMLClock(outputClock, 'Добавлено ' + addValue + ' ' + option); })); |
При обработке клика на кнопках, размещенных рядом с полями типа number
, мы будем получать из их data-атрибутов название функции и тех единиц, которые мы изменяем. С помощью конструкции switch...case
сможем вызвать нужную функцию.
Посмотрим на результат:
Экземпляры класса
Если заглянуть под капот нашего класса, то мы увидим, что все методы, которые мы объявили в классе, сейчас записаны в его свойстве __proto__
, т.е. класс основан на прототипном наследовании. Это значит, что любой из экземпляров класса будет иметь доступ к любой из функций, в нем объявленных.
Мы также можем увидеть, что сам класс Clock является функцией, т.к. представляет собой экземпляр класса Function, а любая переменная, объявленная нами ранее является экземпляром класса Clock:
1 2 3 4 |
console.log(Clock instanceof Function); //true console.log(clock6 instanceof Clock); //true console.log(clock6 instanceof Function); //false console.log(clock6 instanceof Object); //true |
Тем не менее, переменная clock6 не является экземпляром класса Function, т.к. представляет собой объект, т.е. принадлежит к классу Object, который лежит в основе всех объектов JavaScript.
В этой статье мы не рассмотрели статические методы, геттеры и сеттеры, а также наследование одного класса другим, однако и данный пример можно брать за основу при создании собственного класса, чтобы освоить синтаксис.
Ниже вы найдете несколько примеров классов с различными часами от одного автора - Jon Kantner. На его страничке на codepen.io таких примеров намного больше. Они интересны еще и с точки зрения визуальной реализации. Посмотрите код - возможно, вам понравится подход автора.
Пример 1
See the Pen #Clocktober Day 27: Wireframe by Jon Kantner (@jkantner) on CodePen.
Пример 2
See the Pen #Clocktober Day 29: Pie by Jon Kantner (@jkantner) on CodePen.
Пример 3
See the Pen #Clocktober Day 2: Scale by Jon Kantner (@jkantner) on CodePen.