AMP версия сайта
Электронная библиотека
Программисту веб-дизайнеру
Другие материалы
Ядро JavaScript 1.5. Руководство по Использованию
Глава 8 Объектная Модель. Детали.
JavaScript это объектно-ориентированный язык, базирующийся
на прототипах, а не на классах. Из-за этой разницы базисов менее очевидно то,
что JavaScript позволяет создавать иерархии объектов и наследовать свойства и их значения. В этой главе мы попытаемся прояснить ситуацию.
Мы предполагаем, что Вы уже немного знакомы с JavaScript и использовали функции JavaScript для создания простых объектов.
В главе имеются следующие разделы:
Языки на Базе Классов и Языки на Базе Прототипов
Объектно-ориентированные языки на базе классов, такие как Java
и C++, основаны на концепции двух различных сущностей: классов и экземпляров.
Язык на базе прототипов, такой как JavaScript, не имеет таких различий: в нем просто имеются объекты. Язык на базе прототипов содержит понятие prototypical object\прототипичный объект - объект, используемый как шаблон, из которого получаются начальные свойства для нового объекта. Любой объект может специфицировать свои собственные свойства, либо когда Вы создаете его, либо на этапе прогона. Кроме того, любой объект может быть ассоциирован как прототип для другого объекта, давая другому объекту возможность использовать свойства первого объекта.
Определение Класса
В языках на базе классов Вы определяете класс в отдельном определении класса. В этом определении Вы можете специфицировать специальные методы, называемые конструкторами, которые служат для создания экземпляров класса. Конструктор метода может специфицировать начальные значения для свойств экземпляров и выполнять другую обработку на этапе создания. Вы используете оператор new вместе с конструктором метода для создания экземпляров класса.
JavaScript следует простой модели, но не имеет определения класса отдельно от его конструктора. Вместо этого Вы определяете конструктор функции для создания объектов с определенным начальным набором свойств и значений. Любая функция JavaScript может использоваться как конструктор. Вы используете оператор new вместе с конструктором функции для создания новых объектов.
Подклассы и Наследование
В языках на базе классов Вы создаете иерархию классов через определения классов. В определении класса Вы можете специфицировать, что новый класс является subclass\подклассом уже существующего класса. Подкласс наследует все свойства суперкласса (родительского) и может дополнительно вводить новые свойства и модифицировать унаследованные. Например, предположим, что класс Employee имеет только свойства name и dept и что Manager является подклассом от Employee, добавляющим свойство reports. В этом случае экземпляр класса Manager будет иметь все три свойства: name, dept и reports.
JavaScript реализует наследование, позволяя Вам ассоциировать прототипичный объект с любым конструктором функции. Так, Вы можете создать пример Employee-Manager, но используя при этом слегка иную терминологию. Во-первых, Вы определяете конструктор функции Employee, специфицируя свойства name и dept. Затем Вы определяете конструктор функции Manager, специфицируя свойство reports. Наконец, Вы присваиваете новый Employee-объект как прототип конструктору функции Manager. После этого, когда Вы создаете новый Manager-объект, он наследует it свойства name и dept от объекта Employee.
Добавление и Удаление Свойств
В языках на базе классов Вы обычно создаете класс на этапе компиляции и затем создаете экземпляры класса на этапе компиляции или на этапе прогона программы. Вы не можете изменить количество или типы свойств класса после того, как Вы определили этот класс. В JavaScript, напротив, на этапе прогона Вы можете добавлять и удалять свойства любого объекта. Если Вы добавляете свойство к объекту, который используется как прототип для набора объектов, эти объекты также получают новое свойство.
Различия. Резюме.
В таблице дано краткое резюме по некоторым отличиям.
Остальная часть этой главы описывает детали использования конструкторов и
прототипов JavaScript для создания иерархии объектов и сравнивает их с теми же
процессами в Java.
На базе классов (Java) На базе прототипов (JavaScript)
Класс и экземпляр класса являются разными сущностями. --- Все объекты являются экземплярами.
Определяет класс в определении класса; инстанциирует класс методами-конструкторами. --- Определяет и создает набор объектов с помощью конструкторов функций.
Создает отдельный объект оператором new. --- То же самое.
Иерархия объектов конструируется путем использования определения класса для определения подклассов существующих классов. --- Иерархия объектов конструируется присвоением объекта как прототипа, ассоциированного с конструктором функции.
Наследует свойства по цепочке классов. --- Наследует свойства по цепочке прототипов.
Определение класса специфицирует все свойства всех экземпляров класса. Свойства не могут добавляться динамически на этапе прогона программы. --- Конструктор функции или прототип специфицирует начальный набор свойств. Свойства могут удаляться и добавляться динамически в отдельных объектах или сразу для набора объектов.
Пример Employee
Рисунок 8.1    Простая иерархия объектов
В этом примере использованы следующие объекты:
Создание Иерархии
Есть несколько способов определить подходящий конструктор функции для реализации иерархии Employee. Какой из них выбрать, во многом зависит от того, что Ваше приложение должно делать.
В этом разделе показано, как использовать очень простые (и сравнительно гибкие) определения, чтобы продемонстрировать работу наследования. В этих определениях Вы не можете специфицировать никаких значений свойств при создании объекта. Вновь создаваемый объект просто получает значения по умолчанию, которые Вы позднее можете изменить. На Рисунке 8.2 изображена иерархия с этими простыми определениями.
В реальном приложении Вы, вероятно, определите конструкторы, которые позволят Вам предоставлять значения свойств во время создания объекта (см. Более Гибкие Конструкторы). Эти простые определения демонстрируют появление наследования.
Рисунок 8.2 Определения Объекта Employee
Следующие определения Java и JavaScript для Employee сходны. Единственным отличием является то, что в Java Вы должны специфицировать тип каждого свойства, а в JavaScript - нет, и что Вам нужно создать явно метод-конструктор для Java-класса.
function Employee () {
this.name = "";
this.dept = "general";
}
public class Employee {
public String name;
public String dept;
public Employee () {
this.name = "";
this.dept = "general";
}
}
Определения Manager и WorkerBee показывают отличия в специфицировании более высокого объекта в иерархии. В JavaScriptВы добавляете прототипичный экземпляр как значение свойства прототипа конструктора функции. Вы можете сделать это в любое время после определения конструктора. В Java Вы специфицируете суперкласс в определении класса. Вы не можете изменить суперкласс вне определения класса.
function Manager () {
this.reports = [];
}
Manager.prototype = new Employee;
function WorkerBee () {
this.projects = [];
}
WorkerBee.prototype = new Employee; ---
public class Manager extends Employee {
public Employee[] reports;
public Manager () {
this.reports = new Employee[0];
}
}
public class WorkerBee extends Employee {
public String[] projects;
public WorkerBee () {
this.projects = new String[0];
}
}
Определения Engineer и SalesPerson создают объекты, которые происходят от WorkerBee и, следовательно, от Employee. Объект этих типов имеет свойства всех объектов, стоящих выше него в цепи иерархии. Кроме того, эти определения переопределяют наследуемое значение свойства dept новым значением, специфичным для объекта.
function SalesPerson () {
this.dept = "sales";
this.quota = 100;
}
SalesPerson.prototype = new WorkerBee;
function Engineer () {
this.dept = "engineering";
this.machine = "";
}
Engineer.prototype = new WorkerBee;
public class SalesPerson extends WorkerBee
{
public double quota;
public SalesPerson () {
this.dept = "sales";
this.quota = 100.0;
}
}
public class Engineer extends WorkerBee {
public String machine;
public Engineer () {
this.dept = "engineering";
this.machine = "";
}
}
Используя эти определения, Вы можете создать экземпляры этих объектов, которые получают значения по умолчанию для своих свойств. Рисунок 8.3 иллюстрирует использование этих определений JavaScript для создания новых объектов и показывает также значения свойств новых объектов.
Термин instance\экземпляр имеет специфическое техническое значение в языках на базе классов. В этих языках экземпляр является отдельным членом/member класса и фундаментально отличается от класса. В JavaScript "экземпляр" не имеет этого технического значения, поскольку JavaScript не имеет различий между классами и экземплярами. Однако, говоря о JavaScript, "экземпляр" может использоваться неформально, являясь объектом, созданным с использованием определенного конструктора функции. Так, в этом примере Вы можете неформально сказать, что jane это экземпляр объекта Engineer. Аналогично, хотя термины parent\родитель, child\дочерний, ancestor\предок и descendant\потомок не имеют формальных значений в JavaScript, Вы можете использовать их неформально для ссылки на объекты выше или ниже в цепочке прототипов.
Рисунок 8.3 Создание Объектов с Помощью Простых Определений
Свойства Объекта
В этом разделе рассматривается наследование объектами свойств других объектов в цепи прототипов и что происходит, если Вы добавляете свойство во время прогона программы.
Наследование Свойств
Предположим, Вы создаете объект mark как экземпляр объекта WorkerBee, как показано на Рисунке 8.3, следующим оператором:
Когда JavaScript встречает оператор new, он создает новый
общий родовой/generic объект и передает этот новый объект как значение ключевого
слова this в конструктор функции WorkerBee. Конструктор функции явно
устанавливает значение свойства projects. Он также устанавливает значение
внутреннего свойства __proto__ в значение WorkerBee.prototype. (Имя этого
свойства содержит два символа подчеркивания в начале и два - в конце.)
__proto__ определяет цепь прототипов, используемую для возвращения значений
свойств. После того как эти свойства установлены, JavaScript возвращает новый
объект, и оператор присвоения устанавливает переменную mark в этот объект.
Этот процесс не помещает явно значения в объект mark (локальные значения) для свойств, которые mark наследует от цепи прототипов. Когда Вы запрашиваете значение свойства, JavaScript сначала проверяет, существует ли значение в этом объекте. Если существует, это значение возвращается. Если локального значения нет, JavaScript проверяет цепь прототипов (используя свойство __proto__). Если объект в цепи прототипов имеет значение для этого свойства, это значение возвращается. Если такое свойство не найдено, JavaScript сообщает, что объект не имеет этого свойства. Таким образом, объект mark имеет следующие свойства и значения:
mark.name = "";
mark.dept = "general";
mark.projects = [];
Объект mark наследует значения свойств name и dept из прототипичного объекта в mark.__proto__. Оно присваивается локальному значению свойства projects конструктором WorkerBee. Это дает Вам наследование свойств и их значений в JavaScript. Некоторые тонкости этого процесса обсуждаются в разделе Повторное Рассмотрение Наследования Свойств.
Поскольку эти конструкторы не позволяют вводить значения, специфичные для экземпляра, эта информация является общей. Значения свойств являются значениями по умолчанию, используемыми всеми новыми объектами, создаваемыми на основе WorkerBee. Вы можете, разумеется, изменять значение любого из этих свойств. Так, Вы можете ввести в mark специфическую информацию:
mark.name = "Doe, Mark";
mark.dept = "admin";
mark.projects = ["navigator"];
Добавление Свойств
В JavaScript Вы можете добавлять свойства любому объекту на этапе прогона программы. Отсутствует ограничение на использование только свойств, предоставленных конструктором функции. Чтобы добавить свойство отдельному объекту, Вы присваиваете значение этому свойству объекта таким образом:
Теперь объект mark имеет свойство bonus, но другие потомки WorkerBee этого свойства не имеют.
Если Вы добавляете новое свойство объекту, который
используется как прототип конструктора функции, вы добавляете это свойство всем
объектам, наследующим свойства от этого прототипа. Например, Вы можете добавить
свойство specialty всем employee с помощью следующего оператора:
Employee.prototype.specialty = "none";
Когда JavaScript выполнит этот оператор, объект mark также получит свойство specialty со значением "none". На рисунке показано эффект от добавления этого свойства прототипу Employee и последующего переопределения этого свойства для прототипа Engineer.
Рисунок 8.4 Добавление Свойств
Более Гибкие Конструкторы
Конструкторы функций не позволяют специфицировать значения свойств при создании экземпляра. Как и в Java, Вы можете предоставлять конструктору аргументы для инициализации значений свойств экземпляров. На рисунке показан один из способов реализации этого.
Рисунок 8.5 Специфицирование свойств в конструкторе, шаг 1
В таблице даны определения Java и JavaScript для этих объектов.
function Employee (name, dept) {
this.name = name || "";
this.dept = dept || "general";
}
public class Employee {
public String name;
public String dept;
public Employee () {
this("", "general");
}
public Employee (name) {
this(name, "general");
}
public Employee (name, dept) {
this.name = name;
this.dept = dept;
}
}
function WorkerBee (projs) {
this.projects = projs || [];
}
WorkerBee.prototype = new Employee; ---
public class WorkerBee extends Employee {
public String[] projects;
public WorkerBee () {
this(new String[0]);
}
public WorkerBee (String[] projs) {
this.projects = projs;
}
}
function Engineer (mach) {
this.dept = "engineering";
this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;
public class Engineer extends WorkerBee {
public String machine;
public WorkerBee () {
this.dept = "engineering";
this.machine = "";
}
public WorkerBee (mach) {
this.dept = "engineering";
this.machine = mach;
}
}
Эти определения JavaScript используют специальную идиому для установки значений по умолчанию:
Операция JavaScript "логическое ИЛИ" (||) вычисляет свой первый аргумент. Если он конвертируется в true, операция возвращает его. Иначе, операция возвращает значение второго аргумента. Следовательно, эта строка кода проверяет, имеет ли name используемое значение для свойства name. Если это так, в this.name устанавливается это значение. В ином случае, в this.name устанавливается пустая строка. В этой главе используется эта идиома используется для краткости; однако это может на первый взгляд показаться непонятным.
Имея эти определения при создании экземпляра объекта, Вы можете специфицировать значения для локально определяемых свойств. Как показано на Рисунке 8.5, Вы можете использовать следующий оператор для создания нового Engineer:
jane.name == "";
jane.dept == "general";
jane.projects == [];
jane.machine == "belau"
Заметьте, что с помощью этих определений Вы не можете специфицировать начальное значение наследуемого свойства, такого как name. Если Вы не хотите специфицировать начальные значения наследуемых свойств в JavaScript, Вам нужно добавить дополнительный код в конструктор функции.
Пока что конструктор функции создал общий объект и специфицировал локальные свойства и значения для нового объекта. Вы можете заставить конструктор добавить свойства, непосредственно вызывая конструктор функции для объект, стоящего выше в цепочке прототипов. Следующий рисунок показывает эти новые определения.
Рисунок 8.6   Специфицирование свойств в конструкторе, шаг 2
Давайте рассмотрим одно из этих определений подробнее. Вот новое определение конструктора Engineer:
function Engineer (name, projs, mach) {
this.base = WorkerBee;
this.base(name, "engineering", projs);
this.machine = mach || "";
}
Предположим, Вы создаете новый Engineer-объект:
jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
Вы можете подумать, что, имея вызов конструктора WorkerBee из конструктора Engineer, Вы установили соответствующее наследование для Engineer-объектов, но это не так. Вызов конструктора WorkerBee гарантирует, что Engineer-объект стартует со свойствами, специфицированными во всех конструкторах функций, которые были вызваны. Однако, если Вы позднее добавите свойства к прототипам Employee или WorkerBee, эти свойства не будут наследоваться Engineer-объектом. Например, мы имеем следующие операторы:
function Engineer (name, projs, mach) {
this.base = WorkerBee;
this.base(name, "engineering", projs);
this.machine = mach || "";
}
jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
Employee.prototype.specialty = "none";
Объект jane не наследует свойство specialty. Вы все еще должны явно установить прототип, чтобы гарантировать динамическое наследование. Предположим, у нас есть такие операторы:
function Engineer (name, projs, mach) {
this.base = WorkerBee;
this.base(name, "engineering", projs);
this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;
jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
Employee.prototype.specialty = "none";
Теперь значение свойства specialty объекта jane установлено в "none".
Повторное Рассмотрение Наследования Свойств
Предыдущие разделы показали, как конструкторы и прототипы JavaScript
предоставляют иерархию и наследование.
В данном разделе обсуждаются некоторые тонкости, неочевидные после предыдущего обсуждения.
Локальные и Наследуемые Значения
Результат выполнения этих шагов зависит от того, как Вы выполняете определения. Оригинал примера имел такие определения:
function Employee () {
this.name = "";
this.dept = "general";
}
function WorkerBee () {
this.projects = [];
}
WorkerBee.prototype = new Employee;
Имея эти определения, создадим amy как экземпляр объекта WorkerBee следующим оператором:
Объект amy имеет одно локальное свойство, projects. Значения свойств name и dept не являются локальными для amy и поэтому получены из свойства __proto__ объекта amy. Таким образом, amy имеет следующие значения свойств:
amy.name == "";
amy.dept = "general";
amy.projects == [];
Теперь предположим, что Вы изменили значение свойства name
в прототипе, ассоциированном с Employee:
Employee.prototype.name = "Unknown"
На первый взгляд, можно ожидать, что новое значение будет распространено на все экземпляры Employee, однако это не так.
Если Вы создаете любой экземпляр объекта Employee, этот экземпляр получает локальное значение свойства name (пустую строку). Это означает, что, если Вы устанавливаете прототип WorkerBee через создание нового Employee-объекта, WorkerBee.prototype имеет локальное значение для свойства name. Следовательно, когда JavaScript видит свойство name объекта amy (экземпляра WorkerBee), JavaScript находит локальное значение этого свойства в WorkerBee.prototype. Он, следовательно, не просматривает далее цепь Employee.prototype.
Если Вы хотите изменить значение свойства объекта на этапе прогона программы и имеете новое значение, наследуемое всеми потомками объекта, Вы не можете определить это свойство в конструкторе функции объекта. Вместо этого Вы добавляете его к ассоциированному с конструктором прототипу. Например, предположим, Вы изменяете предыдущий код таким образом:
function Employee () {
this.dept = "general";
}
Employee.prototype.name = "";
function WorkerBee () {
this.projects = [];
}
WorkerBee.prototype = new Employee;
Employee.prototype.name = "Unknown";
В этом случае свойство name объекта amy стало "Unknown".
Как показывают все эти примеры, если Вы хотите иметь значения по умолчанию для свойств объекта и иметь возможность изменять эти значения по умолчанию на этапе прогона программы, Вы должны установить свойства прототипа конструктора, а не сам конструктор функции.
Определение Взаимодействия Экземпляров
Вам, возможно, понадобится знать, какие объекты находятся в цепочке прототипов для данного объекта, чтобы знать, из каких объектов данный объект наследует свойства.
Начиная с JavaScript версии 1.4, JavaScript предоставляет операцию instanceof для тестирования цепочки прототипов. Эта операция работает точно так же, как функция instanceof, рассматриваемая ниже.
Как уже говорилось в Наследовании Свойств, если Вы используете оператор new и конструктор функции для создания нового объекта, JavaScript устанавливает в свойство __proto__ нового объекта значение свойства prototype конструктора функции. Вы можете использовать это для проверки цепи прототипов.
Например, предположим, у Вас есть уже рассмотренный ранее
набор определений с прототипами, установленными соответствующим образом.
Создайте объект __proto__ таким образом:
chris = new Engineer("Pigman, Chris", ["jsd"], "fiji");
С этим объектом все следующие операторы будут true:
chris.__proto__ == Engineer.prototype;
chris.__proto__.__proto__ == WorkerBee.prototype;
chris.__proto__.__proto__.__proto__ == Employee.prototype;
chris.__proto__.__proto__.__proto__.__proto__ == Object.prototype;
chris.__proto__.__proto__.__proto__.__proto__.__proto__ == null;
Имея это, Вы можете написать функцию instanceOf:
function instanceOf(object, constructor) {
while (object != null) {
if (object == constructor.prototype)
return true;
object = object.__proto__;
}
return false;
}
При таком определении все следующие выражения будут true:
instanceOf (chris, Engineer)
instanceOf (chris, WorkerBee)
instanceOf (chris, Employee)
instanceOf (chris, Object)
instanceOf (chris, SalesPerson)
Глобальная Информация в Конструкторах
Когда Вы создаете конструкторы, нужно проявлять осторожность при установке глобальной информации в конструкторе. Например, предположим, Вы хотите автоматически присваивать уникальный ID каждому новому employee. Вы можете использовать для Employee следующее определение:
function Employee (name, dept) {
this.name = name || "";
this.dept = dept || "general";
this.id = idCounter++;
}
При таком определении, когда Вы создаете новый Employee-объект, конструктор присваивает ему следующий порядковый ID и выполняет затем инкремент глобального счетчика ID. Так, если Ваш следующий оператор будет таким, как ниже, victoria.id будет 1, а harry.id будет 2:
victoria = new Employee("Pigbert, Victoria", "pubs")
harry = new Employee("Tschopik, Harry", "sales")
На первый взгляд - все отлично. Однако idCounter будет увеличиваться каждый раз при создании Employee-объекта. Если Вы создаете всю иерархию Employee, данную в этой главе, конструктор Employee вызывается каждый раз, когда Вы устанавливаете прототип. Предположим, у Вас есть такой код:
function Employee (name, dept) {
this.name = name || "";
this.dept = dept || "general";
this.id = idCounter++;
}
function Manager (name, dept, reports) {...}
Manager.prototype = new Employee;
function WorkerBee (name, dept, projs) {...}
WorkerBee.prototype = new Employee;
function Engineer (name, projs, mach) {...}
Engineer.prototype = new WorkerBee;
function SalesPerson (name, projs, quota) {...}
SalesPerson.prototype = new WorkerBee;
mac = new Engineer("Wood, Mac");
Предположим далее, что отсутствующие здесь определения имеют свойство base и вызывают конструктор, находящийся над ним в цепи прототипов. В этом случае, когда создается объект mac, mac.id будет 5.
В зависимости от приложения, такое излишнее увеличение счетчика может иметь или не иметь значения. Если Вас интересует точное значение счетчика, реализуется еще одно дополнительное решение путем использования следующего конструктора:
function Employee (name, dept) {
this.name = name || "";
this.dept = dept || "general";
if (name)
this.id = idCounter++;
}
Если Вы создаете экземпляр объекта Employee для использования в качестве прототипа, Вы не должны предоставлять аргументы конструктору. Если Вы используете это определение конструктора и не предоставляете аргументы, конструктор не присваивает значение идентификатору id и не обновляет значение счетчика. Следовательно, для того чтобы Employee получил присвоенный id, Вы обязаны специфицировать name для employee. В этом примере, mac.id будет 1.
Нет Множественного Наследования
Некоторые объектно-ориентированные языки разрешают множественное наследование. То есть, объект может наследовать свойства и значения из не связанных между собой родительских объектов. JavaScript не поддерживает множественное наследование.
Наследование значений свойств возникает на этапе прогона программы, когда JavaScript ищет значение по цепочке прототипов объекта. Поскольку объект имеет единственный ассоциированный прототип, JavaScript не может динамически наследовать из более чем одной цепочки прототипов.
В JavaScript Вы можете иметь несколько вызовов одного конструктора функции внутри другого. Это создает иллюзию множественного наследования. Например, рассмотрим следующие операторы:
function Hobbyist (hobby) {
this.hobby = hobby || "scuba";
}
function Engineer (name, projs, mach, hobby) {
this.base1 = WorkerBee;
this.base1(name, "engineering", projs);
this.base2 = Hobbyist;
this.base2(hobby);
this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;
dennis = new Engineer("Doe, Dennis", ["collabra"], "hugo")
Далее предположим, что имеется определение WorkerBee, такое как ранее в этой главе. В этом случае объект dennis имеет три свойства:
dennis.name == "Doe, Dennis"
dennis.dept == "engineering"
dennis.projects == ["collabra"]
dennis.machine == "hugo"
dennis.hobby == "scuba"
Итак, dennis получает свойство hobby от конструктора Hobbyist. Однако предположим, что Вы затем добавляете свойство в прототип конструктора Hobbyist:
Hobbyist.prototype.equipment = ["mask", "fins", "regulator", "bcd"]
Объект dennis не наследует это новое свойство.
Назад Содержание Индекс Вперед