Показанная функция-конструктор не даёт возможности создавать экземпляры. Как и в Java, Вы можете предоставлять аргументы конструктору для инициализации значений свойств экземпляров. На рисунке показан один из способов сделать это.
В языках на основе классов Вы обычно создаёте класс на этапе компиляции, а затем инстанциируете экземпляры на этапе компиляции или на этапе прогона. Вы не можете изменить количество или тип свойств класса, после того как Вы его определили. В JavaScript, однако, Вы можете на этапе прогона добавлять или удалять свойства любого объекта. Если Вы добавляете свойство объекту, который используется как прототип для набора объектов, то объекты, для которых данный объект является прототипом, также получают новое свойство.
В JavaScript Вы можете добавить свойства любому объекту на этапе прогона. Вы не ограничены использованием только свойств, предоставленных функцией-конструктором. Для добавления свойств, специфичных для отдельного объекта, Вы присваиваете значение объекту:
mark.bonus = 3000;Теперь объект mark имеет свойство bonus, но другие WorkerBee этого свойства не имеют.
Если Вы добавляете новое свойство объекту, который используется как функция-конструктор, Вы добавляете это свойство всем объекта, наследующим свойства от данного прототипа. Например, Вы можете добавить свойство specialty ко всем employees следующим оператором:
Employee.prototype.specialty = "none";Как только JavaScript выполнит этот оператор, объект mark также получит свойство specialty со значением "none". На рисунке показан эффект от добавления этого свойства прототипу Employee и последующее переопределение его для прототипа Engineer.
JavaScript это язык на базе прототипов, а не на базе классов. Из-за этого в JavaScript менее очевидно, как создаётся иерархия объектов и происходит наследование свойств и их значений. В данной главе делается попытка прояснит этот вопрос.
Предполагается, что Вы уже немного знакомы с JavaScript и что Вы использовали функции JavaScript для создания простых объектов.
В главе имеются следующие разделы:
Языки на базе классов и языки на базе прототипов Пример EmployeeСоздание иерархии Свойства объектаБолее гибкие конструкторы И снова о наследовании свойствПри создании конструкторов Вы должны быть осторожны, если устанавливаете глобальную информацию в конструкторе. Например, у Вас имеется уникальный идентификатор ID, присваиваемый автоматически каждому новому employee. Вы может использовать такое определение для Employee:
var idCounter = 1;function Employee (name, dept) {Теперь, ели Вы создаёте новый Employee, конструктор присваивает ему следующий ID и выполняет инкремент глобального счётчика ID. Поэтому, если Ваш следующий оператор будет таким, как ниже приведённый, victoria.id будет 1, а harry.id будет 2:
victoria = new Employee("Pigbert, Victoria", "pubs")На первый взгляд всё отлично. Однако idCounter увеличивается всякий раз при создании Employee-объекта. Если Вы создаёте всю иерархию Employee, показанную в данной главе, Employee-конструктор вызывается при каждой установке прототипа. Предположим, у Вас имеется такой код:
var idCounter = 1;function Employee (name, dept) {Предположим далее, что отсутствующие здесь определения имеют свойство base и вызывают конструктор выше себя в цепочке конструкторов. Тогда к моменту создания объекта mac mac.id будет 5.
В зависимости от приложения может или может не быть важно, увеличивается ли счётчик на это дополнительное количество раз. Если Вам необходимо точное значение счётчика, одним из возможных решений может быть использование следующего конструктора:
function Employee (name, dept) {Если Вы создаёте экземпляр Employee для использования его в качестве прототипа, Вы не предоставляете аргументы конструктору. Используя данное определение конструктора и не предоставляя аргументов, конструктор не присваивает значение id и не обновляет счётчик. Следовательно, чтобы Employee получил присвоенный id, Вы обязаны специфицировать имя employee. В данном примере, mac.id может быть 1.
В предыдущем разделе показано, как реализуются иерархия и наследование в конструкторах и прототипах JavaScript.
В данном разделе обсуждаются некоторые тонкости, не очевидные при предыдущем рассмотрении.
Когда Вы выполняете доступ к свойству объекта, JavaScript выполняет следующие шаги, как уже было показано в этой главе:
Проверяется, существует ли значение локально. Если да, это значение возвращается. Если локального значения нет, проверяется цепочка прототипов (через использование свойства __proto__). Если объект в цепочке прототипов имеет значение для специфицированного свойства, это значение возвращается.Если такое свойство не найдено, объект не имеет данного свойства.Результат выполнения этих шагов зависит от того, каковы были определения.
Оригинальный пример имел такие определения:
С помощью этих определений Вы, предположим, создаёте amy как экземпляр WorkerBee таким оператором:
amy = new WorkerBee;Объект amy имеет одно локальное свойство, projects. Значения свойств name и dept не являются локальными по отношению к amy и поэтому получены из свойства __proto__ объекта amy.
Таким образом, amy имеет следующие значения свойств:
Теперь предположим, что Вы изменяете значение свойства 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 () {Теперь свойство name объекта amy становится "Unknown".
Как видно из этих примеров, если Вы хотите иметь значения по умолчанию для свойств объекта и хотите иметь возможность изменять значения по умолчанию на этапе прогона программы, Вы должны устанавливать свойства в прототипе конструктора, а не в самой функции-конструкторе.
Предположим, Вы создаёте объект mark как WorkerBee, как показано на Рисунке 8.3, следующим оператором:
mark = new WorkerBee;Когда JavaScript видит операцию new, он создаёт новый родовой/generic объект и передаёт этот новый объект как значение ключевого слова this функции-конструктору WorkerBee. Функция-конструктор явным образом устанавливает свойства projects. Она также устанавливает в значение внутреннего свойства __proto__ значение WorkerBee.prototype. (Имя этого свойства имеет по два символа подчёркивания в начале и в конце.) Свойство __proto__ определяет цепочку прототипов, используемую для возвращения значения свойства. После установки этих свойств JavaScript возвращает новый объект, и операция присвоения устанавливает переменную mark в этот объект.
Этот процесс не помещает явным образом значения в объект mark (локальные значения) для свойств, которые mark наследует из цепочки прототипов. Когда Вы запрашиваете значение свойства, JavaScript сначала проверяет, существует ли значение в данном объекте. Если значение существует, оно возвращается. Если значение локально отсутствует, JavaScript проверяет цепочку прототипов (используя свойство __proto__). Если объект в цепочке прототипов имеет значение для этого свойства, возвращается это значение. Если такое свойство не найдено, JavaScript сообщает, что такого свойства у объекта нет. Отсюда, объект mark имеет следующие свойства и значения:
mark.name = "";Объект mark наследует значения свойств name и dept из объекта-прототипа в mark.__proto__. Локальное значение свойства projects присваивается конструктором WorkerBee. Это даёт Вам наследование свойств и их значений в JavaScript. Некоторые тонкости этого процесса обсуждаются в разделе "И Снова о Наследовании Свойств".
Поскольку эти конструкторы не предоставляют поддержки значений, специфичных для экземпляра, эта информация является общей/generic. Значения свойств являются значениями по умолчаниями, используемыми всеми новыми объектами, создаваемыми из WorkerBee. Вы можете, конечно, изменить значение любого из этих свойств. Так, Вы можете задать специфическую информацию для mark таким образом:
mark.name = "Doe, Mark";Некоторые объектно-ориентированные языки допускают множественное наследование. То есть объект может наследовать свойства и значения из не соотнесённых/unrelated родительских объектов. JavaScript не поддерживает множественное наследование.
Наследование значений свойств возникает на этапе прогона программы, когда JavaScript ищет значение в цепочке прототипов объектов. Поскольку объект имеет единственный ассоциированный прототип, JavaScript не может динамически наследовать от более чем одной цепочки прототипов.
В JavaScript Вы можете иметь функцию-конструктор, вызывающую более одной функции-конструктора внутри себя. Это создаёт иллюзию множественного наследования. Например, рассмотрим такие операторы:
function Hobbyist (hobby) {Предположим, что имеется определение WorkerBee, как ранее в этой главе. Тогда объект dennis имеет такие свойства:
dennis.name == "Doe, Dennis"Итак, dennis получает свойство hobby из конструктора Hobbyist. Однако, если Вы затем добавите свойство в прототип конструктора Hobbyist:
Hobbyist.prototype.equipment = ["mask", "fins", "regulator", "bcd"]объект dennis не унаследует это новое свойство.
В языках на базе классов Вы определяете класс в отдельном определении класса\class definition. Здесь Вы можете специфицировать специальные методы, называемые конструкторами, которые создают экземпляры данного класса. Метод-конструктор может специфицировать начальные значения свойств экземпляров и выполнять иную работу, необходимую на этапе создания экземпляров. Вы используете операцию new вместе с методом-конструктором для создания экземпляров класса.
В JavaScript используется похожая модель, но отсутствует определение класса, отдельное от конструктора. Вместо этого Вы определяете функцию-конструктор для создания объектов с определённым набором начальных значений и свойств. Любая функция JavaScript может использоваться как конструктор. Вы используете операцию new вместе с функцией-конструктором для создания нового объекта.
Возможно, Вы захотите узнать, какие объекты находятся в цепочке прототипов данного объекта, чтобы определить, из какого объекта данный объект наследует свойства. В языках на базе классов Вы можете использовать для этого операцию instanceof. В JavaScript нет instanceof, но Вы сами можете написать такую функцию.
Как сказано в "Наследовании Свойств", если Вы используете операцию new с функцией-конструктором для создания новых объектов, JavaScript устанавливает в свойство __proto__ нового объекта значение свойства prototype функции-конструктора. Вы можете использовать это для проверки цепочки прототипов.
Например, у Вас есть уже показанный ранее набор определений с соответственно установленными прототипами. Создайте объект __proto__ так:
chris = new Engineer("Pigman, Chris", ["jsd"], "fiji");С эти объектом все следующие операторы true:
chris.__proto__ == Engineer.prototype;Имея это, Вы можете написать функцию instanceOf:
function instanceOf(object, constructor) {При таком определении все следующие выражения true:
instanceOf (chris, Engineer)Но следующее выражение будет false:
instanceOf (chris, SalesPerson)В таблице дано краткое резюме по некоторым отличительным особенностям языков. Остальная часть данной главы посвящена деталям использования конструкторов и прототипов языка JavaScript для создания иерархии и делаются сравнения с аналогичными действиями в Java.
В языках на основе классов Вы создаёте иерархию классов через определения классов. В определении класса Вы можете специфицировать, что новый класс является подклассом\subclass уже существующего класса. Подкласс наследует все свойства своего родителя (суперкласса) и может добавлять новые свойства или изменять наследуемые. Например, класс Employee имеет только свойства name и dept, а Manager является подклассом класса Employee и добавляет свойство reports. В этом случае экземпляры класса Manager будут иметь три свойства: name, dept и reports.
В JavaScript реализовано наследование, что даёт возможность ассоциировать объект-прототип с любой функцией-конструктором. Так, Вы можете воспроизвести тот же пример Employee-Manager, но используя несколько иную терминологию. Сначала Вы определяете функцию-конструктор Employee, специфицируя свойства name и dept. Затем Вы определяете функцию-конструктор Manager, специфицируя свойство reports. Наконец, Вы присваиваете новый объект Employee как prototype функции-конструктору Manager. Затем создаёте новый объект Manager, который наследует свойства name и dept из объекта Employee.
В остальной части данной главы используется иерархия employee, показанная на рисунке.
В этом примере используются следующие объекты:
Employee имеет свойства name (значение по умолчанию - пустая строка) и dept (значение по умолчанию - "general"). Manager базируется на Employee. Он добавляет свойство reports (значение по умолчанию - пустой массив, предназначенный для хранения массива Employee-объектов как значений). WorkerBee также основан на Employee. Он добавляет свойство projects (значение по умолчанию - пустой массив, предназначенный для хранения массива строк как значений). SalesPerson базируется на WorkerBee. Он добавляет свойство quota (значение по умолчанию - 100). Также переопределяет свойство dept значением "sales", указывая, что все менеджеры по продажам/salespersons находятся в этом отделе. Engineer базируется на WorkerBee. Он добавляет свойство machine (значение по умолчанию - пустая строка), а также переопределяет свойство dept property значением "engineering".Следующие определения Employee в Java и в JavaScript похожи. Единственное отличие - Вы должны специфицировать тип каждого свойства в 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 Вы добавляете экземпляр-прототип как значение свойства prototype функции-конструктора. Вы можете сделать это в любое время после определения конструктора. В 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 имеет специфическое техническое значение в языках программирования на базе классов. В них экземпляр это отдельный член класса, фундаментально отличающийся от класса. В JavaScript "экземпляр/instance" не имеет этого технического значения, поскольку JavaScript не различает классы и экземпляры. Однако, говоря о JavaScript, "экземпляр" может использоваться неформально для обозначения объекта, созданного с использованием конкретной функции-конструктора. Так, в данном примере, Вы можете неформально сказать, что jane это Engineer-экземпляр. Аналогично, хотя термины parent\родитель, child\дочерний, ancestor\предок и descendant\потомок не имеют формальных значений в 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; } } |
Есть несколько способов создания функции-конструктора для реализации иерархии Employee. Выбор конкретного способа во многом зависит от того, что должно будет делать Ваше приложение.
В данном разделе показано, как использовать очень простое (и сравнительно негибкое) определение для демонстрации создания работающей иерархии. В этих определениях Вы не можете специфицировать значения свойств, когда создаёте объект. Вновь создаваемый объект просто получает все значения по умолчанию, которые Вы можете изменить позднее. Рисунок 8.2 показывает иерархию с несколькими простыми определениями.
В реальном приложении, Вы, вероятно, определите конструктор, который позволит задавать начальные значения свойств на этапе создания объекта (см. "Более Гибкие Конструкторы"). Итак, эти простые определения демонстрируют появление иерархии.
В этом разделе обсуждается, как объекты наследуют свойства других объектов в цепочке прототипов и что происходит при добавлении свойства на этапе прогона.
Класс и экземпляр это разные сущности. | Все объекты являются экземплярами. |
Класс определяется в определении класса; инстанциация (создание экземпляров) производится методами-конструкторами. | Набор объектов создаётся и определяется функциями-конструкторами. |
Одиночный объект создаётся операцией new. | То же самое. |
Иерархия объектов создаётся через использование определения класса для определения подклассов существующих классов. | Иерархия объектов создаётся путём присвоения объекта как прототипа, ассоциированного с функцией-конструктором. |
Свойства наследуются по цепочке классов. | Свойства наследуются по цепочке прототипов. |
Определение класса специфицирует все свойства всех экземпляров данного класса. Свойства нельзя добавлять динамически на этапе прогона. | Функция-конструктор или прототип специфицируют начальный набор свойств. Свойства могут добавляться динамически отдельному объекту или целому набору объектов. |
Объектно-ориентированные языки на базе классов, такие как Java и C++, помогут понять две разных сущности: класс и экземпляр.
Класс определяет все свойства (методы и поля - в Java, члены, то есть свойства, в C++), характеризующие определённый набор объектов. Класс это абстракция, в отличие от конкретного члена набора объектов, который он описывает. Например, класс Employee может представлять набор всех employees/служащих. Экземпляр, с другой стороны, это инстанциация класса; то есть это один из членов класса. Например, Victoria может быть экземпляром класса Employee, представляя конкретную персону как employee. Экземпляр имеет в точности все свойства своего класса-родителя (ни более, ни менее).В языке на базе прототипов, таком как JavaScript, нет такого отличия: здесь просто имеются объекты. В языке на базе прототипов имеется понятие объект-прототип\prototypical object, это объект, используемый как шаблон, по которому получаются начальные свойства объекта. Любой объект может специфицировать свои собственные свойства либо при создании, либо на этапе прогона. Кроме того, любой объект может быть ассоциирован как прототип другого объекта для совместного использования свойств первого объекта.