Существует старое высказывание: «Если оно ходит как утка и крякает как утка, значит, это утка!». Перевести этот афоризм на язык JavaScript довольно сложно, однако попробуем: «Если в этом объекте реализованы все методы некоторого класса, значит, это экземпляр данного класса». В гибких языках программиро_ вания со слабой типизацией, таких как JavaScript, это называется «грубым оп_ ределением типа»: если объект обладает всеми свойствами класса X, его можно рассматривать как экземпляр класса X, даже если на самом деле этот объект не был создан с помощью функции_конструктора X().1
Грубое определение типа особенно удобно использовать для классов, «заимст_ вующих» методы у других классов. Ранее в этой главе демонстрировался класс Rectangle, заимствующий метод equals() у класса с именем GenericEquals. В ре_ зультате любой экземпляр класса Rectangle можно рассматривать как экземпляр класса GenericEquals. Оператор instanceof не может определить этот факт, но в на_ ших силах создать для этого собственный метод (пример 9.7).
Пример 9.7. Проверка факта заимствования объектом методов заданного класса
// Возвращает true, если каждый из методов c.prototype был
// заимствован объектом o. Если o – это функция, а не объект,
// вместо самого объекта o производится проверка его прототипа.
// Обратите внимание: для этой функции необходимо, чтобы методы были
// скопированы, а не реализованы повторно. Если класс заимствовал метод,
// а затем переопределил его, данная функция вернет значение false. function borrows(o, c) {
1 Термин «грубое определение типа» появился благодаря языку программирова_ ния Ruby. Точное его название – алломорфизм.
192 Глава 9. Классы, конструкторы и прототипы
// Если объект o уже является экземпляром класса c, можно вернуть true if (o instanceof c) return true;
// Совершенно невозможно выполнить проверку факта заимствования методов
// встроенного класса, поскольку методы встроенных типов неперечислимы.
// В этом случае вместо того, чтобы генерировать, исключение возвращается
// значение undefined, как своего рода ответ "Я не знаю".
// Значение undefined ведет себя во многом похоже на false,
// но может отличаться от false, если это потребуется вызывающей программе. if (c == Array || c == Boolean || c == Date || c == Error ||
c == Function || c == Number || c == RegExp || c == String) return undefined;
if (typeof o == "function") o = o.prototype; var proto = c.prototype;
for(var p in proto) {
// Игнорировать свойства, не являющиеся функциями if (typeof proto[p] != "function") continue;
if (o[p] != proto[p]) return false;
}
return true;
}
Метод borrows() из примера 9.7 достаточно ограничен: он возвращает значение true, только если объект o имеет точные копии методов, определяемых классом c. В действительности грубое определение типа должно работать более гибко: объ_ ект o должен рассматриваться как экземпляр класса c, если содержит методы, напоминающие методы класса c. В JavaScript «напоминающие» означает «имею_ щие те же самые имена» и (возможно) «объявленные с тем же количеством аргу_ ментов». В примере 9.8 демонстрируется метод, реализующий такую проверку.
Пример 9.8. Проверка наличия одноименных методов
// Возвращает true, если объект o обладает методами с теми же именами
// и количеством аргументов, что и класс c.prototype. В противном случае
// возвращается false. Генерирует исключение, если класс с принадлежит
// встроенному типу с методами, не поддающимися перечислению.
function provides(o, c) {
// Если o уже является экземпляром класса c, он и так будет "напоминать" класс c if (o instanceof c) return true;
// Если вместо объекта был передан конструктор объекта, использовать объект_прототип if (typeof o == "function") o = o.prototype;
// Методы встроенных классов не поддаются перечислению, поэтому
// возвращается значение undefined. В противном случае любой объект
// будет напоминать любой из встроенных типов.
if (c == Array || c == Boolean || c == Date || c == Error ||
c == Function || c == Number || c == RegExp || c == String) return undefined;
var proto = c.prototype;
for(var p in proto) { // Цикл по всем свойствам в c.prototype
// Игнорировать свойства, не являющиеся функциями if (typeof proto[p] != "function") continue;
// Если объект o не имеет одноименного свойства, вернуть false if (!(p in o)) return false;
9.7. Определение типа объекта
// Если это свойство, а не функция, вернуть false if (typeof o[p] != "function") return false;
// Если две функции объявлены с разным числом аргументов, вернуть false. if (o[p].length != proto[p].length) return false;
}
// Если были проверены все методы, можно смело возвращать true. return true;
}
В качестве примера грубого определения типа и использования метода provide() рассмотрим метод compareTo(), описанный в разделе 9.4.3. Как правило, метод compareTo() не предназначен для заимствования, но иногда бывает желательно выяснить, обладают ли некоторые объекты возможностью сравнения с помощью метода compareTo(). С этой целью определим класс Comparable:
function Comparable( ) {} Comparable.prototype.compareTo = function(that) {
throw "Comparable.compareTo() – абстрактный метод. Не подлежит вызову!";
}
Класс Comparable является абстрактным: его методы не предназначены для вы_ зова, он просто определяет прикладной интерфейс. Однако при наличии опреде_ ления этого класса можно проверить, допускается ли сравнение двух объектов:
// Проверить, допускается ли сравнение объектов o и p
// Они должны принадлежать одному типу и иметь метод compareTo() if (o.constructor == p.constructor && provides(o, Comparable)) {
var order = o.compareTo(p);
}
Обратите внимание: обе функции, представленные в этом разделе, borrows() и provides(), возвращают значение undefined, если им передается объект одного из встроенных типов JavaScript, например Array. Сделано это по той простой причине, что свойства объектов_прототипов встроенных типов не поддаются пе_ речислению в цикле for/in. Если бы функции не могли выполнять проверку на принадлежность встроенным типам и возвращать undefined, тогда обнаружилось бы, что встроенные типы не имеют методов, и для них всегда возвращалось бы значение true.
Однако на типе Array следует остановиться особо. Вспомним, что в разделе 7.8 приводилась масса алгоритмов (таких как обход элементов массива), которые прекрасно работают с объектами, не являющимися настоящими массивами, а лишь подобными им. Метод грубого определения типа можно использовать для выяснения, является ли некоторый экземпляр объектом, напоминающим массив. Один из вариантов решения этой задачи приводится в примере 9.9.
Пример 9.9. Проверка объектов, напоминающих массивы
function isArrayLike(x) {
if
(x instanceof Array) return true;
//
Настоящий массив
if
(!("length" in x))
return false;
//
Массивы имеют свойство length
if (typeof x.length != "number") return false; // Свойство length должно быть число,
if
(x.length
<
0)
return false;
// причем неотрицательным
if
(x.length
>
0)
{
// Если массив непустой, в нем как минимум должно быть свойство с именем length_1
194 Глава 9. Классы, конструкторы и прототипы
if (!((x.length_1) in x)) return false;
}
return true;
}
Пример: вспомогательный метод defineClass()
Данная глава заканчивается определением вспомогательного метода define_ Class(), воплощающего в себе обсуждавшиеся темы о конструкторах, прототи_ пах, подклассах, заимствовании и предоставлении методов. Реализация метода приводится в примере 9.10.
Пример 9.10. Вспомогательная функция для определения классов
/**
* defineClass() – вспомогательная функция для определения JavaScript_классов.
*
* Эта функция ожидает получить объект в виде единственного аргумента.
* Она определяет новый JavaScript_класс, основываясь на данных в этом
* объекте, и возвращает функцию_конструктор нового класса. Эта функция
* решает задачи, связанные с определением классов: корректно устанавливает
* наследование в объекте_прототипе, копирует методы из других классов и пр.
*
* Объект, передаваемый в качестве аргумента, должен иметь все
* или некоторые из следующих свойств:
*
* name: Имя определяемого класса.
* Если определено, это имя сохранится в свойстве classname объекта_прототипа.
*
* extend: Конструктор наследуемого класса. В случае отсутствия будет
* использован конструктор Object(). Это значение сохранится
* в свойстве superclass объекта_прототипа.
*
* construct: Функция_конструктор класса. В случае отсутствия будет использована новая
* пустая функция. Это значение станет возвращаемым значением функции,
* а также сохранится в свойстве constructor объекта_прототипа.
*
* methods: Объект, который определяет методы (и другие свойства,
* совместно используемые разными экземплярами) экземпляра класса.
* Свойства этого объекта будут скопированы в объект_прототип класса.
* В случае отсутствия будет использован пустой объект.
* Свойства с именами "classname", "superclass" и "constructor"
* зарезервированы и не должны использоваться в этом объекте.
*
* statics: Объект, определяющий статические методы (и другие статические
* свойства) класса. Свойства этого объекта станут свойствами
* функции_конструктора. В случае отсутствия будет использован пустой объект.
*
* borrows: Функция_конструктор или массив функций_конструкторов.
* Методы экземпляров каждого из заданных классов будут
* скопированы в объект_прототип этого нового класса, таким образом
* новый класс будет заимствовать методы каждого из заданных классов.
9.8. Пример: вспомогательный метод defineClass()
* Конструкторы обрабатываются в порядке их следования, вследствие
* этого методы классов, стоящих в конце массива, могут переопределить
* методы классов, стоящих выше.
* Обратите внимание: заимствуемые методы сохраняются
* в объекте_прототипе до того, как будут скопированы свойства
* и методы вышеуказанных объектов.
* Поэтому методы, определяемые этими объектами, могут
* переопределить заимствуемые. При отсутствии этого свойства
* заимствование методов не производится.
*
* provides: Функция_конструктор или массив функций_конструкторов.
* После того как объект_прототип будет инициализирован, данная функция
* проверит, что прототип включает методы с именами и количеством
* аргументов, совпадающими с методами экземпляров указанных классов.
* Ни один из методов не будет скопирован, она просто убедится,
* что данный класс "предоставляет" функциональность, обеспечиваемую
* указанным классом. Если проверка окажется неудачной, данный метод
* сгенерирует исключение. В противном случае любой экземпляр нового класса
* может рассматриваться (с использованием методики грубого определения типа)
* как экземпляр указанных типов. Если данное свойство не определено,
* проверка выполняться не будет.
**/
function defineClass(data) {
// Извлечь значения полей из объекта_аргумента.
// Установить значения по умолчанию. var classname = data.name;
var superclass = data.extend || Object;
var constructor = data.construct || function( ) {}; var methods = data.methods || {};
var statics = data.statics || {}; var borrows;
var provides;
// Заимствование может производиться как из единственного конструктора,