Подробнее об объектной модели

Перевод не завершен. Пожалуйста, помогите перевести эту статью с английского.

JavaScript — это объектно-ориентированный язык, основанный на прототипировании, а не на классах. Из-за этого, является менее очевидным то, каким образом JavaScript позволяет создавать иерархии объектов и обеспечивает наследование свойств и их значений. Эта глава является скромной попыткой прояснить ситуацию.

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

Языки, основанные на классах против Прототипно-ориентированных языков

Основанные на классах объектно-ориентированные языки программирования, такие как Java и C++, строятся на концепции двух отдельных сущностей: класс и экземпляр.

  • Класс определяет все свойства (учитывая методы и все поля в  Java, или свойства в C++), которые характеризуют группу объектов. Класс это абстрактная вещь, а не какой-либо конкретный член множества объектов, которые он описывает. Например, класс Employee может описывать множество всех сотрудников.
  • Экземпляр, это воплощение класса в виде конкретного объекта. Например, Victoria может быть экземпляром класса Employee, представляющее собой конкретное отдельное лицо. Экземпляр класса имеет ровно столько свойств, сколько и родительский класс (не больше и не меньше).

Прототипно-ориентированный язык, например JavaScript, не реализует данное различие: он имеет только объекты. Языки, основанные на прототипах, имеют понятие прототипа объекта — это объект, используемый в качестве шаблона, с целью получить изначальные свойства для нового объекта. Любой объект может иметь собственные свойства, присвоенные либо во время создания, либо во время выполнения. В дополнение, любой объект может быть указан в качестве прототипа для другого объекта, это позволит второму объекту использовать свойства первого.

Определение класса

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

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

Подклассы и наследование

В языках, основанных на классах, вы создаете иерархию классов через объявление классов. В объявлении класса вы можете указать, что новый класс является подклассом уже существующего класса. При этом, подкласс унаследует все свойства суперкласса и в дополнение сможет добавить свои свойства или переопределить унаследованные. Например, предположим, что класс Employee включает два свойства: имя и отдел, а класс Manager является подклассом Employee и добавляет свойство отчеты. В этом случае, экземпляр класса Manager будет иметь три свойства: имя, отдел, и отчеты.

JavaScript реализует наследование, позволяя связать прототипный объект с любой функцией - конструктором. Итак, вы можете создать объект точь-в-точь, как в примере Employee — Manager, но используя несколько иную технику. Для начала нужно определить функцию-конструктор Employee, с которая определяет свойства имя и отдел. Затем, определяем функцию-конструктор Manager, в которой в свою очередь, будет явно вызываться конструктор Employee и определяться новое свойство отчеты. Наконец, присваиваем новый экземпляр Employee, в качестве prototype для функции-конструктора Manager. Теперь, когда вы создадите нового Manager, он унаследует свойства имя и отдела из объекта Employee.

Добавление и удаление свойств

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

Подытожим различия

Следующая таблица дает краткий обзор некоторых из этих различий. А оставшаяся часть этой главы описывает детали использования конструкторов и прототипов JavaScript для создания иерархии объектов и сравнивает это с тем, как вы могли бы сделать это в Java.

Table 8.1 Сравнение языков на основе классов (Java) и на базе прототипов (JavaScript)
Основанные на классах (Java) Основанные на базе прототипов (JavaScript)
Класс и экземпляр являются разными сущностями. Все объекты могут наследовать свойства другого объекта
Определяем класс с помощью определения класса; создаем экземпляр класса с помощью метода-конструктора. Определение и создание объекта происходит с помощью функций-конcтрукторов.
Создание отдельного объекта с помощью оператора new. Так же.
Иерархия объектов строится с помощью определения классов и их подклассов.

Построение иерархии объектов происходит путем присвоения объекта в качестве прототипа функции-конструктора.

Наследование свойств в цепочке классов. Наследование свойств в цепочке прототипов.
Определение класса определяет все свойства всех экземпляров класса. Нельзя динамически добавлять свойства во время выполнения. Функция-конструктор или прототип задает начальный набор свойств. Можно добавить или удалить свойства динамически к отдельным объектам или всей совокупности объектов.

Пример Сотрудник

Оставшаяся часть этой главы объясняет иерархию сотрудников, показанную на следующем рисунке:

Рисунок 8.1: Простая иерархия объектов

Этот пример использует следующие объекты:

  • Employee имеет свойство name (значение которого по умолчанию пустая строка) и dept (значение которого по умолчанию "general").
  • Manager основывается на Employee. Он добавляет свойство reports (значение которого по умолчанию пустой массив, предназначенный для хранения массива объектов Employee).
  • WorkerBee так же основан на Employee. Он добавляет свойство projects (значение которого по умолчанию пустой массив, предназначенный для хранения строк).
  • SalesPerson основан на WorkerBee. Он добавляет свойство quota (значение которого по умолчанию 100). Он также переопределяет свойство dept, со значением "sales", указывая, что все продавцы находятся в одном отделе.
  • Engineer основан на WorkerBee. Он добавляет свойство machine (значение которого по умолчанию пустая строка), а так же определяет свойство dept значением "engineering".

Создание иерархии

Известно несколько способов определить подходящие функции-конструкторы, которые раелизуют иерархию Employee. Выбор способа определения в большей степени зависит от того, на что рассчитано ваше приложение.

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

В реальном приложении, вы, вероятно, будете определять конструкторы, которые позволяют устанавливать нужные вам значения свойств во время создания объекта (см Более гибкие конструкторы). В данном же случае конструкторы упрощены сознательно, для того чтобы сфокусироваться на сути наследования.

Следующие определения Employee для языков Java и JavaScript довольно похожи. Единственное отличие состоит в том, что вам необходимо указать тип каждого свойства в Java, но не в JavaScript (потому что Java является строго типизированным языком, в то время как JavaScript слабо типизированный).

JavaScript Java
function Employee() {
  this.name = "";
  this.dept = "general";
}
public class Employee {
   public String name = "";
   public String dept = "general";
}

Определения классов Manager и WorkerBee показывают разницу в определении вышестоящего объекта в цепочке наследования. В JavaScript вводится связующий объект (прототипный экземпляр), который присваивается в качестве значения свойству prototype функции-конструктора. Вы можете сделать это в любое время после того, как вы создали конструктор. В Java, необходимо указать суперкласс внутри определения класса. Вы не можете изменить суперкласс вне определения класса.

JavaScript Java
function Manager() {
  Employee.call(this);
  this.reports = [];
}
Manager.prototype = Object.create(Employee.prototype);
//создаем пустой объект с прототипом от коструктора Employee
//и используем этот объект как прототип для Manager

function WorkerBee() {
  Employee.call(this);
  this.projects = [];
}
WorkerBee.prototype = Object.create(Employee.prototype);
public class Manager extends Employee {
   public Employee[] reports = new Employee[0];
}

public class WorkerBee extends Employee {
   public String[] projects = new String[0];
}

Классы Engineer и SalesPerson создают объекты, которые происходят от WorkerBee и, следовательно, от Employee. Объект этих типов имеет свойства всех объектов, расположенных над ним в иерархии. Также, эти классы переопределяют наследуемое значение свойства dept своими значениями, характерными для этих объектов.

JavaScript Java
function SalesPerson() {
   WorkerBee.call(this);
   this.dept = "sales";
   this.quota = 100;
}
SalesPerson.prototype = Object.create(WorkerBee.prototype);

function Engineer() {
   WorkerBee.call(this);
   this.dept = "engineering";
   this.machine = "";
}
Engineer.prototype = Object.create(WorkerBee.prototype);
public class SalesPerson extends WorkerBee {
   public double quota;
   public dept = "sales";
   public quota = 100.0;
}

public class Engineer extends WorkerBee {
   public String machine;
   public dept = "engineering";
   public machine = "";
}
   

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

Примечание: Термин экземпляр имеет специфическое значение в языках на базе классов. В этих языках экземпляр — это индивидуальная сущность определенного класса и принципиально отличается от класса. В JavaScript «экземпляр» не имеет такого технического значения, потому что JavaScript не делает таких отличий между классами и экземплярами. Однако, в разговоре о JavaScript, термин «экземпляр» может неформально использоваться для обозначения объекта, созданного с использованием конкретной функции конструктора. Так, в этом примере, вы можете неформально сказать, что jane является экземпляром Engineer. Аналогично, хотя термины parent, child, ancestor и descendant (родитель, дети, предок и потомок) не имеют формальных значений в JavaScript, вы можете использовать их неформально для ссылки на объекты выше или ниже в цепочке прототипов.

figure8.3.png
Рисунок 8.3: Создание объектов с простыми определениями

Свойства объекта

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

Наследование свойств

Предположим, вы создаете объект mark в качестве WorkerBee (как показано на Рисунок 8.3) с помощью следующего выражения:

var mark = new WorkerBee;

Когда JavaScript видит оператор new, он создает новый обобщенный объект и передает этот новый объект в качестве значения this в функцию-конструктор WorkerBee. Функция-конструктор явно устанавливает значение свойства projects и неявно устанавливает WorkerBee.prototype значением  внутреннего свойства __proto__. Это свойство имеет  по два символа нижнего подчеркивания в начале и в окончании. Свойство __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 = 3000;

Теперь объект mark имеет свойство bonus, но никакой другой WorkerBee не имеет этого свойства.

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

Employee.prototype.specialty = "none";

Как только JavaScript выполняет это выражение, объект mark также получает свойство specialty со значением "none". Следующий рисунок показывает результат добавления этого свойства в прототип Employee и последующее переопределение его в прототипе Engineer.


Рисунок 8.4: Добавление свойств

Более гибкие конструкторы

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


Рисунок 8.5: Определение свойств в конструкторе, вариант 1

Следующая таблица показывает определения для этих объектов в JavaScript и Java.

JavaScript Java
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 (String name) {
      this(name, "general");
   }
   public Employee (String name, String 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) {
      projects = projs;
   }
}

 
function Engineer (mach) {
   this.dept = "engineering";
   this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;
public class Engineer extends WorkerBee {
   public String machine;
   public Engineer () {
      dept = "engineering";
      machine = "";
   }
   public Engineer (String mach) {
      dept = "engineering";
      machine = mach;
   }
}

В JavaScript эти определения используют специальную идеому для установки значений по умолчанию:

this.name = name || "";

В JavaScript логический оператор ИЛИ (||) оценивает свой первый аргумент. Если этот аргумент преобразуется в true, оператот возвращает его. Иначе, оператор возвращает значение второго аргумента. Следовательно, эта строчка кода проверяет, содержит ли аргумент name значение, пригодное для свойства name. Если так и есть, this.name определяется  с этим значением. В ином случае, значением this.name назначается пустая строка. Эта глава использует такую идиому для краткости; тем не менее, с первого взгляда она может озадачить.

Примечание: Это может работать, не так, как ожидается, если функция-конструктор вызвается с аргументами, которые преобразуются в false, вроде нуля (0) или пустой строки (""). В этом случае будет выбрано значение по умолчанию.

С помощью таких определений, создавая экземпляр объекта, вы можете указать значения для локально определенных свойств. Как показано на Рисунокт 8.5, можно использовать следующее выражение для создания нового Engineer:

var jane = new Engineer("belau");

Свойства созданного объекта jane:

jane.name == "";
jane.dept == "engineering";
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, следующим образом:

var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");

JavaScript выполняет следующие действия:

  1. Оператор new создает обобщенный объект и устанавливает его свойству __proto__ значение Engineer.prototype.
  2. Оператор new передает этот новый объект в конструктор Engineer в качестве значения ключевого слова this.
  3. Конструктор создает новое свойство с именем base для этого объекта и присваивает значение свойства base из конструктора WorkerBee. Это делает конструктор WorkerBee методом объекта, созданного Engineer. Имя свойства base не является специальным словом. Вы можете использовать любое допустимое для свойства имя; base всего-лишь напоминает о предназначении свойства.
  4. Конструктор вызывает метод base, передавая в качестве аргументов два из аргументов, переданных конструктору ("Doe, Jane" и ["navigator", "javascript"]), а также строку "engineering". Явное использование "engineering" в конструкторе указывает на то, что все объекты, созданные Engineer, имеют одинаковое значение для наследуемого свойства dept, это значение переопределяет значение, унаследованное из Employee.
  5. Поскольку base является методом Engineer, внутри вызова base JavaScript привязывает ключевое свойство this к объекту, созданном в шаге 1. Таким образом, функция WorkerBee передает поочередно аргументы "Doe, Jane" and "engineering" в функцию-конструктор Employee. Получив результат из Employee, функция WorkerBee использует оставшийся аргумент для установки значения свойства projects.
  6. После возвращения из метода base, конструктор Engineer инициализирует свойство объекта machine со значением "belau".
  7. После возвращения из конструктора, JavaScript присваивает новый объект переменной jane.

Можно подумать, что вызвав WorkerBee из конструктора Engineer, вы настроили соответствующим образом наследование для объектов, создаваемых Engineer. Это не так. Вызов конструктора WorkerBee обеспечивает только то, что объект Engineer запускается со  свойствами, определенными во всех функциях-конструкторах, которые были вызваны. Так, если позже добавить свойства в прототипы Employee или WorkerBee, эти свойства не унаследуются объектами из Engineer. Например, предположим, вы использовали следующие определения:

function Engineer (name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, "engineering", projs);
  this.machine = mach || "";
}
var 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;
var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
Employee.prototype.specialty = "none";

Теперь свойство specialty объекта jane имеет значение "none".

Другой способ вызвать родительский конструктор в контексте создаваемого объекта это использование методов call() / apply(). Следующие блоки эквивалентны:

function Engineer (name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, "engineering", projs);
  this.machine = mach || "";
}
function Engineer (name, projs, mach) {
  WorkerBee.call(this, name, "engineering", projs);
  this.machine = mach || "";
}

Использование метода call() является более чистой реализацией наследования, так как он не требует создания дополнителного свойства, именованного в примере как base.

Тонкости наследования свойств

В секции выше рассказывалось каким образом конструкторы и прототипы в JavaScript обеспечивают иерархию и наследование. В секции ниже будут затронуты тонкости, которые выше были не так очевидны.

Локальные значения против унаследованных

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

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

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

function Employee () {
  this.name = "";
  this.dept = "general";
}

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

Предположим, на основе конструкции выше, вы создаете обект amy как экземпляр класса WorkerBee следующим выражением:

var 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. Однако, этого не случится.

Когда вы устанавливаете прототип для WorkerBee вы создаете новый объект Employee, таким образом WorkerBee.prototype получает свое собственное локальное свойство name (в данном примере пустую строку). Следовательно, когда JavaScript ищет свойство name у объекта amy (экземпляра WorkerBee), он первым делом натыкается на него в прототипе WorkerBee.prototype, и до проверки Employee.prototype дело не доходит.

Если у вас есть необходимость изменять некоторое свойство объекта во время работы приложения, и применять это изменение на все существующие экземпляры, не нужно создавать это свойтсво внутри конструктора. Вместо этого, добавьте свойство в прототип принадлежащий конструктору. Для примера, предположим вы изменили код, чтоб был показан выше, следующим образом:

function Employee () {
  this.dept = "general";
}
Employee.prototype.name = "";

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

var amy = new WorkerBee;

Employee.prototype.name = "Unknown";

в таком случае свойство name у объекта amy примет значение "Unknown".

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

Разбираемся во взаимосвязи экземпляров

Поск свойств в JavaScript начинается с просмтра самого объекта, и если в нем свойство не найдено, поиск переключается на объект на который указывается ссылка __proto__. Это продолжается рекурсивно и такой процесс поиска называется "поиск в цепочке прототипов".

Специальное свойтсво __proto__ устанавливается автоматически при создании объекта. Оно принимает значение свойства prototype функции-конструктора. Таким образом new Foo() создаст объект для которого справедливо выражение __proto__ == Foo.prototype. В следствие этого, любые изменения свойств у Foo.prototype, оказывают эффект на процесс поиска свойств во всех объектах, созданых при помощи new Foo().

Все объекты (за исключением глобального объекта Object) имеют свойство __proto__. Все функции имеют свойство prototype. Благодаря этому, родственные связи объектов в иерархии могут быть установлены. Вы можете установить родство и происхождение объекта, сравнив его свойство __proto__ со свойством prototype конструктора. Здесь JavaScript предастваляет оператор instanceof как более простой способ проверки, наследуется ли объект от конкретного конструктора. Для примера:

var f = new Foo();
var isTrue = (f instanceof Foo);

Для более детального примера, предположим у вас имеются теже определения что приведены в разделе Inheriting properties. Создадим экземпляр Engineer как показано здесь:

var chris = new Engineer("Pigman, Chris", ["jsd"], "fiji");

Для полученного объекта будут истинными все из следующих выражений:

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) {
   object = object.__proto__;
   while (object != null) {
      if (object == constructor.prototype)
         return true;
      if (typeof object == 'xml') {
        return constructor.prototype == XML.prototype;
      }
      object = object.__proto__;
   }
   return false;
}
Замечание: Реализация выше особым образом обрабатывает тип "xml". Это сделано для того, чтобы обойти особенность представления XML объектов в последних версиях JavaScript. Смотрите описание ошибки баг 634150 если вам интересны детали.

Следующие вызовы функции instanceOf, заданной выше, вернут истинные значения:

instanceOf (chris, Engineer)
instanceOf (chris, WorkerBee)
instanceOf (chris, Employee)
instanceOf (chris, Object)

Но следующее выражение вернет false:

instanceOf (chris, SalesPerson)

Глобальные данные в конструкторах

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

var idCounter = 1;

function Employee (name, dept) {
   this.name = name || "";
   this.dept = dept || "general";
   this.id = idCounter++;
}

Здесь, когда вы создаете новый экземпляр Employee, конструктор присваивает ему все новый и новый ID увеличивая значение глобальной переменной idCounter. Следовательно, при выполнении кода ниже, victoria.id станет равным 1 а harry.id — 2:

var victoria = new Employee("Pigbert, Victoria", "pubs")
var 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;

var mac = new Engineer("Wood, Mac");

Предположим что каждый из конструкторов, тело которого опущено для краткости, содержит вызов конструктора прородителя. Это приведет к тому, что id у объекта mac примет значение 5 вместо ожидаемой единицы.

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

function Employee (name, dept) {
   this.name = name || "";
   this.dept = dept || "general";
   if (name)
      this.id = idCounter++;
}

Когда вы создаете экземпляр Employee в качестве прототипа, вы не предоставляете аргументы в конструктор за ненадобностью. Конструктор выше проверяет наличие аргумента name, и в случае, если значение не указано, идентификатор id объекту не присваивается, а значение глобального счетчика idCounter не увеличивается. Таким образом, для получения униклаьного id становится обязательным указание параметра name при вызове конструктора Employee. С внесенными в пример выше изменениями, mac.id станет равным долгожданной, заветной единице.

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

Некоторые из объектно-ориентированных языков, предоставляют возможность множественного наследования. Когда один объект может унаследовать свойства и методы множества других, несвязанных дуг с другом объектов. В 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;

var 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 этого свойтсва не унаследует.

Метки документа и участники

 Внесли вклад в эту страницу: vherever, Megabyteceer, SedovDP, ndrsrv, NobbsNobby, Saviloff, makdeb, fscholz, esskia, ivan.p
 Обновлялась последний раз: vherever,