Wiki-учебник по веб-технологиям: JavaScript/ПовторноеРассмотрениеНаследованияСвойств ...

Главная | |

Повторное рассмотрение наследования свойств

Оглавление документа

Предыдущие разделы показали, как конструкторы и прототипы JavaScript предоставляют иерархию и наследование.
В данном разделе обсуждаются некоторые тонкости, неочевидные после предыдущего обсуждения.

1. Локальные и наследуемые значения


Когда Вы осуществляете доступ к свойству объекта, JavaScript выполняет следующие шаги, как уже было описано в этой главе ранее:

1. Проверяет, существует ли значение локально. Если так, возвращается это значение.
2. Если локального значения нет, проверяет цепочку прототипов (используя свойство proto).
3. Если объект в цепочке прототипов имеет значение для специфицированного свойства, возвращается это значение.
4. Если такое свойство не найдено, объект не имеет этого свойства.

Результат выполнения этих шагов зависит от того, как вы выполняете определения. Оригинал примера имел такие определения:

function Employee () {
this.name = "";
this.dept = "general";
}
 
function WorkerBee () {
this.projects = [];
}
WorkerBee.prototype = new Employee;


Имея эти определения, создадим amy как экземпляр объекта WorkerBee следующим оператором:

amy = new 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;
 
amy = new WorkerBee;
 
Employee.prototype.name = "Unknown";


В этом случае свойство name объекта amy стало “Unknown”.

Как показывают все эти примеры, если вы хотите иметь значения по умолчанию для свойств объекта и иметь возможность изменять эти значения по умолчанию на этапе прогона программы, вы должны установить свойства прототипа конструктора, а не сам конструктор функции.

2. Определение взаимодействия экземпляров


Вам, возможно, понадобится знать, какие объекты находятся в цепочке прототипов для данного объекта, чтобы знать, из каких объектов данный объект наследует свойства.

Начиная с 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)


Но это выражение будет false:

instanceOf (chris, SalesPerson)


3. Глобальная информация в конструкторах


Когда вы создаёте конструкторы, нужно проявлять осторожность при установке глобальной информации в конструкторе. Например, предположим, вы хотите автоматически присваивать уникальный ID каждому новому employee. Вы можете использовать для Employee следующее определение:

var idCounter = 1;
 
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 вызывается каждый раз, когда вы устанавливаете прототип. Предположим, у вас есть такой код:

var idCounter = 1;
 
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.

4. Нет множественного наследования


Некоторые объектно-ориентированные языки разрешают множественное наследование. То есть, объект может наследовать свойства и значения из не связанных между собой родительских объектов. 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 не наследует это новое свойство.