Назад Содержание Индекс Вперёд |
Ядро JavaScript 1.5. Руководство по Использованию. |
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.
Остальная часть главы использует иерархию объектов, показанную на рисунке.
В этом примере использованы следующие объекты:
Есть несколько способов определить подходящий конструктор функции для реализации иерархии Employee. Какой из них выбрать, во многом зависит от того, что Ваше приложение должно делать.
В этом разделе показано, как использовать очень простые (и
сравнительно гибкие) определения, чтобы продемонстрировать работу наследования. В
этих определениях Вы не можете специфицировать никаких значений свойств при
создании объекта. Вновь создаваемый объект просто получает значения по умолчанию,
которые Вы позднее можете изменить. На Рисунке 8.2
изображена иерархия с этими простыми определениями.
В реальном приложении Вы, вероятно, определите
конструкторы, которые позволят Вам предоставлять значения свойств во время
создания объекта (см. Более Гибкие Конструкторы). Эти
простые определения демонстрируют появление наследования.
Следующие определения Java и JavaScript для Employee
сходны. Единственным отличием является то, что в Java Вы должны специфицировать тип
каждого свойства, а в JavaScript - нет, и что Вам нужно создать явно
метод-конструктор для Java-класса.
Определения Manager и WorkerBee показывают отличия в
специфицировании более высокого объекта в иерархии. В JavaScriptВы добавляете
прототипичный экземпляр как значение свойства прототипа конструктора функции. Вы
можете сделать это в любое время после определения конструктора. В Java Вы
специфицируете суперкласс в определении класса. Вы не можете изменить суперкласс
вне определения класса.
Определения Engineer и SalesPerson создают объекты,
которые происходят от WorkerBee и, следовательно, от Employee. Объект этих типов
имеет свойства всех объектов, стоящих выше него в цепи иерархии. Кроме того, эти
определения переопределяют наследуемое значение свойства dept новым значением, специфичным для объекта.
Используя эти определения, Вы можете создать экземпляры
этих объектов, которые получают значения по умолчанию для своих свойств.
Рисунок 8.3 иллюстрирует использование этих определений JavaScript для
создания новых объектов и показывает также значения свойств новых объектов.
Термин instance\экземпляр имеет специфическое техническое значение в языках на базе классов. В этих языках экземпляр является отдельным членом/member класса и фундаментально отличается от класса. В JavaScript "экземпляр" не имеет этого технического значения, поскольку JavaScript не имеет различий между классами и экземплярами. Однако, говоря о JavaScript, "экземпляр" может использоваться неформально, являясь объектом, созданным с использованием определённого конструктора функции. Так, в этом примере Вы можете неформально сказать, что jane это экземпляр объекта Engineer. Аналогично, хотя термины parent\родитель, child\дочерний, ancestor\предок и descendant\потомок не имеют формальных значений в JavaScript, Вы можете использовать их неформально для ссылки на объекты выше или ниже в цепочке прототипов.
В этом разделе рассматривается наследование объектами свойств других объектов в цепи прототипов и что происходит, если Вы добавляете свойство во время прогона программы.
Предположим, Вы создаёте объект 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.
Конструкторы функций не позволяют специфицировать значения свойств при создании экземпляра. Как и в Java, Вы можете предоставлять конструктору аргументы для инициализации значений свойств экземпляров. На рисунке показан один из способов реализации этого.
В таблице даны определения Java и JavaScript для этих объектов.
Эти определения JavaScript используют специальную идиому
для установки значений по умолчанию:
Операция JavaScript "логическое ИЛИ" (||) вычисляет свой
первый аргумент. Если он конвертируется в true, операция возвращает его. Иначе,
операция возвращает значение второго аргумента. Следовательно, эта строка кода
проверяет, имеет ли name используемое значение для свойства name. Если это так,
в this.name устанавливается это значение. В ином случае, в this.name
устанавливается пустая строка. В этой главе используется эта идиома используется
для краткости; однако это может на первый взгляд показаться непонятным.
Имея эти определения при создании экземпляра объекта, Вы
можете специфицировать значения для локально определяемых свойств. Как показано
на
Рисунке 8.5, Вы можете использовать следующий оператор для создания нового Engineer:
jane.name == "";
jane.dept == "general";
jane.projects == [];
jane.machine == "belau"
Заметьте, что с помощью этих определений Вы не можете
специфицировать начальное значение наследуемого свойства, такого как name. Если
Вы не хотите специфицировать начальные значения наследуемых свойств в JavaScript,
Вам нужно добавить дополнительный код в конструктор функции.
Пока что конструктор функции создал общий объект и
специфицировал локальные свойства и значения для нового объекта. Вы можете
заставить конструктор добавить свойства, непосредственно вызывая конструктор
функции для объект, стоящего выше в цепочке прототипов. Следующий рисунок показывает эти новые определения.
Давайте рассмотрим одно из этих определений подробнее. Вот
новое определение конструктора 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");
JavaScript выполняет следующие шаги:
Вы можете подумать, что, имея вызов конструктора 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
предоставляют иерархию и наследование.
В данном разделе обсуждаются некоторые тонкости, неочевидные после предыдущего обсуждения.
Когда Вы осуществляете доступ к свойству объекта, 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 не наследует это новое свойство.