Представлен вариант реализации средств объектно-ориентированного программирования в широко используемом в Интернет-программировани языке сценариев JavaScript. Показано, что для эффективной реализации практически всех концепций объектно-ориентированного программирования таких, как объектные свойства и методы, свойства и методы классов, инкапсуляция, наследование и полиморфизм, достаточно средств, уже имеющихся в языке JavaScript.
Введение. В большинстве объектно-ориентированных языков программирования существует возможность определять классы объектов, а затем создавать отдельные объекты как экземпляры этих классов. Например, можно объявить класс Complex, призванный представлять комплексные числа и выполнять арифметические действия с этими числами, тогда объект Complex представлял бы единственное комплексное число и мог бы создаваться как экземпляр этого класса.
Язык JavaScript не обладает прямой поддержкой классов как другие языки, например C++, Java или C#. Тем не менее в JavaScript обеспечивается возможность определять классы с помощью таких средств, как функции-конструкторы и прототипы объектов.
Основная часть. Хотя JavaScript [1, 2] поддерживает тип данных, который называется объектом, в нем нет формального понятия класса. Это в значительной степени отличает его от классических объектно-ориентированных языков программирования, таких как C++ и Java [3, 4]. Общая черта объектно-ориентированных языков – это их строгая типизация и поддержка механизма наследования на базе классов. По этому критерию JavaScript легко исключить из числа истинно объектно-ориентированных языков. С другой стороны, JavaScript активно использует объекты и имеет особый тип наследования на базе прототипов. Таким образом, можно считать, что JavaScript – это истинно объектно-ориентированный язык, неплохо имитирующий возможности языков на базе классов, таких как C++ и Java. Проведем более формальные параллели между JavaScript и истинными объектно-ориентированными языками на базе классов.
Начнем с того, что определим некоторые базовые термины. Объект – это структура данных, которая содержит различные фрагменты именованных данных, а также может содержать методы для работы с этими фрагментами данных. Объект группирует связанные значения и методы в единый удобный набор, который, как правило, облегчает процесс программирования, увеличивая степень модульности и возможности для многократного использования кода. Объекты в JavaScript могут иметь произвольное число свойств, и свойства могут добавляться в объект динамически. В строго типизированных языках, таких как C++ и Java, это не так. В них любой объект имеет предопределенный набор свойств, а каждое свойство имеет предопределенный тип. Имитируя объектно-ориентированные приемы программирования при помощи JavaScript-объектов, как правило, заранее определяется набор свойств для каждого объекта и тип данных, содержащихся в каждом свойстве.
В C++ и Java класс определяет структуру объекта. Класс точно задает поля, которые содержатся в объекте, и типы данных этих полей. Он также определяет методы для работы с объектом [3]. В JavaScript нет формального понятия класса, но в этом языке приближение к возможностям классов реализуется с помощью конструкторов и объектов-прототипов [2].
Члены класса C++ или Java могут принадлежать одному из четырех основных типов: объектные свойства, объектные методы, свойства класса и методы класса. Неотъемлемыми характеристиками классов являются также инкапсуляция, наследование и полиморфизм.
Объектные свойства. Каждый объект имеет собственные копии объектных свойств. Например, в классе Rectangle любой объект Rectangle имеет свойство width, определяющее ширину прямоугольника. По умолчанию любое свойство объекта в JavaScript является объектным свойством. Однако, чтобы по настоящему имитировать объектно-ориентированное программирование, нужно говорить, что объектные свойства в JavaScript – это те свойства, которые создаются и/или инициализируются функцией-конструктором.
Объектные методы. Объектный метод во многом похож на объектное свойство, за исключением того, что это функция, а не значение. В C++ и Java функции и методы не являются данными, как это имеет место в JavaScript, поэтому в C++ и Java данное различие выражено более четко. Объектные методы вызываются по отношению к определенному объекту, или экземпляру. Метод area() класса Rectangle представляет собой объектный метод и вызывается так: a = r.area().
Объектные методы ссылаются на объект, с которым они работают, при помощи ключевого слова this. Объектный метод может быть вызван для любого объекта класса, но это не значит, что каждый объект содержит собственную копию метода, как в случае объектного свойства. Вместо этого объектный метод совместно используется всеми объектами класса. В JavaScript объектный метод определяется путем присваивания функции свойству объекта-прототипа в конструкторе. Так, все объекты, созданные данным конструктором, совместно используют унаследованную ссылку на функцию и могут вызывать ее с помощью приведенного синтаксиса вызова методов.
В C++ и Java область видимости объектных методов включает объект this, поэтому метод area() может быть реализован проще: return width * height.
В JavaScript приходится явно вставлять ключевое слово this перед именами свойств: return this.width * this.height. Если в JavaScript неудобно вставлять this перед каждым именем объектного свойства, можно воспользоваться инструкцией with, например:
Rectangle.prototype.area = function() {
with(this) {
return width*height;
}
}
Свойства класса. Свойство класса в Java или статическое свойство класса C++ – это свойство, связанное с самим классом, а не с каждым объектом этого класса. Независимо от того, сколько создано объектов класса, есть только одна копия каждого свойства класса.
В отличие от объектных свойств, доступных через объект класса, доступ к свойствам класса можно получить через сам класс. Запись Number.MAX_VALUE – это пример обращения к свойству класса в JavaScript. Так как имеется только одна копия каждого свойства класса, свойства класса по существу являются глобальными. Однако их достоинство состоит в том, что они связаны с классом и имеют логичную нишу, позицию в пространстве имен JavaScript, где они вряд ли будут перекрыты другими свойствами с тем же именем. Очевидно, что свойства класса имитируются в JavaScript простым определением свойства самой функции-конструктора. Например, свойство класса Rectangle.UNIT для хранения единичного прямоугольника с размерами 1x1 можно создать так: Rectangle.UNIT = new Rectangle(1, 1). Здесь Rectangle – это функция-конструктор, но поскольку функции в JavaScript представляют собой объекты, можно создать свойство функции точно так же, как свойства любого другого объекта.
Методы класса. Метод класса – это метод, связанный с классом, а не с объектом класса; он вызывается через сам класс, а не через конкретный объект класса. Метод Date.parse() – это метод класса. Он всегда вызывается через объект конструктора Date, а не через конкретный объект класса Date.
Поскольку методы класса вызываются через функцию-конструктор, они не могут использовать ключевое слово this для ссылки на какой-либо конкретный объект класса, поскольку в данном случае this ссылается на саму функцию-конструктор. Обычно ключевое слово this в методах классов вообще не используется.
Как и свойства класса, методы класса являются глобальными. Методы класса не работают с конкретным объектом, поэтому их, как правило, проще рассматривать в качестве функций, вызываемых через класс. Как и в случае со свойствами класса, связь этих функций с классом дает им в пространстве имен JavaScript удобную нишу и предотвращает возникновение конфликтов имен. Для того чтобы определить метод класса в JavaScript, требуется сделать соответствующую функцию свойством конструктора.
Инкапсуляция. Одна из наиболее общих характеристик традиционных объектно-ориентированных языков программирования, таких как C++, заключается в возможности объявления частных (private) свойств класса, обращаться к которым можно только из методов этого класса и недоступных за пределами класса. Распространенная техника программирования, называемая инкапсуляцией данных, заключается в создании частных свойств и организации доступа к этим свойствам только через специальные методы чтения/записи. JavaScript позволяет имитировать такое поведение посредством замыканий, но для этого необходимо, чтобы методы доступа хранились в каждом объекте класса и по этой причине не могли наследоваться от объекта-прототипа.
Следующий фрагмент демонстрирует, как можно добиться этого. Он содержит реализацию класса прямоугольников Rectangle, ширина и высота которых доступны и могут изменяться только путем обращения к специальным методам:
function ImmutableRectangle(w, h) {
this.getWidth = function() { return w; }
this.getHeight = function() { return h; }
}
// Класс может иметь обычные методы в объекте-прототипе
ImmutableRectangle.prototype.area = function() {
return this.getWidth() * this.getHeight();
};
Наследование. В C++, Java и других объектно-ориентированных языках на базе классов имеется явная концепция иерархии классов. Каждый класс может иметь базовый класс, от которого он наследует свойства и методы. Любой класс может быть расширен, т. е. иметь производный класс, наследующий его поведение. JavaScript поддерживает наследование прототипов вместо наследования на базе классов. Тем не менее в JavaScript могут быть проведены аналогии с иерархией классов. В JavaScript класс Object – это наиболее общий класс, и все другие классы являются его специализированными версиями, или производными классами. Все классы наследуют несколько базовых методов класса Object.
В JavaScript объекты наследуют свойства от объекта-прототипа их конструктора. Объект-прототип сам представляет собой объект; он создается с помощью конструктора Object(). Это значит, что объект-прототип наследует свойства от Object.prototype. Поэтому, например, объект класса комплексных чисел Complex наследует свойства от объекта Complex.prototype, который в свою очередь наследует свойства от Object.prototype. Когда выполняется поиск некоторого свойства в объекте класса Complex, сначала выполняется поиск в самом объекте. Если свойство не найдено, поиск продолжается в объекте Complex.prototype. И наконец, если свойство не найдено и в этом объекте, выполняется поиск в объекте Object.prototype. Поскольку в объекте-прототипе Complex поиск происходит раньше, чем в объекте-прототипе Object, свойства объекта Complex.prototype скрывают любые свойства с тем же именем из Object.prototype. Так, если в объекте Complex.prototype переопределить метод toString(), определение которого уже имеется в Object.prototype, то объекты Complex никогда не увидят последнее, поскольку определение toString() в Complex.prototype будет найдено раньше.
Типичным для программирования на JavaScript являются классы, производные непосредственно от класса Object; обычно в создании более сложной иерархии классов нет никакой необходимости. Однако когда это требуется, можно создать производный класс любого другого класса. Предположим, требуется создать производный класс от класса Rectangle, чтобы добавить в него свойства и методы, связанные с координатами прямоугольника. Для этого просто нужно быть уверенным, что объект-прототип нового класса сам является объектом Rectangle и потому наследует все свойства Rectangle.prototype. Пример создания производного класса JavaScript ниже повторяет определение простого класса Rectanle и затем расширяет это определение за счет создания нового класса PositionedRectangle:
// Базовый класс простых прямоугольников.
function Rectangle(w, h) {
this.width = w;
this.height = h;
}
Rectangle.prototype.area = function() { return this.width * this.height; }
// Производный класс позиционированных прямоугольников
function PositionedRectangle(x, y, w, h) {
Rectangle.call(this, w, h);
this.x = x;
this.y = y;
}
PositionedRectangle.prototype = new Rectangle();
// В новом классе не нужно наследовать свойства width и height
delete PositionedRectangle.prototype.width;
delete PositionedRectangle.prototype.height;
// Нужно, чтобы PositionedRectangle ссылался на другой конструктор
PositionedRectangle.prototype.constructor = PositionedRectangle;
// К прототипу производного класса добавляется объектный метод
PositionedRectangle.prototype.contains = function(x, y) {
return (x > this.x && x < this.x + this.width &&
y > this.y && y < this.y + this.height);
}
Создание производных классов в JavaScript выглядит более сложным, чем наследование от класса Object. Первая проблема связана с необходимостью вызова конструктора базового класса из конструктора производного класса, причем конструктор базового класса нужно вызывать как метод вновь созданного объекта. Затем подменяется конструктор объекта-прототипа производного класса. Этот объект-прототип создается как объект базового класса, после чего изменяется свойство constructor объекта-прототипа. Пришлось также удалить свойства, которые создаются конструктором базового класса в объекте-прототипе, поскольку очень важно, чтобы свойства объекта-прототипа наследовались из нужного прототипа.
Имея такое определение класса PositionedRectangle, его можно использовать в программах примерно так:
var r = new PositionedRectangle(2, 2, 2, 2);
print(r.contains(3, 3)); // Вызывается добавленный объектный метод
print(r.area()); // Вызывается унаследованный объектный метод
print(r.x + ", " + r.y + ", " + r.width + ", " + r.height); // Работа с полями
// Созданный объект может рассматриваться как объект всех 3 классов
print(r instanceof PositionedRectangle &&
r instanceof Rectangle &&
r instanceof Object);
Полиморфизм. Полиморфизм – это возможность использовать одну и ту же инструкцию для выполнения любого из многих различных действий. В C++ и Java действие, которое выполняется в данный момент, определяется типом объекта, от имени которого производится действие. Полиморфизм в C++ реализуется с помощью виртуальных функций, вызываемых из объектов, ссылающихся с помощью указателей на базовый класс [3].
В JavaScript из-за слабой типизации все объектные переменные можно рассматривать как указатели на базовый класс Object [2]. Поэтому для обеспечения полиморфного поведения методов JavaScript достаточно показать возможность переопределять для производных методы базовых классов.
В JavaScript, когда в производном классе определяется метод, имеющий то же самое имя, что и метод базового класса, производный класс переопределяет (overrides) этот метод. Зачастую переопределение методов производится не с целью полной замены, а лишь для того, чтобы расширить их функциональность. Для этого метод должен иметь возможность вызывать переопределенный метод. В определенном смысле такой прием по аналогии с конструкторами можно назвать вызовом методов по цепочке. Однако вызвать переопределенный метод гораздо менее удобно, чем конструктор базового класса. Для демонстрации этого рассмотрим следующий пример. Предположим, что класс Rectangle определяет метод toString() следующим образом:
Rectangle.prototype.toString = function() {
return "[" + this.width + ", " + this.height + "]";
}
Этот метод необходимо переопределить в классе PositionedRectangle, чтобы объекты производного класса могли иметь строковое представление, отражающее значения не только ширины и высоты, но и всех остальных их свойств. С целью достижения большей общности будем обрабатывать значения свойств координат в самом классе, а обработку свойств width и height делегируем базовому классу. Сделать это можно примерно следующим образом:
PositionedRectangle.prototype.toString = function() {
return "(" + this.x + "," + this.y + ") " + // поля этого класса
Rectangle.prototype.toString.apply(this); // вызов надкласса по цепочке
}
Реализация метода toString() базового класса доступна как свойство объекта-прототипа базового класса. Нельзя вызвать метод напрямую – пришлось воспользоваться методом apply(), чтобы указать, для какого объекта вызывается метод. Однако если в PositionedRectangle.prototype добавить свойство superclass, можно сделать так, чтобы этот код не зависел от типа базового класса:
PositionedRectangle.prototype.toString = function() {
return "(" + this.x + ", " + this.y + ") " + // поля этого класса
this.superclass.prototype.toString.apply(this);
}
Свойство superclass может использоваться в иерархии наследования только один раз. Если оно будет задействовано классом и производным от него классом, это приведет к бесконечной рекурсии.
Выводы. Представленные средства объектно-ориентированного программирования показывают, что язык сценариев JavaScript может использоваться для программирования моделей приложений, строящихся по принципу древовидных иерархий объектов предметной области. С использованием подобных объектных моделей строятся, например, пакеты офисных приложений, включая различные текстовые редакторы, электронные таблицы, СУБД и презентационные приложения. И, в заключение, следует упомянуть необходимость использования объектно-ориентированных возможностей в родной среде языка JavaScript, а именно, в веб-браузерах для работы с объектной моделью документа и объектной моделью браузера [5]. Естественно, эффективность такого использования во многом определяется скоростью работы с упомянутыми объектными моделями самих веб-браузеров.
Список литературы:
1. Флэнаган Д. JavaScript. Подробное руководство. – Пер. с англ. – СПб: Символ_Плюс, 2008. – 992 с.
2. Danny Goodman, Michael Morrison, Paul Novitski, Tia Gustaff Rayl. JavaScript Bible: Seventh Edition. - Wiley Publishing, Inc., 2010. – 1186 pp.
3. Щербаков Є.В., Щербакова М.Є. Мовні засоби системного програмування: Навчальний посібник. – Луганськ: Вид-во СНУ ім.. В. Даля, 2006. – 376 с.
4. Щербаков Є.В., Щербакова М.Є., Скарга-Бандурова І.С. Діалогові засоби системного програмування: Навчальний посібник. – Луганськ: Вид-во СНУ ім.. В. Даля, 2010. – 408 с.
5. Jeremy Keith, Jeffrey Sambells. DOM Scripting: Web Design with JavaScript and the Document Object Model: Second Edition. – Apress, 2010. – 337 pp.
ЕКОНОМІЧНІ НАУКИ
УДК 658
Хандій О.О.