Теперь о создании собственных объектов и основной идее объектно-ориентированного программирования. Тема, конечно, обширная. И сомневаюсь, что в одном посте я её изложу достаточно внятно. Но по меньшей мере проиллюстрирую основные важные вещи.
Объект позволяет программисту группировать функции, а также некоторые общие для разных функций переменные в одном месте - внутри объекта. Т.е. объект можно рассматривать как модуль программы. Соответственно, чтобы не путаться в переменных и функциях, и понимать, что к чему относится, полезно программу разбивать не просто на функции и переменные, а именно на объекты. И выстраивать из вложенных друг в друга объектов разные структуры.
Объекты возможно лишние, когда есть один "сплошной" алгоритм. Там достаточно лишь этот алгоритм написать и использовать внутри него переменные. Но из-за механизма обработки событий программа не может быть одним "сплошным" алгоритмом. Она состоит из множества разных функций, которые приходится создавать для обработки событий стандартных объектов. Поэтому создание собственных объектов и группировка функций внутри них - дело правильное, привносящее в программу стройность и порядок, уменьшающее риски возникновения случайных ошибок.
На самом деле объект отличается от модуля одной важной особенностью. Модуль в программе - это просто кусок кода, в котором собраны вместе описания функций и переменных. Объект - это всегда выделенная область памяти, в которой хранятся переменные. Поэтому может быть много одинаковых объектов - каждый со своим собственным набором переменных, но модуль всегда один, и его переменные глобальные во всей программе.
Создание объекта начинается с придумывания его интерфейса. Интерфейс - это набор операций (функций), которыми должен обладать объект, чтобы быть удобным для решения какой-то задачи.
Например тот же таймер. Он имеет функцию запуска, функцию остановки, переменную с номером и переменную с функцией-обработчиком. Может быть полезно всё это хозяйство оформить в виде объекта, интерфейс которого содержит операции:
- создать таймер,
- назначить таймеру обработчик (или сделать у таймера событие),
- назначить временной промежуток срабатывания,
- включить,
- выключить.
Тогда работа с таким объектом будет включать в себя:
- создание и настройку,
- а затем по потребности лишь вызов включения и выключения.
Вот давай и разберём, как же сделать собственный объект таймера.
Создание объекта обычно занимает несколько строчек: нужно создать пустой объект, назначить ему разные свойства и методы, и только после этого объект будет готов. Т.е. создание объекта - это алгоритм. Такой алгоритм принято записывать внутри функции. Функция, предназначенная специально для создания объекта называется конструктором.
Так повелось ещё с языка Java, что обычные функции в JavaScript называют с маленькой буквы. Но функции-конструкции принято называть с большой буквы. Так сразу видно, что функцией нужно пользоваться как конструктором.
// Конструктор объекта Таймер
function Timer() {
var timer = new Object(); // создали пустой объект
// Назначаем свойства
timer.id = null; // сюда будем записывать номер таймера, полученный от window.setInterval
timer.period = NaN; // здесь должен быть период срабатывания таймера
// Назначаем события
timer.ontick = null; // событие tick - срабатывание таймера
// Назначаем методы
timer.start = function() { // запуск таймера
if(timer.id != null) {
// Если свойство id не null, значит таймер уже запущен.
// Прервём работу программы с ошибкой.
throw new Error("Timer has already started.");
} else if(!(Number(timer.period) > 0)) {
// Если свойство period не число или число меньше или равно 0,
// таймер работать не сможет. Тоже прервём работу программы с ошибкой.
throw new Error("Timer's period is invalid.");
} else if(!(timer.ontick instanceof Function)) {
// Если событие tick не назначено, то и таймер запускать нет смысла.
// Просто ничего не делам - выходим из функции.
return;
} else {
// Наконец, если все проверки прошли, запускаем таймер.
// Назначаем свойство id.
timer.id = window.setInterval(timer.ontick, timer.period);
}
}
timer.stop = function() { // остановка таймера
if(timer.id == null) {
// Если свойство id равно null, значит таймер ещё не запущен.
// Прервём работу программы с ошибкой.
throw new Error("Timer hasn't yet started.");
} else {
// Если прошли все проверки, останавливаем таймер.
window.clearInterval(timer.id);
// Очищаем свойство id.
timer.id = null;
}
}
return timer;
}
Пользоваться таким объектом можно следующим образом:
var timer = Timer();
timer.period = 5000;
timer.ontick = function() {
alert("Tick");
}
timer.start();
Вдумчивый программист спросит: а как так получается, что внутри функции start в таймере видна переменная timer, которая вовсе не является собственной переменной функции start?
Это происходит благодаря особому механизму языка JavaScript, называющемуся замыканием функции. Если одну функцию вложить внутрь другой, то все переменные внешней функции будут сохраняться в памяти до тех пор, пока может вызываться внутренняя функция. Если внутренняя функция вызывается несколько раз, то все переменные внешней функции ей будут будут видны всё теми же - запомненными с первого вызова внешней функции.
Например, у нас есть, с одной стороны, событие одного объекта x.onevent, с другой стороны метод другого объекта y.f. Как их связать таким образом, чтобы на событие откликался метод объекта? Решением является как раз замыкание:
function delegateEventToF(x, y) {
x.onevent = function() {
y.f();
}
}
delegateEventToF(x, y);
Здесь вызывается 1 раз внешняя функция delegateEventToF, которая назначает обработчик событию. Параметры x, y которой оказываются в замыкании и сохраняются для обработчика события. Поэтому внутри обработчика можно видеть переменную y с объектом, у которого и вызвать соответствующий метод. Обработчик будет срабатывать столько раз, сколько произойдёт событие, а переменная y всякий раз будет всё та же - с тем же объектом.
Этим обстоятельством можно активно пользоваться, комбинируя замыкание и объект в их конструкторе.
В примере выше с объектом таймера мы храним свойства id и period внутри объекта, потому что они нужны для работы методов start и stop. Если хорошо подумать, свойство id внутри объекта в общем-то и не нужно. А вдруг его по ошибке "снаружи" кто-то поменяет? Его хорошо бы убрать из объекта, но для методов оно нужно. Как раз при помощи замыкания мы его можем сделать переменной внутри конструктора, но оно останется видимым для методов объекта. "Упрятывание" некоторых вспомогательных полей и функций внутри объекта называется инкапсуляцией. В общем-то, чем больше спрятано, тем меньше возможностей при работе с объектом совершить ошибку или неправильно понять, как этим объектом пользоваться. Т.е. в идеале в объекте должны оставаться только методы и события, и только самые нужные. Весь вспомогательный код должен быть инкапсулирован внутри объекта - в замыкании.
Так даже свойство period не очень-то смотрится в объекте таймера. Например, оно важно только при запуске таймера. Но если мы его поменяем во время работы таймера, ничего не произойдёт, хотя можно было бы предполагать, что таймер немедленно среагирует на такую перемену и изменит свой период срабатывания. Т.е. поведение этого свойства для внешнего использования выглядит неочевидным. Разумнее сделать специальный метод setPeriod, который бы организовывал все действия по смене периода.
Ещё можно заметить, что любой конструктор, получается, должен иметь строчки вида:
var obj = new Object();
// настройка объекта
return obj;
Для этого в языке JavaScript предусмотрена специальное правило вызова конструктора - оператор new. Он сразу автоматически создаёт пустой объект, который становится доступен внутри конструктора при помощи служебного слова this. И даже return делать не надо.
В соответствии со всем вышесказанным окончательно наш объект таймера будет описываться следующим образом:
function Timer() {
// Свойства инкапсулированы и находятся в замыкании.
// Договоримся все переменные замыкания начинать с подчёркивания,
// чтобы сразу их видеть.
var _id = null;
var _period = NaN;
// Назначаем события
this.ontick = null;
// Назначаем методы
this.start = function() {
if(_id != null) {
throw new Error("Timer has already started.");
} else if(!(Number(_period) > 0)) {
throw new Error("Timer's period is invalid.");
} else if(!(this.ontick instanceof Function)) {
return;
} else {
_id = window.setInterval(this.ontick, _period);
}
}
this.stop = function() {
if(_id == null) {
throw new Error("Timer hasn't yet started.");
} else {
window.clearInterval(_id);
_id = null;
}
}
this.setPeriod = function(milliseconds) {
if(!(Number(milliseconds) > 0)) {
throw new Error("Argument 'milliseconds' is invalid.");
} else {
_period = milliseconds;
if(_id != null) {
// После смены периода, если таймер работает,
// автоматически перезапускаем с новым периодом.
this.stop();
this.start();
}
}
}
}
Ну и использование объекта таймера:
var timer = new Timer();
timer.setPeriod(5000);
timer.ontick = function() {
alert("Tick");
}
timer.start();
Отсюда несложно догадаться, что стандартные функции Array, Number, Function, Date, Error, String, Object и т.д. попросту являются функциями-конструкторами. Чтобы создать соответствующие объекты, их достаточно вызвать вместе с оператором new.
Любая функция-конструктор может вызываться в программе много раз и создавать много однотипных (одинаково устроенных) объектов. Хотя эти объекты и независимы друг от друга, живут каждый своей самостоятельной жизнью, каждый хранит свои собственные уникальные данные. По этой причине функция-конструктор может рассматриваться внутри языка JavaScript как аналог класса в других языках, таких как та же Java или даже PHP.
Любой объект хранит внутри себя информацию о том, при помощи какой функции-конструктора он был создан. У любого объекта есть свойство constructor, которое как раз содержит функцию-конструктор этого объекта. Ещё есть специальное выражение instanceof, которое позволяет проверить, создавался ли тот или иной объект с помощью того или иного конструктора. Например, в таймере есть строчка:
this.ontick instanceof Function
здесь проверяется, является записанное в событие значение объектом, созданным конструктором функции (т.е. является ли этот объект функцией). Выражение даёт логический ответ true - да или false - нет и обычно используется в условиях.
Ещё есть оператор typeof, который позволяет узнать, какой встроенный простой тип имеет объект: число, строчка, объект (созданный любым определённым программистом конструктором или Object), дата, ошибка, массив и т.д. - включая даже undefined. При этом null имеет тип object. Этот оператор возвращает в результате строчку. Например, безопасная проверка на undefined:
var x;
if(typeof x == "undefined")
Разумеется, объектно-ориентированное программирование в JavaScript имеет множество других нюансов, например, прототипы, которых мы не касались. Но не думаю, что это понадобится, поэтому углубляться не буду. Важно запомнить:
- зачем вообще в программе объекты (чтобы группировать функции и переменные в логические блоки программы);
- интерфейс (какие свойства, события и операции доступны внутри объекта - они должны быть логически продуманными);
- инкапсуляция (что все вспомогательные переменные и функции нужно прятать внутри замыкания конструктора объекта, убирать из интерфейса);
- синтаксические конструкции языка для создания объектов, проверки их "классов" (конструкторов) и т.п.
P.S. Не обсуждавшийся выше оператор throw попросту "валит" программу. Если это "падение" происходит внутри оператора try/catch (упоминавшегося в предыдущих постах) то программа сразу переходит к выполнению оператора catch. Если же нет, то программа "падает" окончательно и больше не работает до полного перезапуска (перезагрузки страницы).