|
|
|
Вы можете подумать, что числа и строки объекты более общего вида, чем объекты, непосредственно порожденные классом Object. А давайте посмотрим на методы, им доступные. Разве вы не встречали что-нибудь вроде myString.substring(2,3) или myString.indexOf("test")? Даже числовые объекты могут похвастаться свойствами, например, Number.MAX_VALUE, так же как toString и valueOf. Что же касается старых добрых 5 или 7 - не это ли объекты более общего вида? Снова нет, потому что это не объекты, а литералы. Точно так же и "Hello" и "World". Мы часто слышим, что в ActionScript все есть объект, но это не так. (а функции, активизирующие объект - ведь тоже не объекты, а лишь описания). Числа до тех пор не будут объектом, пока не создадутся с помощью функции Number( ). trace( Number(5).toString() ); // 5 trace( 5.toString() ); // error Со строками то же самое, у них есть метод String(), но они более темные лошадки. Обычные строки автоматически преобразуются в объекты, если они вызывают строковые методы (исключая методы "только для строк" - concat, fromCharCode, slice и substr). В этом случае создается временный объект, метод запускается, затем объект удаляется. trace( "hello".substring(1,4) ); // ell Это делает строки более гибкими в использовании, а знание этого объясняет некоторую тормознутость строковых методов подобных "split" - все должно быть преобразовано в строковые объекты, даже если это уже объект! |
Для осмысления того, что все вещи произошли от Object.prototype, нужно согласиться с тем, что Object всего-навсего класс, особым его делает лишь высокое положение. Когда вы создаете объект, говоря:
x = new Object();
вы просто-напросто создаете объект самого общего вида (generic) в ActionScript (см. примечание выше). Класс Object имеет два встроенных метода в своем прототипе: toString и valueOf. Есть также еще одно свойство, которое мы рассмотрим позже. Все объекты тоже наследуют эти три свойства. Даже если объект порожден другим классом, в каком-то колене он все-равно является потомком Object. Экземпляр проверяет прототип своего класса, тот, в свою очередь, Object.prototype.
Когда вы создаете, например, массив, он будет являться объектом класса Array. У класса Array есть прототип, который обращается к Object.prototype за отсутствующими свойствами. То же самое происходит и с классом Color, Date и любым другим. Методы и свойства, содержащиеся в них, делают их уникальными, однако они могут спокойно делать то, что под силу вышестоящим объектам в их иерархии. Все от того, что, например, объект типа Color привязан не только к Color.prototype, но и к Object.prototype, потому что Color.prototype сам привязан к Object.prototype. Та же ситуация возникнет и с вашими собственными классами:
Object.prototype | ![]() |
Dog.prototype | ![]() |
rover |
Ну ладно, с этим мы разобрались. Давайте рассмотрим сам механизм того, откуда объект узнает о направлении поисков. Эти связи совершаются при помощи __proto__. Это и есть то самое третье свойство Object.prototype и поэтому его можно найти во всех остальных объектах. Задача его проста и сводится к тому, чтобы указывать на следующего вероятного благодетеля. Проще говоря, ему можно дать имя "Спроси-вон-того-если-тебе-что-то-нужно". Итак, имя_объекта.__proto__ указывает на Класс.prototype, этот Класс.prototype тоже объект, обладающий свойством Класс.prototype.__proto__ указывающим на Obejct.prototype. Узнать его вы всегда сможете по четырем знакам подчеркивания.
Открою вам один секрет: __proto__ МОЖНО ИЗМЕНЯТЬ. Вы вольны направить его в любую сторону. Вы можете заставить __proto__ объекта rover указывать на Cat.prototype вместо Dog.prototype и этот разбойник (rover) будет только рад поискать необходимые свойства и методы в новом месте. Причем, таким образом вы можете манипулировать не только подобными объектами. Можно попросить прототип класса порыться в месте, отличном от Object.prototype. Хитрость в том, что нужно сделать два класса и направить __proto__ прототипа первого на прототип другого (вместо Object.prototype). Таким образом вы получите "младший" класс, подкласс (SubClass) и "старший" класс, суперкласс (SuperClass). Делается это простой операцией:
SubClass.prototype.__proto__ = SuperClass.prototype;
SubClass и SuperClass просто имена; так сказать, "дедушку" можно обозвать SuperSuperClass. Если вы создадите новый объект на основе младшего класса SubClass, цепочка будет выглядеть следующим образом:
Object.prototype | ![]() |
SuperClass.prototype | ![]() |
SubClass.prototype | ![]() |
instance |
Еще одна вещь, которую необходимо знать: __proto__ можно использовать несколько раз при подъеме по иерархической лестнице. Так instance.__proto__.__proto__ указывает на SuperClass.prototype. Пусть вас не пугает этот, на первый взгляд, сложный синтаксис - просто посчитайте на рисунке красные стрелочки. Папа, дедушка, прадедушка, прапрадедушка...
Вау, пора привести пример! Давайте начнем с нескольких не связанных между собой классов, а сделать это нужно (зачем - поймем далее). Допустим у нас есть Собака (Dog), Кот (Cat) и Хомяк (Hamster):
// Dog Class Dog = function( name )
{
this.name = name;
}
Dog.prototype.legs = 4;
Dog.prototype.price = 10; Dog.prototype.pet = true; // Cat Class Cat = function( name ) { this.name = name; } Cat.prototype.legs = 4; Cat.prototype.price = 5; Cat.prototype.pet = true; // Hamster Class Hamster = function( name ) { this.name = name; } Hamster.prototype.legs = 4; Hamster.prototype.price = 15; Hamster.prototype.pet = true;
Как видите, здесь присутствуют нежелательные повторения, делающие программу менее гибкой. Что будет, если к оговоренным имени (name), количеству лап (legs), цены (price) и отношению хозяев к своей зверюшке (pet) мы захотим добавить новую категорию, возраст, скажем? Нам бы пришлось забивать его отдельно для каждого зверька, а это неправильно. Мало того, что возрастает объем программы, это в дальнейшем чревато всякими путаницами, ошибками и прочими багами. Что же делать? А давайте посмотрим на код.
Решением может являться создание нового надкласса (superClass) с именем, допустим, "Pet" (Любимец). Всех зверюшек (вы можете добавить своих) породим от этого класса и в него же засунем описание всех этих звериных свойств - имя, количество лап, цену и флажок любим/нелюбим
Ну и совет, скажете вы. Что ж, занимаясь ООП вы поймете, что часто не бывает идеального решения проблемы, особенно если вы не располагаете всей, порой специфической, информацией. К счастью, все можно легко поправить в будущем, если вдруг изменятся требования.
Последнее, что необходимо сделать, это развернуть прототипы Dog, Cat и Hamster в сторону Pet.prototype вместо Object.prototype:
Dog.prototype.__proto__ = Pet.prototype; Cat.prototype.__proto__ = Pet.prototype; Hamster.prototype.__proto__ = Pet.prototype;
Итак, вооружившись всем этим, приступим.
// Pet class Pet = function( name ) { this.name = name; } Pet.prototype.legs = 4; Pet.prototype.pet = true; // Dog class Dog = function( name ){ } Dog.prototype.__proto__ = Pet.prototype; Dog.prototype.price = 10; // Cat class Cat = function( name ){ } Cat.prototype.__proto__ = Pet.prototype; Cat.prototype.price = 5; // Hamster class Hamster = function( name ){ } Hamster.prototype.__proto__ = Pet.prototype; Hamster.prototype.price = 15;
Оставим на время наши попытки и убедимся, что это работает (на самом деле это еще не работает, вы можете заметить проблему?).
rover = new Dog( "Rover" ); fluffy = new Cat( "Fluffy" ); ratboy = new Hamster( "Rat-Boy" ); for(var i in rover){ trace( i + ":\t" + rover[i] ) } /* output pet: true legs: 4 price: 10 */ for(var i in fluffy){ trace( i + ":\t" + fluffy[i] ) } /* output pet: true legs: 4 price: 5 */ for(var i in ratboy){ trace( i + ":\t" + ratboy[i] ) } /* output pet: true legs: 4 price: 15 */
Свойство __proto__ выполнило задачу. Свойства в Pet.prototype (legs и pet) стали доступны для каждого объекта. Объекты также получили доступ к свойствам в своих прототипах (price).
Проблема в том, что свойство name осталось не у дел. Вглядевшись, мы замечаем, что оно ниоткуда недоступно. Что мы сделали не так? Когда мы создавали новый объект, выполнялся код конструктора этого класса. Однако мы засунули свойство name в класс Pet, чтобы не переписывать заново код для каждого животного. Но этот конструктор еще не запускался. Вы можете использовать этот код, однако конструкторы каждого младшего класса останутся пустыми. Есть даже термин для этого - "three-legged-dog" (собака на трех лапах, хромая собака). Необходимо запустить старшие конструкторы, до запуска младших.
Когда программа не работает так, как вам хотелось бы, лучшее, что вы можете сделать - это устранить проблему. Это всегда работает для простейших программ в несколько строчек. Хорошо, как ее устранить:
Несмотря на то, что наши экземпляры наследуют свойства от разных уровней цепочки, запускается только младший конструктор.
... и решается проблема парой строчек кода:
Pet = function( name ) { this.name = name; } Dog = function( name ){ } Dog.prototype.__proto__ = Pet.prototype; rover = new Dog( "Rover" ); // test trace(rover.name); // undefined
Сначала можно просто попытаться запускать конструктор Pet каждый раз, когда создается экземпляр Dog, просто вызывая его из конструктора Dog, правильно? Пока аплодисменты не стали слишком оглушительными, необходимо успеть сказать, что это не работает. Для понимания этого необходимо быть очень внимательными и осторожными, возможно, вновь представить себе объекты в виде черных ящиков. Вот как это может выглядеть:
Dog = function( name ) { Pet( name ); } Dog.prototype.__proto__ = Pet.prototype; rover = new Dog( "Rover" );
Pet() в данном случае вызывается в момент активации экземпляра класса Dog (блок кода под описанием класса). Когда Pet() запускается как метод (не как класс - нигде не видно оператора new), this указывает на то, что его вызывает (в нашем случае экземпляр класса). Как только мы узнаем об активации объектов, мы тут же про них забываем и свойство name пропадает вместе с ними. Что нам нужно сделать, так это запустить конструктор класса Pet, причем из самого экземпляра. Существует два решения проблемы, одно великое и ужасное, другое посложнее, но не такое страшное. Плохо, что нет волшебного суперключевого слова, которое бы нам помогло. Итак, пристегните ремни - первое решение.
Конструктор фактически ничего не знает о каждом конкретном экземпляре, однако он догадывается о том, что с помощью оператора new создается новый безымянный объект. Проще говоря, он "думает", что this указывает на некий объект, который будет создан. |
Мы знаем, что this в конструкторе Dog указывает на его воплощение - rover (см. Примечание). Мы также знаем, что когда объект вызывает метод, this в методе указывает на вызывающий объект. Что нам мешает превратить Pat в метод экземпляра rover и запускать его оттуда? Другими словами, rover будет заключать в себе код конструктора Pet как одно из своих свойств и мы просто попросим rover запустить свой же метод. Давайте скажем это в третий раз: rover съел, проглотил Pet и теперь он внутри него, оттуда мы его и запустим. Теперь, когда метод(!) Pet запустится, this будет указывать на rover, поэтому все свойства, добавленные через this, добавляются к rover. Когда метод Pet отработает, он нам уже будет ни к чему, его смело можно удалить. Причем, нам совершенно не обязательно называть его Pet, его код должен быть таким же, а имя может быть любым. Таким любым, что вы должны быть абсолютно уверены в том, что нигде его не используете в программе как свойство объекта, ведь мы его совсем скоро удалим. Вполне подойдет имя $_base (просто base не подойдет, иначе все ваши base будут лежать в мусорной корзине). Итак, смотрим:
Pet = function( name ) { this.name = name; } Dog = function( name ) { // делаем Pet методом экземпляра this.$_base = Pet; // запускаем Pet отсюда, т.к. "this" // эквивалентен экземпляру this.$_base( name ); // освобождаемся от внедренного метода! delete this.$_base; } Dog.prototype.__proto__ = Pet.prototype; rover = new Dog( "Rover" ); // проверяем trace(rover.name); // Rover
Ну наконец-то работает! Есть несколько вещей, которые нужно знать. Первое и главное: весь блок кода, касающийся $_base, ДОЛЖЕН быть самым первым в каждом конструкторе. Это ОЧЕНЬ ВАЖНО. Суть в том, что конструктор запускается сверху вниз, а не снизу вверх. Допустим, вы решили переопределить свойство name в конструкторе Dog и пишете: <this.name = 'Dawg'>; стоит вам это написать до блока $_base, тем самым вы установите имя экземпляра в "Dawg", следующий за ним код переименует его в "rover". Конструктор старшего класса перепишет конструктор младшего.
Вторая проблема состоит в том, что на данный момент у вас существуют две ссылки на родительские классы. Программирую в ООП вы часто будете тасовать классы в поисках лучшего решения и у вас всегда есть шанс что-то упустить. Если у вас прототип связан с одним классом, а конструктор с другим - да поможет вам Бог. Очень трудно отловить баг. ООП требует представления программ в виде блоков и поскольку класс тоже является отдельным блоком, он должен иметь единственную цепочку наследования (как, впрочем, и в других языках ООП). Возможно, это и не кажется чем-то важным, однако, когда программа возрастает, вышесказанное становится непомерной ношей.
Последнее замечание носит скорее косметический характер. Может быть это и тяжело, но вы не должны строить свои решения на примере одного и того же кода. Конечно, большинство людей в конце-концов создали свои наработки в этой области и пользуются ими, чтобы автоматизировать процесс. Когда-нибудь и вы обзаведетесь своими излюбленными приемами. Ну а перед этим нужно пробовать, искать, изучать...
|