ООП во Flash 5: Введение

Введение

Этот учебник - попытка объяснить, что такое объектно-ориентированное программирование (ООП), почему оно полезно и как его можно использовать во Flash 5, попытка объяснить ООП, как новую систему, не связанную с "нормальным" процедурным стилем программирования. Предполагается, что вы приемлемо знакомы с ActionScript или JavaScript. Этот учебник скорее всего наиболее полезен для тех, кто не имеют почти никакого опыта программирования или тех, кто продвинут в понимании ООП методов, но в других языках.

Проблема в изучении ООП не в его сложности, а том, что вам требуется изменить способ мышления относительно знакомых вам вещей. Это трудно, требуется такое же переосмысление, которое происходит, когда вы впервые лично встречаете "друга из интернета". Особенно, если вы ошиблись в своих представлениях о его половой принадлежности. Ничто не изменилось, это тот же самый человек, но в то же время это новый человек с лицом и голосом (и возможно с неимоверными грудями или бородой...). Сначала вы просто отвергаете это, мысленно воспринимаете "он-лайн" и "реального" человека, как двух различных людей, хотя и понимаете, что между ними имеется очень много общего, вы путаетесь в процессе общения и даже начинаете сомневаться кого из них раньше знали - его/ее/их. Возможно в этот момент вы оглядываетесь назад, пробуя приспособить старые факты к новой информации, перечитываете некоторые ваши старые электронные письма, улыбаетесь и возможно краснеете. Постепенно все медленно изменяется, становятся понятны некоторые моменты, которые были неясны прежде. Время идет и в конце концов вы привыкаете к новому, уже не понимая, как могло быть иначе. Все встало на свои места и вы стали объектно-ориентированным программистом.

Итак, что такое ООП?
Лучше всего начать его изучение со скромного понятия Объект.

<< ООП во Flash 5 >>


ООП во Flash 5: Объекты

Объекты

personA = new Object();

personA - это новый Объект. Теперь можно добавить объекту новые свойства, как это показано ниже:

personA.name = "Ann";
personA.age  = 31;

Это означает, что:

  • personA имеет свойство name; (имя), значение которого равно "Ann"
  • personA имеет свойство age (возраст), значение которого равно 31

    Достаточно просто. То же самое можно сделать по другому:

    personA = { name:"Ann", age:31 }

    Примечание

    ActionScript игнорирует пробелы и табуляцию. Например, создать объект personA можно было бы так:

    personA =
{
  name : "Bob",
  site : "bob.com",
  favs :
  {
    site1 : "flash.com",
    site2 : "flesh.com"
  }
}

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

    o = { x:x, y:y, z:{a:a, b:b} }

    Обратите внимание на скобки, запятые, двоеточия и отсутствия точки с запятой в конце строки.

    Фигурные скобки { } быстрый и удобный способ создания объекта. Вот другой объект:

    personB = new Object();
personB.name  = "Bob";
personB.age   = 25;
personB.hobby = "drinking";

    А это то же самое:

    personB = { name:"Bob", age:25, hobby:"drinking" }

    Это было просто. Теперь у нас есть два объекта: personA и personB. Каждый из них имеет два свойства: name и age, а PersonB имеет еще одно дополнительное свойство hobby (хобби). Если вам что-то не понятно, посмотрите пример еще раз, потому что следующее предложение содержит очень важную информацию.

    Также где-то есть третий объект, имеющий два свойства - personA и personB. Это дает нам первое представление об ООП в работе. Все содержится внутри объекта.

    Если ваши глаза начали стекленеть, подождите... Давайте рассмотрим такую метафору. Представьте, что кто-то переезжает. Это событие влечет за собой перевозку вещей. Здесь есть два подхода. Первый - подогнать грузовик (желательно со съемным тентом) к входной двери и затем погрузить в него вещи, ящики, полки и заплатить шоферу. Это самый быстрый и самый простой способ переезда и он прекрасно работает в жизни. "Нормальный" способ переезда - рассовать вещи по коробкам и подписать (какими-нибудь закорючками) все коробки, согласно их содержанию. Это включает принятие тысячи решений и неотложных действий в час и это хороший тест для супругов, если вы когда-нибудь такой искали.

    Боб и Энн переезжают. Давайте поможем им разложить их вещи по коробкам. Объекты, подобны коробкам, так что это не причинит вреда вашему обучению. Общеизвестно, что американцы самые "переезжающие" люди в мире (ok, после тибетских кочевников). Трое из пяти американцев переезжают каждые пять лет, совершаются миллионы и миллионы переездов. Дальнейшее исследование показало, что в каждом из этих переездов, имеется ящик с пометкой "important stuff" (важные вещи). Учитывая то огромное значение, которое имеет для американцев ящик "important stuff", мы и начнем с него.

    importantStuff = new Object();
importantStuff.money       = 500;
importantStuff.papers      = 96;
importantStuff.annPassport = "er246vjl";
importantStuff.bobPassport = "kl554mkt";

    Заметили, что мы добавили несколько единиц в ящик? А если мы поместим туда что-нибудь еще, что уже находится в другом ящике, например шкатулку с побрякушками?

    importantStuff.jewelryBox = 1;

    Это кажется правильным, но если Энн захочет найти свое ожерелье, сможет ли она сделать это? Мы поместили список вещей в каждый ящик, но ее ожерелья нет в списке. Что мы действительно должны сделать - создать список того, что находится в шкатулке с побрякушками, что-нибудь вроде этого:

    importantStuff.jewelryBox = new Object();
importantStuff.jewelryBox.necklace1 = "Pearl";   // подделка
importantStuff.jewelryBox.necklace2 = "Diamond"; // подделка

    Теперь шкатулка с побрякушками имеет список ее содержимого (конечно же не забываем комментировать при необходимости). Это не лучшее решение для Энн, потому что список ее драгоценностей, находится внутри закрытой шкатулки. Ей потребуется все открыть, чтобы найти свое ожерелье. Однако является удачей для нас то, как объекты ведут себя в ActionScript, поэтому оставим ящики так, как они есть. Надеюсь у нее нет возражений на этот счет.

    Мы поместили объект в объект - ящик в ящик. Теперь не проблема увидеть другой путь. Предложите им использовать два транспортных средства для переезда: автомобиль и грузовик. Часть ящиков сложить в один, часть в другой. "Important stuff" находится в автомобиле, так что ожерелье может быть найдено в car.importantstuff.jewelrybox.necklace1. Это очень похоже на то, что произошло, когда мы создали personA и personB. Мы создавали свои объекты просто "где-нибудь" и это где-нибудь всегда находится внутри объекта, когда вы создаете объекты в ActionScript. В мире Энн и Боба легко увидеть, что, в конечном счете, предельным контейнером будет Земля, Вселенная, ОМ. В ActionScript OM также существует, принимая форму "объекта, вызывающего Объект".

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

    Давайте попробуем поместить объект внутрь personA, просто посмотрим, как это работает.

    personA.favs = new Object( );

    Мы только что добавили новое свойство к personA. Проверьте ваш пульс и продолжайте, когда он придет в норму. Мы легко можем добавить свойства к этому новому объекту favs:

    personA.favs.site1 = "flash.com";
personA.favs.site2 = "google.com";

    Или, как было описано выше:

    personA.favs = { site1:"flash.com", site2:"google.com" }

    Теперь можете сказать:
    personA имеет свойство favs, которое имеет свойство site1, со значением "flash.com".
    personA имеет свойство favs, которое имеет свойство site2, со значением "google.com".

    Просто как Flash

    Если это кажется запутанным, хорошо возвратиться непосредственно к Flash'у (с который мы будем много работать). Это всегда было одним из самых лучших конкретных примеров полноценности объектно-ориентированной структуры. Если на сцене имеется мувик (movieclip) personA, то _root имеет свойство, называемое personA. Если вы помещаете мувик в personA и называете его favs, то можете сказать:

    personA имеет свойство, называемое _x, значение которого равно 5.
    personA имеет мувиклип, называемый favs, свойство которого, называемое _x имеет значение равное 100.

    Очевидно, что можно думать о мувиклипах, как об объектах, а о вещах подобных _x или _width, как о свойствах (фактически это то, чем они и являются на самом деле).

    Итак, теперь, когда вы понимаете, как поместить объект в объект, вы знаете все, что необходимо знать об ООП. Это конечно шутка, но не настолько, насколько вы могли бы подумать. Тут важно понять то, что объект подразумевается находящимся внутри другого объекта. Возможно вы знаете все это, часто помещая один мувик внутрь другого, но всегда полезно подвести итог.

    Время жизни объектов

    Первая, наиболее очевидная импликация это то, что внутренний (дочерний) объект некоторым образом связан с внешним (родительским) объектом. Это называется родительско-дочерним отношением, хотя в данном случае более точно следовало бы назвать это материнско-зародышевым отношением. Если мать попадает под поезд, младенец в ее утробе разделяет ее судьбу. Если вы удаляете родительский объект, дочерний объект также исчезает. Обратное утверждение, разумеется, не верно, если удаляется дочерний объект, родительский остается неизменным, за одним исключением - минус один дочерний объект. Обратите внимание на то, что дочерние объекты могут иметь только одного родителя. Младенцы могут иметь только одну мать, объекты - только одного родителя (подумайте про муравьиную матку, если встретилось много объектов). Существуют другие типы родительско-дочерних отношений, которые мы в конечном счете обсудим.

    Область видимости

    Область видимости является вторым следствием вложенности объектов. Область видимости означает что-то вроде "контекста", в том смысле, "что именно можно увидеть/понять с разных точек/мест". Возвращаясь опять к метафоре с младенцем, младенец может видеть "внутри" своей матери (с фонариком), но не может видеть видеть то, что видит мама. В ActionScript вы можете автоматически делать ссылку на область видимости объекта, в первую очередь, ActionScript будет искать свойства в собственной области видимости объекта.

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

    personA=
{
  name : "Ann",
  favs :
  {
    site1 : "flash.com",
    test1 : function(){ trace (this.site1) },
    test2 : function(){ trace (this.name ) }
  },
  test3: function(){ trace (this.name )
 },
  test4: function(){ trace (this.site1) }
};
personA.favs.test1(); // flash.com
personA.favs.test2(); // null (ребеной не может видеть родителя)
personA.test3();      // Ann
personA.test4();      // null (родитель не может видеть внутри ребенка)

    В этом примере, ключевое слово this означает "контейнер, вызвавший эту функцию", что по счастью означает именно тот контейнер, который мы пытаемся увидеть внутри. На данном этапе, совершенно необязательно разбираться в том, как это работает, достаточно на простом языке описать, что будет на выходе каждого свойства test.

    test1: Свойство, называемое site1 в контейнере personA.favs.
       // которое имеет значение "flash.com"

test2: Свойство, называемое name в контейнере personA.favs.
       // нет такого свойства в personA.favs

test3: Свойство, называемое name в контейнере personA.
       // которое имеет значение "Ann"

test4: Свойство, называемое site1 в контейнере personA.
       // нет такого свойства в personA

    Интересно, что будет, если мы изменим test4 следующим образом:

    test4: function(){return this.favs.site1;}
       // Обратите внимание на '.favs.'

    Отлично, работает. Что же на выходе?

    test4: Свойство, называемое site1, в контейнере, называемом favs,
       который находится в данном контейнере.
       // ? попробуйте найти значение, используя новый test4 код...

    Адресное пространство

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

    Ключевая концепция в объектно-ориентированном программировании: Исключительно важно всегда продумывать, куда именно будут направляться объекты в вашей программе. Как именно будет выполнена конкретная задача, зачастую значительно менее важно по сравнению с тем, где она будет выполнена. Представьте себе парочку, занимающуюся любовью в общественной столовой... Вот уж действительно - не так важно "как", как "где" - тот самый случай.

    Просто как Flash

    Возможно, вы все это уже знаете из опыта использования Flash. Именно так работает "целеуказание" клипов. Возьмите клип с именем personA и включите в него клип под названием favs. Теперь, из линейки personA (изнутри контейнера personA), при вызове this._x на выходе будет свойство _x, клипа personA. Если вы вызовете this.favs._x, на выходе будет свойство _x, клипа personA.favs. Если вы создаете свойство this.name="Bob", его значение изнутри клипа favs не будет видно (если только не использовать _parent - всегда ведь найдется обходной путь!). Такое сходство неслучайно - клипы являются таким же типом объекта, как и все остальное.

    Связи типа Has-a

    Отлично, это было нетрудно. вы можете создавать объекты, объекты со свойствами (которые часто называются членами (members) или атрибутами (attributes) и эти свойства имеют свои имена и значения. Как мы уже видели, этими свойствами могут быть и другие объекты, которые, в свою очередь, могут иметь свойства и так далее, и так далее... Такой тип связи называется связь типа has a. В терминологии объектно-ориентированного программирования полным-полно длинных слов вроде "полиморфизм" или "инкапсуляция", но хватает и коротких вроде "this", "has a", или "is a"... К счастью, большинство из этих концепций значительно проще поддается пониманию, чем наименованию.

    Связь типа has a состоит в том, что у объекта имеется внутренний объект. Например, у машины имеется ('has-a') топливный бак. У нее имеется ('has-a') рулевое колесо. У нее имеется ('has-a') двигатель. ('has-an' означает тоже самое, что 'has-a'. Английская грамматика!). На первый взгляд, это кажется очевидным, но по сравнению с другими типами связей между объектами вроде is-a (т.е. "является", "машина является средством передвижения"), все не так просто. Объектно-ориентированное программирование во многом опирается именно на эти две концепции.

    Ну, вот - мы опять забегаем вперед. Поэкспериментируйте с нижеприведенными примерами, а потом мы перейдем к классам - шаблонам для создания объектов.

    == Упражнение ==================

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

    <<link to swf>>
    ===============================


ООП во Flash 5: Классы

Классы

Забудьте то, что знаете о функциях. Забудьте, что они получают аргументы и возвращают ответ. Забудьте, что они хороший способ сократить блок кода до простого имени. Фактически они являются просто шаблонами, используемыми для задания свойств в объекте. Здесь ключевое слово - ШАБЛОН. Шаблон может использоваться для создания новых объектов, а так же для изменения существующих. С одним и тем же шаблоном можно создавать так много объектов, как это вам необходимо.

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

Это все немного абстрактно, однако повторюсь еще раз - это очень простое понятие. Например:

Template = function()
{
  this.x = 5;
  this.y = 7;
}

inst1 = new Template( );
inst2 = new Template( );
inst3 = new Template( );

trace( inst1.x); // 5
trace( inst1.y); // 7
trace( inst2.x); // 5
trace( inst2.y); // 7

Слово, используемое в ООП терминологии для обозначения этих шаблонов - "Класс" (Class) (здесь следует думать о классификации, а не о классной комнате).

Класс (Class) это шаблон, который используется для создания новых объектов, называемых "экземплярами" (instances). Объекты и экземпляры суть одно и то же и могут быть взаимозаменяемыми (и часто!). Упомянутое слово "экземпляр" обычно предполагает объекты, созданные из класса с использованием оператора new, в то время как "объект" является более общим термином для всех объектов.

Регистр в именах

Имена классов всегда должны начинаться с заглавной буквой, имена экземпляров - с маленькой. Если вы будете придерживаться этого правила, вам будет легко и просто обращаться к ним.

Несмотря на то, что класс это просто абстрактная идея в эфире, а экземпляр настолько реален, что оставляет след, когда шлепается на вас, в программе они имеют тенденцию выглядеть устрашающе одинаково.

Помните: ClassName - instanceName

Экземпляры происходят от классов, поэтому переходим к следующему разделу с экземплярами ...


ООП во Flash 5: Экземпляры

Экземпляры и краткое описание оператора "New"

Определения

Класс | (Class) | - шаблон, используемый для указания свойств объекта.
Экземпляр | (Instance) | - объект, создаваемый с помощью класса.

Связь классов и экземпляров с тем, что мы уже рассмотрели:

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

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

1) Template = function()
2) {
3)     this.x = 5;
4)     this.y = 7;
5) }
6) inst1 = new Template();

Каким образом получилось так, что inst1 заканчивается свойствами x и y? Откуда ActionScript знает, что это именно то, что нам нужно? Давайте разберём этот пример шаг за шагом. Первым делом определяется класс и это только описание, от него мало толку, пока его не вызвали. Первая строка, которая указывает на что-то конкретное - строка #6. Правая сторона уравнения всегда первична и рассматривается в первую очередь. Вот мы с неё и начнём. Слово new используется в разных случаях, но самое главное, зачем оно нам может понадобиться - создание нового объекта (поля, контейнера...) без свойств и имени. Потом оно отсылает этот объект к тому классу, на который указывает выражение, находящееся рядом с ним. В нашем примере это класс, с именем Template (Шаблон). Итак, правая часть уравнения в строке #6 отсылает "объект без свойств" к "Template".

Каждый класс, будучи вызванным, ожидает и получает пустой контейнер без имени. Этому контейнеру с началом блока кода временно присваивается имя this и затем он "проходит" через класс. Во время такого "прохождения" ему присваиваются свойства, в соответствии с инструкциями класса, используя это ключевое слово this. Подумайте об этом, как о коробке, едущей по конвейеру и наполняемой различными объектами по мере её продвижения. Каждый ее шаг означает: "данная коробка получает такое-то свойство", "данная коробка получает такое-то свойство"... В нашем примере контейнеру присваивается два свойства: x и y со значениями 5 и 7, соответственно. После достижения конца определения класса, контейнер, которому теперь присвоено два новых свойства, возвращается обратно. Таким образом в строке #6 содержится:

inst1 = << контейнер (объект) с двумя свойствами, x:5 и y:7 >>

Следующее, что нужно сделать - дать контейнеру имя. В нашем примере это "inst1". Вот и всё. Представьте людей, ожидающих коробки в конце конвейера, чтобы написать на них свои имена и затем забрать домой. Теперь это ИХ коробки, тогда как раньше это были ПРОСТО коробки на конвейере. Вот для чего нужна левая часть уравнения в сроке #6 - дать имя объекту. Это происходит, когда класс закончил свою работу. Теперь можете проверить конечный объект на наличие свойств x и y. Они должны иметь значения 5 и 7.

Единственное, что способно определить класс в ActionScript, это функция (по крайней мере в 5-й версии). Это может быть ваша собственная функция или встроенная, такая как Color или Array. Только классы, встроенные или созданные, могут создавать экземпляры. Вскоре мы разделим функции на два типа: классы (classes) и методы (methods). Тогда во избежание путаницы можно будет перестать пользоваться словом "функция".

Просто как Flash

Скорее всего вы уже знакомы со словом "экземпляр" (instance) по работе с Macromedia Flash. Вы создаёте экземпляры символов, просто "перетаскивая" их из библиотеки на сцену. Это то же самое, что и создание экземпляра из класса, абсолютно то же самое. "Вытаскивая" символ на сцену, вы тем самым создаёте "новый" объект. Таким образом создаётся пустой контейнер (если это мувиклип, то пустой мувиклип). Потом этот контейнер передается шаблону класса и "проходит через него", получая по дороге различные свойства (как на конвейере). Разумеется, класс это описание символа, которое вы находите в библиотеке, а свойства - графические элементы в символе (то, что кладется в коробку на конвейере). Должно быть, вы уже поняли, почему очень удобно пользоваться шаблонами (template) для создания объектов во Flash'е. По тем же самым причинам удобно пользоваться шаблонами и в ActionScript.

Возможно, вам кажется, что не всякий класс получает "безымянный" контейнер. Это отчасти верно, например:

A = function()
{
    return 7;
}
y = A(); // 7

Первое, что следует отметить, это не класс. Классы никогда не возвращают значений и они всегда вызываются с помощью оператора new. На самом же деле это метод (method), о котором речь пойдёт несколько позже. А пока вкратце скажем, что метод это функция (function), имеющая принадлежность к классу. Фактически термины "функция" и "метод" часто взаимозаменяемы. "Классы" же, это совсем другое дело. Как бы мы ни называли их, все функции "принимают" объекты и присваивают им значение this. Классы "принимают" пустые объекты, а методы принимают существующие объекты. Но вернёмся к нашим примерам...

Написав A(), может показаться, что мы никоим образом не отправляем объект в функцию, чтобы "налепить" на него свойства. Откуда бы ему взяться? Однако, если даже вы не определяете объект (как в случае с использованием new, или inst.A();), то в этом случае под объектом подразумевается (и соответственно направляется) текущий контейнер. Контейнер, в котором помещается весь этот код это, конечно же, _root (то есть _level0, если именно отсюда берётся код). Таким образом, используется именно этот контейнер. ( A() на самом деле означает _root.A(); ). Следующий код показывает это:

A = function()
{
    this.x = 5;  // _root.x = 5
    return 7;
}
var y = A();
trace (_root.x); // 5
trace (y);       // 7

Как мы видим, _root был отослан в функцию и присвоен ключевому слову this. После выполнения функции, свойства, назначенные параметру this, также стали распространяться и на _root, как следует из вышеприведённого примера.


ООП во Flash 5: Область видимости

Область видимости

Что происходит, когда мы определяем свойство, используя ключевое слово var? Создаётся локальное свойство. "Локальный" - значит "действующий только в пределах границ данного объекта" или, если хотите, "в пределах границ данного контейнера". Что-то типа "первого парня на деревне", которые рождаются и умирают в пределах одной маленькой деревеньки и никогда никуда из нее не выбираются. Вот пример локального свойства, определённого в классе:

A = function()
{
   var temp = 5;
   this.x   = temp;
}
trace(A.temp); // не здесь

Где именно действует свойство temp в данном примере? Его действие распространяется на часть функции A, являющуюся "объектом активации" (иногда также используется термин "конструктор функции"). Означает ли это, что мы найдём его с помощью выражения A.temp? Нет. Потому что в этом примере мы его только определяем, целиком объект активации остаётся не вызванным пока не вызвано выражение A(). Означает ли это, что он станет доступным после того, как вызвано выражение A? Нет. Потому что, после того, как вызвано A, он не принимается в расчёт. Вообще, каждый раз, когда вызывается функция, создаётся такой вот объект активации, и каждый раз, когда функция заканчивает своё действие, этот объект активации перестаёт приниматься в расчёт (хотя во Flash 5 из памяти он обычно не удаляется и не исчезает. Ай-яй-яй!). Итак, temp действует в пределах границ этого временного объекта. Однако, из-за того, что у этого объекта активации нет имени и он просто "находится неподалёку" во время действия функции, никакая другая часть программы не имеет к нему доступа (если только вы не вызовете функцию из функции и та вторая функция не увидит его с помощью своего свойства this). Когда он заканчивает своё действие, его локальные свойства не сохраняются.

Многие думают, что "в ActionScript всё, что ни есть - всё объект". Это не совсем так. Кое-что может и не быть объектом или, как это ни странно, может быть более, чем одним объектом. Вышеприведённая функция, это два объекта (вообще-то, строго говоря, целых три, но об этом позже). Существуют объекты-ссылки, называемые A, которые ведут себя, как любые другие объекты. Вы можете добавлять к ним свойства, например A.temp и эти свойства будут вставлены в объект A. У вас также есть блок инструкций, которые составляют функцию. Это и будет вторым объектом - объектом активации, который создаётся каждый раз, когда вызывается функция.

При использовании первого объекта A существует ряд ограничений. Главная задача этого объекта - дать имя блоку функций, чтобы его можно было вызвать. После этого можете использовать его именное пространство для хранения информации о самой функции. Такой информацией может быть, например, имя класса объекта (которое может понадобиться для исправления ошибок), или счётчик, который следит за количеством экземпляров, вызванных с помощью объекта. Такие свойства никак не повлияют на работу функции. В последующих экземплярах от них не останется и следа.

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

Просто как Flash

Идея создания вызываемого блока инструкций, принадлежащего функции, вновь напоминает нам о Flash. Помните? Символ в библиотеке имеет информацию, которая сообщается экземплярам, управляя их поведением и видом. А как насчёт другой части функции, объекта A в предыдущем примере? Эта часть используется для хранения информации об объекте не относящейся к экземплярам. Например, все символы в библиотеке имеют имена, но эти имена не присваиваются (и вообще никак не относятся) к экземплярам. Если включить опцию "Update Use Count" в библиотеке, каждый символ будет показывать, сколько раз он используется в вашем клипе. Вот и получается, что это свойство имеет отношение к классу, но не имеет отношения к экземпляру. Самое логичное было бы помещать такую информацию вместе с объектом класса. В ActionScript предусмотрена возможность доступа к такой информации (используя intance.constructor.temp, - мы к этому ещё вернёмся), тогда как во Flash она недоступна для ваших экземпляров (она доступна только вам, как пользователю). Конечно, у вас не так уж и часто возникает необходимость в использовании именного пространства, но иногда это просто необходимо.


ООП во Flash 5: Наследование

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

Всё, что мы делали до сих пор можно охарактеризовать, как "композиция". В нашем случае это слово означает придумывание, составление объекта из более мелких компонентов, а не урок музыки. Наверняка вы уже используете композицию в ваших программах. Прежде, чем мы двинемся дальше, давайте дадим этому понятию точное определение, дабы потом не забыть, что же это такое. Как уже было сказано ранее, это понятие часто обозначается, как "имеется" (Has a). У объекта А имеется объект В. В доме имеется кухня, у собаки имеется хвост, у мятежника имеется причина. Трудно представить себе объект, у которого или в котором ничего не имеется (то есть ничего нет), в конце концов, во всём имеется (есть) что-то, всё из чего-то состоит. Однако всегда ли это так? Как насчёт самих идей? Например, какое отношение имеет "приспособление" к "холодильнику"? Какое отношение "разводной ключ" имеет к "разводу" или к "инструменту"? Какое отношение Джеймс Дин имеет к Рональду Рейгану? К Элвису? К Че Геварре? Как только мы начинаем организовывать идеи и концепции, одних "имеющихся" понятий становится недостаточно. Теперь нам потребуется "наследование" -- понятие из разряда "являющихся" (Is a).

Прежде чем продолжить, давайте познакомимся с этим понятием. Понятие "имеющийся" предполагает содержание одного предмета в другом. В машине есть двигатель, в двигателе есть поршни, в поршнях есть... э-э-э... такие маленькие круглые чёрные штуковины, кажется (прошу прощения, но этим ограничиваются мои познания в автомобилях). Понятия "являющиеся" используются, когда один объект является типом другого. Так автомобиль является транспортным средством, транспортное средство является машиной, машина является созданным человеком приспособлением с различными вращающимися частями (ещё раз прошу прощения!). Теперь самое время остановиться и поразмыслить о различных системах наследования. Вот некоторые разъяснения.

"Наследование" несложно осознать, но его трудно представить визуально. В реальном мире ничто не ведёт себя абсолютно одинаково. "Наследование", это скоре подсознательная система классификаций, если так можно выразиться. Очень просто увидеть млекопитающее, когда с ним разговариваешь, но ведь такого животного, как "млекопитающее" не существует. Есть только типы млекопитающих. Это обстоятельство, конечно же, не означает, что слово "млекопитающее" для нас бесполезно. Но ведь нет же в зоопарке клетки с надписью "млекопитающее". Раньше мы говорили об объектах, как о коробках или контейнерах. Это допустимо для "композиции", где вы помещаете одни объекты внутрь других. Однако такого рода определения не работают при использовании "наследования". Чтобы представить себе "наследование", нам понадобится немного другая модель.

Давайте-ка ещё раз убедимся, что мы поняли разницу между "имеющимися" и "являющимися" отношениями между объектами (рассмотрим хотя бы простейшие случаи). Изучите нижеприведённый список и определите, являются ли отношения между объектами в каждой строке "наследованием" или "композицией" - "являющимися" или "имеющимися". Попробуйте найти единое общее для каждой группы "являющихся" (наследований), а также для всех "имеющихся" (композиций) примеров.

  1. Животное, млекопитающее, собака, пудель
  2. Компьютер, материнская плата, процессор, микросхема
  3. Артист, певец, рок-звезда, Элвис
  4. Дом, кухня, раковина, пробка
  5. Книжная лавка, компьютерный отдел, стенд "Flash", книга по actionscript
  6. Книга, книга по компьютерной тематике, книга о Flash, книга по actionscript
  7. Книга, глава, параграф, предложение, слово
  8. Континент, страна, регион, город/деревня, улица, дом
  9. Азия, Китай, Сиань, ЗенгКуоАн, 15
  10. http://www.macromedia.com/support/flash/
  11. Приложение, документ, полоса прокрутки, указатель
  12. Окно, диалоговое окно, сообщение об ошибке, очередь на печать
  13. Еда, десерт, пирог, банановый крем
  14. 01 (204) 435-8301

Теперь давайте вместе подробно рассмотрим эти примеры (только не будем на них особенно задерживаться!)

  1. Животное, млекопитающее, собака, пудель - Наследование.
    Пудель - это собака, собака - это млекопитающее, млекопитающее - это животное. Какие свойства добавляются на каждом уровне? Попробуйте "прикрепить" вот эти: шерсть, глаза, дикий, домашний.
  2. Компьютер, материнская плата, процессор, микросхема - Композиция.
    В компьютере есть материнская плата, на материнской плате есть процессор и так далее. Если у компьютера есть кнопка включения, есть ли она у процессора? (только ради бога не говорите, что у процессора их 5 миллиардов!). А теперь попробуйте ответить на подобный вопрос, если речь пойдёт о животных и собаках.
  3. Артист, певец, рок-звезда, Элвис - Наследование.
    Элвис был рок-звездой, рок-звезда - это певец, певец - это артист. А все ли рок-звёзды - певцы? И куда в этом списке поместить актёров? Мы видим, что "Элвис" - это пример, а всё остальное - классы.
  4. Дом, кухня, раковина, пробка - Композиция.
    В кухне есть раковина, а раковина домом не является (разве только для тараканов в студенческом общежитии). Теоретически, можно вырезать кухню и утопить её в океане. Но кухня останется кухней. Это объект в доме.
  5. Книжная лавка, компьютерный отдел, стенд "Flash", книга по actionscript - Композиция.
    Каждый следующий физически находится в предыдущем. Можно, конечно, вообразить взаимообмен компонентов, например, устроить на кухне (которая давно на дне океана) компьютерный отдел. Огромное преимущество использования организованных объектов заключается в том, что один и тот же объект можно использовать в нескольких различных местах.
  6. Книга, книга по компьютерной тематике, книга о Flash, книга по actionscript - Наследование.
    Все они являются типами или подтипами. Книга по actionscript - это книга по компьютерной тематике. А примеры в этой цепочке есть? Какой пример вы бы добавили?
  7. Книга, глава, параграф, предложение, слово - Композиция.
    Этот пример с хитринкой. Перечисленные компоненты не всегда физические. Конечно, вы можете дотронуться до параграфа в любой книге, но, тем не менее, если мы говорим о книге вообще, то "наследование" не подходит. В книгах есть главы, предложения состоят из слов. Такая логика подходит и для предыдущих примеров на "Композиция", однако, в данном случае это более наглядно. На кухнях бывают раковины, и в моей кухне есть раковина. В обоих случаях раковина - это компонент кухни. Не путайтесь!
  8. Континент, страна, регион, город/деревня, улица, дом - Композиция.
    Хотя на этот раз абстрактное мышление просто необходимо, это всё же композиция. Совершенно очевидно, что улица - это не разновидность страны. Таким образом одни концепции могут быть компонентами других концепций. Внутри контейнера "континент" находятся "страны". В свою очередь "страны" содержат "регионы"...
  9. Азия, Китай, Сиань, ЗенгКуоАн, 15 - Композиция.
    Такая же иерархия, как и в предыдущем случае, только здесь мы видим конкретные примеры. Каждая концепция теперь снабжена объектом, но объекты эти являются компонентами и содержатся один в другом. На континенте Азия есть страна Китай. В стране Китай есть город Сиань.
  10. http://www.macromedia.com/support/flash/ - Композиция.
    В домене macromedia.com есть раздел под названием support. А в нём вы найдёте что-нибудь по Flash. Сюда вы можете обратиться, если ничто другое не помогает. Здесь вы можете найти копию справочного руководства. Подумайте, что представляет собой структура каталогов на вашем жёстком диске - "Композиция" или "Наследование"? И любая ли система каталогов может считаться композицией? Подумайте хорошенько.
  11. Приложение, документ, полоса прокрутки, указатель - Композиция.
    Да, программы объектно-ориентированы... в документах есть полоса прокрутки, на полосе прокрутке есть указатель (это такая маленькая штука, которую вы тянете мышкой)...
  12. Окно, диалоговое окно, сообщение об ошибке, очередь на печать - Наследование.
    Да, программы тоже используют наследование. Очередь на печать - это тоже разновидность окна, равно как и остальные (диалоговое окно и сообщение об ошибке).
  13. Еда, десерт, пирог, банановый крем - Наследование.
    Пирог - это еда, десерт - это еда. Обратите внимание, что структура древоподобна. Просто помните, что десерт может быть разный, равно как и пирог. Все эти структуры древоподобны.
  14. 01 (204) 435-8301 - Композиция.
    Это всё равно, что сказать "Северная Америка > Манитоба > Майами > мой старый телефонный номер". Телефонная система очень похожа на почтовую. Чтобы вас найти, здесь используется строго структурированный каталог имён.

Наследование, как и объектно-ориентированное программирование вообще, становится необходимым инструментом, когда у вас появляется достаточно классов подобных друг другу и они начинают как бы само-организовываться (мечты, мечты!). Методика в любом случае достойна упоминания, потому что многие программы на основе Flash довольно просты и невелики в объёмах. Более того, нагромождать многочисленные уровни наследования для того лишь, чтобы привязать кнопки к страницам, это абсурд. Если же вы способны написать подобную маленькую программу, вы почувствуете себя начальником почтового отделения в небольшой деревеньке. Чтобы доставить почту, вам не нужно ничего, кроме имени получателя. В случае каких-либо изменений, если, например, кто-то приехал в город или изменил место жительства или же улица поменяла название, у вас есть возможность просто запомнить всё это. Вы станете смеяться над почтальонами больших городов, которые принимают всерьёз все эти нудные и непонятные правила и часами спорят из-за незначительного изменения в списке почтовых индексов. С другой стороны, ведь почта так и так доходит до адресата, и в больших и в малых населённых пунктах. Но до тех пор, пока вы знаете, что ваша деревенская система отличается от городской, вы можете быстро и чисто справляться со своими задачами, отвечая возрастающим потребностям, это факт. В конце концов, вам не нужно разделять млекопитающих до бесконечности - от собак до атомов. Одно из первостепенной важности умений в объектно-ориентированном программировании - умение найти золотую середину между сложностью и завершённостью (кстати, перед подобной же дилеммой стоит и автор, которому нужно написать короткую главу об ООП в ActionScript!).

Сделать: типы символов (?)


ООП во Flash 5: Прототипы

Прототипы

В наших предыдущих примерах классов у нас было, как правило, три возможности для назначения свойств. Мы могли прицепить их прямо к объекту класса, например A.temp. Или же могли установить временные свойства внутри блока класса {}, используя выражение var x = 5. Мы также могли назначать свойства для объекта (this), "проходящего" сквозь объект активации класса, во время создания экземпляра. Такие свойства затем отображались в новом экземпляре. Казалось бы, вариантов для назначения свойств достаточно много, но всё же кое-что до сих пор оставалось для нас невозможным. Нет никакого способа назначать свойства, принадлежащие классу, но в то же время доступные для экземпляра. Наверное, это звучит странно, так что давайте рассмотрим такую ситуацию подробнее. Вот один простой пример, так сказать "собачий" (Dog):

Dog = function()
{
    this.legs = 4;
}
rover  = new Dog();
fido   = new Dog();
yeller = new Dog();

Сначала может показаться, что свойство this.legs принадлежит к классу Dog из-за того, что оно назначается там же. Можно подумать, что его специально "оставили доступным" для каждого экземпляра Dog и что это свойство, вместе с тем, является частью класса. Однако это не так. Свойство legs сказывается только на объекте экземпляра и нет никакой возможности добраться до него, если вы начнёте с Dog (его не находят даже такие выражения: Dog.instance.legs, Dog.benji.legs, Dog.legs). Определённо, это не свойство Dog. Это свойство экземпляра (benji.legs, fido.legs... - вот так пойдёт). А что если мы попробуем так: Dog.legs = 4 ? Может тогда это свойство станет свойством класса доступным для экземпляров? Если немного "поколдовать", экземпляры будут (иногда) иметь доступ к такому свойству (benji.constructor.legs - уж если вам так интересно), но правильно ли это? Должен ли benji "спрашивать" класс, определяющий Dog, сколько у него должно быть legs или он и так будет знать об этом? Если предположить, что benji - это тип (разновидность) собаки (Dog), то он (benji) должен иметь все собачьи (Dog) достоинства, которые должны распространяться на него автоматически. Если мы напишем Dog.legs = 4, то benji.legs останется равным 0. Нам поможет то, что называется объект-прототип.

Все классы имеют объект-прототип, который "виден" экземплярам. Он как бы подвешен к классу и содержит родственные ему свойства. Интересно, что экземпляры класса автоматически "заглядывают" туда за свойствами. Разумеется, сначала экземпляры проверяют сами себя, ведь зачем спрашивать друга "сколько времени?", если у самого на руке часы! Экземпляр проверяет себя на наличие свойства и если таковое не найдено, то он "рассуждает" следующим образом: "Я - разновидность Собаки (в нашем примере - Dog) и мне интересно, есть ли у Собаки (Dog) это свойство". После этого он "идёт и берёт" свойство у прототипа своего класса (Говорящая собака - это круто!).

Теперь можно поместить в собачий (Dog) класс информацию обо всех собаках и использовать экземпляры для информации, которая уникальна для каждой собаки. Осталось только выучить синтаксис, что обычно не представляет проблем:

Dog = function( ){}
Dog.prototype.legs = 4;

rover  = new Dog( );
fido   = new Dog( );
yeller = new Dog( );

fido.puffyHair = true;

Теперь всё на своём месте - у всех Собак (Dog) по четыре лапы, но вот puffyHair - личный выбор fido. Однако, это не означает, что у остальных собак не может быть точно такой же "причёски". Просто данное свойство (puffyHair) специфично и индивидуально, оно не является общей особенностью собак вообще. Итак, у fido есть свойство puffyHair, у других его нет. У всех у них есть свойство legs, хотя оно пошло скорее от Dog.prototype.legs, чем от fido.legs. Каким образом у fido есть доступ к legs? Вот в чём вся прелесть! Обращение к такому свойству происходит так же, как и любое другое обращение к любому другому свойству fido - fido.legs. И вообще, мы не знаем, не хотим знать, да нам и не нужно знать, откуда берётся это свойство, что, разумеется, не означает, что нам не надо поставить и хранить его в специально отведённом для него месте во время работы нашей программы. Но обращаетесь вы к нему так, как будто это свойство экземпляра: rover, fido или yeller. Да, это замечательно, но возникает путаница с терминологией, поскольку мы уже привыкли называть объекты контейнерами и даже коробками. Давайте же немного проясним ситуацию. Мы можем представлять себе объекты, как коробки со списком их содержимого, прилепленным клейкой лентой к внешней стенке. Однако, происходящее внутри этих объектов вовсе не похоже на то, что может случиться с обычной картонной коробкой. Для этого нам потребуется новая метафора.

Наследование работает подобно многослойному оконному стеклу. Если вы бывали когда-нибудь на севере, то могли видеть окна с тройными стёклами. Вот то, что нам нужно. Они обладают такой прекрасной изоляцией, что дольше сохраняют тепло в вашем доме ("Тепло" значит "хорошо" - это для тех, кто в Фениксе, ведь вы, ребята, должно быть, пользуетесь тройными стёклами, чтобы уберечься от жары). Мы заговорили об этих окнах потому, что они имеют одно очень интересное для нас свойство. Если вы просто посмотрите на них, вы не узнаете, сколько в них слоёв. Может кое-кому и достаточно взглянуть на счёт за отопление, чтобы узнать, сколько их там. Кстати, у строителей на этот случай есть свои методы. Они зажигают зажигалку перед стеклом и считают, сколько там отражений. (Отсюда правило: даже строителю нужна зажигалка). Итак, у нас три (или более) слоя стекла. Подсознательно мы уже понимаем, что стекло там не одно, но, сколько именно их там, мы не знаем. Последнее, что можно сделать, это попробовать просунуть клочки бумаги между стёклами (только не делайте так дома, а то испортите окно и нарушите его герметичность!).

Вы можете просунуть какие-нибудь предметы между разными слоями стекла, но на вид будет казаться, что все они остались на одном слое, вот что самое интересное! Если разместить кусок бумаги на одном слое в то же место, где на другом слое находится такой же кусок бумаги, в поле зрения у вас останется только кусок ближайшего к вам слоя. Ближайший к вам слой стекла представляет для нас наибольший интерес, так как до него проще добраться, чем до остальных. Вы можете изменять куски бумаги на переднем слое (modify) или добавлять новые (add) или даже изымать их и выбрасывать (delete). Другие слои, как правило, остаются неизменными с тех пор, как вы их создали, если только у вас нет крайней необходимости изменить их (можно прибегнуть к расслоению, но вы должны быть предельно осторожными!). Обычно вы делаете изменения, помещая куски бумаги на передний слой стекла (тот, что закрывает другие), а также изменяя этот кусок. Ни в коем случае нельзя рисовать на стекле - вам, наверное, об этом говорили, когда вам было года четыре или около того. Казалось бы, информации для запоминания слишком много. Но не унывайте! В ActionScript встроена автоматическая проверка!

Посмотрите на изображение стёкол выше. Видите, как расположены квадратики каждого слоя? Жёлтые на первом слое, оранжевые - на втором, фиолетовые - на третьем.
- Вопрос:
Что бы вы делали, если бы теперь вам нужно было изменить центральный (фиолетовый) квадрат?
- Ответ:
Налепил бы новый бумажный квадратик сверху - а то слишком уж муторно расслаивать стёкла.

Таким образом, свойства объекта менее похожи на схематичные рисунки мелом на доске, а скорее на кусочки бумаги, приклеенные к многослойному оконному стеклу. Первый (передний) слой стекла - это наш экземпляр. Когда он создаётся, то помещает свойства на своём уровне. Свойства прикреплены к нему (или, точнее, к безымянному объекту, которым он станет) посредством использования ключевого слова this После того, как он создан, свойства также могут быть прикреплены вручную. Для этого можно использовать выражение instance.x=5. Далее мы рассмотрим ещё несколько способов появления свойств на слое экземпляра. Второй слой стекла содержит свойства, принадлежащие создавшему экземпляр классу. Они находятся в выражении Class.prototype. Что бы вы ни добавляли ко второму слою, всё это будет "видно" для всех экземпляров, созданных с помощью класса. Возвращаясь к экземпляру с собакой (Dog), если бы мы добавили свойство puffyHair к Dog.prototype (второй слой стекла), то все три собаки имели бы свойство puffyHair. Старый крикун (yeller), каким бы не был преданным и верным, вряд ли бы остался доволен.

Есть кое-что в нашей новой "оконной" модели, что требует отдельного объяснения. А именно: свойство, добавляемое к Class.prototype появляется во всех экземплярах класса. Означает ли это, что все экземпляры получают "копию" этого слоя стекла? Нет. Слой стекла один и тот же для всех экземпляров. Возможно, вам больше понравится представлять себе слои стекла экземпляров, находящиеся под углом ко второму слою? Тогда, по мере перемещения от экземпляра к экземпляру, всё само собой "выравнивается" и встаёт на свои места. Аллегория с многослойными оконными стёклами хорошо подходит для описания одного экземпляра, но не всей системы наследования. В случае с наследованием, такая метафора нас не устраивает потому, что наследование, это скорее подсознательная, чем визуально-осознанная система классификаций. Нетрудно понять, что все млекопитающие подпадают под одно и то же определение и если это определение изменится, то изменятся и определения всех млекопитающих таким образом, чтобы получить новую информацию. Такое тасование информации невозможно в случае с реальными объектами, которые физически связаны друг с другом. Разумеется, это не означает, что модель становится ненужной, ведь она прекрасно работает с описанием отдельного экземпляра и замечательно объясняет старшинство, но об этом в следующем разделе.


ООП во Flash 5: Старшинство

Старшинство

Теперь у нас есть пример, где у всех собак по четыре лапы, а у некоторых ещё и космы (то есть свойство puffyHair). Рухнет ли наша модель, если yeller подскользнётся и поранит одну лапу в результате неаккуратного обращения с пилой. Мы не хотим писать Dog.prototype.legs = 3, потому что это изменение коснётся всех собак (если только мы не пройдёмся той же пилой по остальным... брр!). Возвращаясь к модели со слоями стекла, мы увидим, что лучшее место, где можно зафиксировать такой трагический поворот событий, это первый слой, принадлежащий yeller'у. Всё произойдёт точно так же, как в своё время fido получил модную причёску. Это логично, ведь беда приключилась только с yeller'ом, а не со всеми собаками сразу. Но что случится, если вы измените свойство yeller'а с помощью следующего выражения:

yeller.legs = 3?

Затронет ли такое изменение (значение Dog.prototype.legs меняется на 3) какие-нибудь ещё элементы? К счастью для всего собачьего сообщества, нет. ActionScript не позволит экземпляру случайно изменить свой прототип (подробнее об этом в разделе Защита). Вы защищены от собственной криворукости! Можете снова пользоваться пилой.

Постойте-ка, но ведь у yeller'а теперь два свойства legs - одно в его экземпляре, другое - в прототипе? Давайте сейчас снова вернёмся к модели многослойных стёкол. Действительно, теперь есть бумажка, с надписью legs = 4 на слое прототипа, и ещё одна с надписью legs = 3 на слое экземпляра. Но существует одно правило: если два "кусочка бумаги" имеют одно и то же имя свойства, то они перекрываются, помните? Итак, эти две бумажки как бы "встали на одну прямую", и какую же из них мы видим? Верхнюю, разумеется. Таким образом, получится, yeller.legs == 3. Вот что произойдёт с нашим кодом:

Dog = function( ){}
Dog.prototype.legs = 4;

yeller = new Dog( );
yeller.legs = 3;

Это должно выглядеть логичным даже для тех, кто совсем ничего не понимает: "У собак по четыре лапы, но у yeller'а их три". Именно это и называется старшинством. Если у двух или более свойств обнаружены одинаковые имена, возвращается то из них, которое находится ближе всего к экземпляру. Аллегория со слоями стекла очень показательна. Если один предмет закрывает другой, то вы видите только ближайший к вам. Разумеется, если вы уберёте передний, то снова увидите тот, что был позади него. Но точно ли так обстоит дело с прототипами? Сохранилось ли ещё то первое свойство? Да, сохранилось. Удалите "закрывающее" свойство, и вы снова его увидите. Вот, как это будет выглядеть:

trace( yeller.legs ); // 3 - из экземпляра
delete( yeller.legs );
trace( yeller.legs ); // 4 - из прототипа

Из вышеприведённого примера видно, что личное свойство yeller'а legs просто перекрыло или "старше" (вспомним армию) свойства Dog.prototype.legs. Может читать это так: "Когда-то у yeller'а было три лапы, но теперь, хвала современной медицине!, у него, как и у всех остальных собак их снова четыре". И вновь на глаза наворачиваются слёзы.


ООП во Flash 5: Переназначение

Переназначение

Переназначение часто путают со старшинством, они просто похожи по звучанию (имеется в виду английское звучание этих терминов - прим. переводчика), но не по действию. Старшинство блокирует старое значение, подменяя его новым, а переназначение поступает именно так, как и называется - перезаписывает новое значение "поверх" старого, безвозвратно уничтожая его. Скорее всего не потребуется показывать пример переназначения, если вы имеете опыт случайной записи двухкилобайтного теста поверх своего лучшего десятимегабайтного FLA-файла, но в нашем "собачьем" случае это выглядит так:

Dog  = function(){}
fido = new Dog();
fido.puffyHair = true;
fido.puffyHair = false;

У Fido была прическа puffyHair, но тут ему сделали нормальную стрижку. Предыдущее значение утеряно и если хотите запомнить первоначальную фидосовскую прическу, потребуется новое свойство экземпляра, что-то типа fido.hadPuffyHair или нечто подобное. В конце-концов индивидуальность имеет право быть зафиксированной индивидуально (как замечания в школьном дневнике...)


ООП во Flash 5: Защита

Защита. Изменения свойств прототипов

Мы имеем доступ к свойствам в прототипе, как если бы они находились в экземпляре. Любое свойство в экземпляре имеет приоритет перед свойством в прототипе его класса, если имена этих свойств совпадают. Однако, вы наверняка уже догадались, что если у нас есть доступ к свойствам в прототипе, словно бы они принадлежали экземпляру, то мы имеем возможность изменять их, верно? Давайте создадим койота (Coyote), он настолько туп (кстати, как вы думаете, насколько именно?), что уже отгрыз себе три лапы, но не ту которую следовало бы и всё ещё находится в капкане. Раз за разом он лишался одной из лап, так что наш пример будет выглядеть так:

Coyote = function( ){}
Coyote.prototype.legs = 4;

ralph = new Coyote( );
ralph.legs--;
ralph.legs--;
ralph.legs--;

Но возникает проблема. Догадываетесь, какая? Правильно, койот всё равно не может убежать. Свойство legs находится в Coyote.prototype, а не в экземпляре ralph. Когда мы пишем ralph.legs--, то ожидаем, что свойство legs в Coyote.prototype будет иметь меньшее значение. Так возникает очень большая проблема для всех койотов, потому что они все получают свои legs из того же Coyote.prototype которое теперь имеет значение 3 вместо 4. Одна капля дёгтя портит весь мёд в бочке. Может вообще не помещать элементы в прототип? Шутка! Вообще-то, есть одно прекрасное решение проблемы ("автоматический" значит "прекрасный") и мы уже говорили об этом, когда бы экземпляр ни изменял свойство прототипа (legs--), это свойство сначала копируется в экземпляр и только после этого даёт себя изменить. Таким образом, это избавляет нас от ненужных хлопот со свойствами, идущими из прототипа. Вам даже не нужно задумываться о том, откуда пришло то или иное свойство. Если вы изменяете свойство в экземпляре, то на самом деле вы говорите следующее: "это свойство в этом экземпляре теперь другое". Наконец-то стало понятно, почему такие изменения происходят в экземплярах, если даже свойства пришли откуда-то из других мест. Однако, при обращении к свойству, нет никакой необходимости вызывать информацию, хранящуюся в экземпляре, если это свойство принадлежит всему классу.

Подумайте над разницей между предложениями "У Джона есть большие уши" и "У Джона есть уши". В первом случае уши Джона имеют особую "величину", которая отличает их от обычных ушей, так что подсознательно это свойство ассоциируется у нас с Джоном (или с его ушами). Если же просто сказать "У Джона есть уши", то это значит, что уши у Джона такие же, как и у всех остальных, так что мы подсознательно ассоциируем "уши" с "людьми", а не с Джоном. Если мы соединим эти два предложения в одно: "У Джона есть уши, и они очень большие", то наш супермозг просто свихнётся. Подсознательно мы берём у людей свойство "уши", делаем копию для Джона и изменяем их величину. Да простят меня большеухие! Если бы вашими мозгами управлял ActionScript, то данная трансмутация выглядела бы примерно так:

Human= function( ){}
Human.prototype.ears = 4; // у всех людей нормальные уши
      // Должно быть, это свойство задаётся где-то в начале жизни

john = new Human( );
john.ears++; // уши копируются в экземпляр 'john' и затем увеличиваются
      // Это случается спустя несколько мнгновений после появления Джона

В данном случае свойство ears появилось в прототипе и было скопировано в экземпляр (одновременно сохраняясь неизменным в прототипе). Ваш мозг никогда не вообразит, что у всех людей вырастут большие уши после встречи с Джоном, так как информация об ушах Джона хранится в его (Джона) экземпляре. ActionScript автоматически делает то же самое. Ну а что если мы случайно удалим объект в прототипе, а не в экземпляре? Ничего страшного не произойдёт. ActionScript не позволит удалить объект в прототипе, если только не обращаться напрямую к Human.prototype.ears вместо того, чтобы работать с john.ears или с this.ears, как обычно бывает в таких случаях. В прототипе элементы находятся в безопасности, экземпляр не может изменить такое свойство, если только не дать ему на то специальных полномочий, позволяя делать всё, что угодно даже с самим собой. Кстати, Джон недавно отгрыз себе оба уха и не может завести подружку...


ООП во Flash 5: Аргументы

Аргументы

Свойства общие для всего класса должны устанавливаться в прототипе класса. Но тогда зачем нужен тот кусок кода, который запускается, когда мы создаём экземпляр? В предыдущих иллюстрациях мы использовали этот блок для установления значений в экземплярах, но значения эти были постоянные, одни и те же для всех экземпляров. Но ведь если для всех экземпляров они одинаковы, то, скорее всего, они принадлежат классу, именно поэтому мы переместили свойство legs в прототип. Если бы всё, что мы могли делать с этим блоком кода, ограничивалось установлением постоянных значений, тогда его употребление было бы действительно весьма неэффективным и малоупотребимым. К счастью, как вам должно быть уже известно, можно передавать значения функциям (а значит и классам). Это нужно, чтобы иметь возможность вызывать класс каждый раз по-разному. Вызов классу приходит от будущего экземпляра, а значит, он сам знает, что ему нужно. Следующая иллюстрация может показаться похожей на стандартную функцию, но давайте рассмотрим её, как процесс создания объекта - безымянного контейнера, с прикреплёнными к нему свойствами впоследствии назначаемого другому свойству. Заметьте также, что этот новый объект унаследует свойства от прототипа класса.

Dog = function( age, hair )
{
    this.age  = age;
    this.hair = hair;
}
Dog.prototype.legs = 4;

rover  = new Dog( 6,  "shaggy" );
fido   = new Dog( 4,  "puffy"  );
yeller = new Dog( 12, "gray"   );

Здесь есть несколько вещей достойных внимания. Во-первых, отметьте, что имена аргументов идентичны именам, назначаемым свойствам экземпляра (this.age = age). Возможно вы скажете, что дабы избежать конфликтов имён, было бы лучше использовать такие имена аргументов, как, например passedAge и passedHair. Однако, если получше разобраться, мы увидим, что это необязательно. this.age ссылается на свойство age в экземпляре. Тогда как само по себе age ссылается на свойство age в объекте активации (точнее в его части {...}). Мы знаем, что установление var age = 6 внутри этого блока кода перезапишет аргумент age, но не перезапишет this.age. Очень важно понимать и отличать эти именные пространства. Попробуйте теперь составить несколько своих примеров, если вам надоели мои.

Другой немаловажный и почти очевидный факт, это то, что аргументы назначаются только в соответствии с их расположением и ни с чем другим. Если мы передадим верхнему классу Dog два свойства: hair и age *именно в таком порядке*, то этот порядок не будет изменён, даже если ваш класс использует те же имена (age и hair). Аргумент age всегда получает первое переданное значение, аргумент hair, всегда второе. Вот пример, основанный на старом мультике (тут я, признаться, обязан кое-чем Джиму Юнгеру!)

marylin  = new Date(36,22,35);
bridgette = new Date(38,23,36);
herman    = new BlindDate(42,18,37);

но когда вы рассматриваете классы:

function Date( bust, waist, hips )
{
    this.bust  = bust;
    this.waist = waist;
    this.hips  = hips;
}

пока что всё великолепно...

function BlindDate( age, IQ, shoeSize )
{
   // uh ohhh...
    this.age      = age;
    this.IQ       = IQ;
    this.shoeSize = shoeSize;
}

... очевидно, что класс получает только значения и класс же решает, что эти значения означают, простите за каламбур (!).

Последнее, о чём стОит упомянуть - тот факт, что при передаче происходит сортировка "по значению". Таким образом, аргументы представляют только значение того, что было передано, а не изначальное имя или объект. Разумеется, если мы передаём объект, как значение, то изменения, произошедшие с ним, будут отражены в объекте. Суть в том, что только значения играют роль. Посмотрите на следующий (не вполне объектно-ориентированный) код:

function test(arg1, arg2)
{
    arg1 = "changed";
    arg2.prop2 = 6;
}
arg1 = "original";
arg2 = { prop1:5 }
test( arg1, arg2 );

trace( arg1 );       // "original" - не изменился
trace( arg2.prop1 +" & "+ arg2.prop2 );
                    // 5 & 6 - prop2 был добавлен к объекту

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

Итак, подводя итоги, можно сказать, что экземпляр, вызывающий класс должен проверить класс на предмет того, какие аргументы ему (классу) нужны и в каком порядке. При передаче аргументы сортируются в соответствии с их значением. Аргументов мы коснёмся более детально в дальнейшем, при рассмотрении массива аргументов.


ООП во Flash 5: Конструктор

Конструктор

Термин конструктор означает "нечто, что создаёт (конструирует) объекты". Мы знаем, что все объекты создаются из классов. Так что конструктор должен быть ЕОВСД (Ещё-Одним-Выдуманным-Словом-Для) класса. Ну, как бы это сказать... Точно так же, как и класс является частным случаем функции (только в ActionScript!), конструктор - это некая часть класса. Эта часть помогает "конструировать" объекты, как вы, наверное, уже догадались. Мы знаем, что объекты создаются при помощи объекта активации и аргументов. Но, стало быть и конструктор - это такой же объект активации. Да, так и есть. Трудности начнутся, когда мы вспомним, что объекта активации не существует, пока мы его не вызовем, а по окончании своей миссии он уничтожается - таких друзей в адресную книгу не вносят. Получается, что единственный способ "активировать" этот объект активации - вызвать свойство, которому он назначен, то есть объект, который даёт имя классу. Это прекрасно работает, потому что аргументы как бы "зажаты" между этими двумя объектами и так прочно друг к другу привязаны, что их часто рассматривают, как единое целое.

Почему нам так важно дать наиболее полное и точное определение конструктора и его работы? Потому что это ещё одно из недокументированных ключевых слов ActionScript, которые иногда бывают просто необходимы. Всякий экземпляр имеет свойство constructor и оно указывает на класс, который его создал, точнее на объект, давший имя классу. Когда вы рассуждаете о конструкторе с точки зрения экземпляра (как в случае с конструктором rover'а), то конструктор это класс, который создал экземпляр. Когда речь идёт о частях класса, то конструктор это скорее объект активации, наряду с аргументами и объектом имени. На самом же деле, подразумевается прототип или свойства, назначенные объекту имени класса (например, Dog.count), потому что они напрямую не участвуют в "конструировании" объекта. Кое-кто может сказать, что мы просто играем словами. Да, возможно, это так. В большинстве других объектно-ориентированных языках программирования конструктор и имя класса, это абсолютно разные понятия. В ActionScript они, конечно, тоже не взаимозаменяемы, но довольно тесно связаны друг с другом и разница между ними подчас очень невелика. Вот почему так важно оперировать точными определениями этих понятий. Фигурально выражаясь, можно сказать, что:

  • Class - это нечто вроде хижины.
  • Constructor - это её часть, отвечающая за создание новых вещей.

Ранее мы задавались вопросом, каким именно образом экземпляры ссылаются на класс, который их создал. Ответ на этот вопрос - ключевое слово constructor. Он всегда используется изнутри объекта. Наилучшим образом он действует в границах экземпляра, который вы создали из вашего класса (хотя этим его действие, конечно же, не ограничивается, всегда полезно "пощупать" ActionScript в поисках чего-то новенького и неописанного!). Вот два способа, нахождения конструктора из экземпляра.

Dog = function( )
{
  this.instNum = ++this.constructor.count;
}

// следующие два свойства на самом деле не являются частью 'конструктора'
Dog.count = 0;         // используется для подсчета созданных собак
Dog.className = "Dog"; // используется для запоминания имени класса

rover  = new Dog( );
fido   = new Dog( );

trace( rover.instNum ); // 1 (первая собака создана)
trace( fido.instNum );  // 2 (вторая собака создана)
trace( rover.constructor.className ); // Dog

Хотя использование конструктора не всегда нормативно, иногда без него просто не обойтись. Очевидным преимуществом доступа к свойствам Dog через instance.constructor.xxx вместо Dog.xxx является то, что если вы когда-нибудь измените имя класса Dog на Canine, вам не придётся перелопачивать весь ваш код в поисках кусков, где есть ссылка на Dog. Такой способ также делает ваш код намного более портативным на тот случай, если, например, вам захочется использовать его в другом классе или даже в другой программе.

Что интересно, у прототипа класса всё-таки есть конструктор, потому что это, в конце концов, такой же объект, как и любой другой - объект, который был кем-то создан. Когда вы определяете класс, он немедленно создаёт объект имени, но не создаёт объекта активации (который создаётся только когда его вызывают). Но вот объект прототипа всё-таки создаётся и это происходит, как только мы определяем класс. Куда указывает конструктор прототипа? Точно так же, как и экземпляр, он указывает на объект класса имени. Но как бы нам это проверить? Можно просто добавить в верхнюю функцию следующее выражение:

trace( Dog.prototype.constructor.className ); // Dog

Довольно громоздко, не так ли? Но если присмотреться, мы увидим, что эта строка всего лишь говорит: "Отследить имя класса объекта, создавшего ("сконструировавшего") прототип Dog". Ладно, если это до сих пор не вполне понятно, то попробуем по-другому. Если перемещать эту строку вверх и вниз по коду, то мы увидим, что объект прототипа создается, как только мы определяем класс Dog, до того, как в прототипе устанавливаются какие-либо свойства.

И напоследок ещё немного странностей. Объект имени класса (Dog) не имеет свойства конструктора, даже не смотря на то, что это объект, который был создан. Имена функций также не имеют конструктора в ActionScript, даже не смотря на то что в других подобных языках программирования (например, в JavaScript) это не так. Причины объяснить не так просто. Попробуйте для начала перелопатить весь ActionScript так, чтобы ему было "удобно" в "крошечном" 200-килобайтном Flash Playr'е. Вот-вот, и я про то же! Давайте-ка лучше перейдём к следующему разделу про методы. Он интересный!


ООП во Flash 5: Методы и свойства

Методы и свойства

До сих пор во всех наших примерах свойства классов принимали простейшие значения, такие как "rover" или 5. Имя класса (Dog, например) назначалось им свойству и прототип становился свойством этого класса (Dog.prototype в нашем случае). В ActionScript мы фактически можем назначить что-нибудь свойству. Это отличает ActionScript от многих других языков (Java, например), так что мы оказываемся перед необходимостью разъяснить, что подразумеваем под "свойством". В Java и С++ существует большая разница между методами и свойствами, чего нет а ActionScript-е. Так что же такое методы?

Метод очень похож на то, что мы обычно понимаем под функцией в программе, с одним отличием, методы всегда привязаны к классу и, как правило, вызываются из объекта-реализации этого класса. Методы призваны модифицировать состояние этого объекта и/или возвращать вычисленные значения. Они не используются для создания нового экземпляра класса. Методы никогда не используют свои прототипы, они не помещают свойства в собственное пространство имен (namespace). Методы не похожи на классы. Это просто программы, принадлежащие классу.

Можно сказать, они "выглядят" как классы. Они тоже созданы при помощи функции, однако они не вызываются при помощи оператора new. Их ключевое слово this ссылается на объект, который вызвал этот метод, никак не на созданный новый. Поскольку они, подобно классам, автоматически не возвращают новый объект, они могут возвращать какое-то значение, даже если оно пустое (void) (хотя это не требование, это просто хороший стиль программирования в более строгих языках). И последнее, методам, как и остальным свойствам классов, для их отличия от этих самых классов, лучше всего давать имена, начинающиеся с прописной буквы. Теперь давайте посмотрим маленький примерчик того, как создаются и вызываются методы, и потом разберемся во всех этих терминах, смысл которых, возможно, потихоньку начал от вас ускользать.

// Класс Dog
Dog = function( age )
{
    this.age = age;
    this.dogYears = this.calcDogYears( this.age );
}
// метод Dog - вычисление возраста в собачьих годах
Dog.prototype.calcDogYears = function( age )
{
    return ( this.age * 6 );
}
// метод Dog - у собаки день рождения
Dog.prototype.haveBirthday = function( )
{
    this.age++;
    this.dogYears = this.calcDogYears( this.age );
    trace("С Днем Рождения!")
}

// создание экземпляра собаки (Dog)
yeller = new Dog( 12 );
trace( yeller.dogYears ); // 72
yeller.haveBirthday( );   // метод выдает "С Днем Рождения!"
trace( yeller.dogYears ); // 78

А заметили ли вы, что мы всегда определяем прототипы до того, как создадим экземпляр класса? Flash не может вызывать методы, которые еще не определены, поскольку он считывает и выполняет код в том порядке, как он написан (это все потому, что при загрузке swf файла вы никогда не можете быть уверены в том, загрузился ли определенный кусок кода). Посмотрев на конструктор класса в примере, вы можете углядеть противоречие вышесказанному. Метод calcDogYears вызывается из конструктора до того, как он будет описан в прототипе. Однако конструктор запустится лишь в тот момент, когда будет создан объект "yeller". Когда вы пишете код, вы должны рассматривать класс и его прототип как единый блок и определять их вместе. Методы прототипа обычно определяются после его свойств.

Хотя методы и можно поместить в экземпляр класса, такое происходит редко. Методы по природе своей более статические, чем строки или числа, так что они вероятнее всего останутся отдельным большим классом. Будет, возможно, ситуация, когда вы захотите установить методы во время выполнения, передавая их как аргумент конструктору. Это тот случай, когда вы раз и навсегда откажетесь от этого, окончательно запутавшись в многочисленных операторах if-else. Вскоре мы это разъясним.

Функция, или Класс, или Конструктор, или Метод, или Свойство... Уффф!

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

Определения

  • Класс (Class) - это определение или шаблон, необходимый для создания нового объекта. Ключевое слово: new. Сюда входит и определение прототипа класса. При этом не возвращается никакого значения, потому что возвращается новый созданный объект. Каждый раз, используя оператор new, вы создаете новый экземпляр класса. Ключевое слово this ссылается на новый созданный экземпляр. Аргументы используются для того, чтобы наделить каждый объект определенными свойствами.
  • Метод (Method) - это программа, почти всегда определяется в прототипе класса и всегда запускается посредством объекта (чаще всего экземпляром класса, имеющего прямой доступ к методу). Метод может использовать аргументы и возвращать определенные значения. Ключевое слово this ссылается на объект, вызывающий метод. Хотя у методов есть прототипы и пространство имен, они игнорируются.
  • Функция (Function) - вы можете задаться вопросом, как сюда "вписываются" обычные функции, не создающие новых объектов и не принадлежащие классам? Что ж, никак. Если вы собираетесь плотно заняться ООП, вы должны хорошо это усвоить. Функции, существующие сами по себе, - жители другого мира, называемого процедурным программированием (ах да, они тоже весьма полезны и круты, но только не в мире ООП).
  • Конструктор (Constructor) - относится к классу, который создает/создавал объект. Имя его совпадает с именем класса, представляет из себя блок кода, необходимый для инициализации объектов класса.
  • Свойства (Properties) - технически это нечто, присвоенное объекту, что стало свойством этого объекта. Это нечто может быть строкой, числом и т.д., а также методом и другим объектом... Под свойствами часто подразумевают все "неметоды", так что будьте готовы к этому. ActionScript не делает различий между методами и свойствами, но вам порой это будет необходимо. Свойства иногда называют атрибутами (только данные), переменными (только данные), членами класса (данные и методы), четными полями (данные и методы), хотя они всегда будут нести в себе проблему двойного значения с того момента, когда вы начнете пользоваться этими терминами. К счастью, в 99 случаев из 100 об истинном смысле вы можете догадаться из контекста. Оставшийся 1% - всего лишь свойство ActionScript-а. Резюмируя, они могут означать все неметоды, или методы и неметоды, все зависит от контекста.
  • Члены класса (Members) - тоже заслуживают внимания, поскольку это общее название для свойств и методов объекта. В данном случае класс или объект можно рассматривать как набор определенных вещей и вещи эти суть члены этого набора.
  • Объекты или экземпляры класса (Object vs Instance) - это в общем-то синонимы, хотя экземпляром обычно называют объект, созданный с помощью оператора new, в то время как объект - общее название для всех объектов. Строки и числа несомненно являются объектами, фактически же это экземпляры, созданные внутренними конструкторами ActionScript-а. Обычно экземпляр явно не указывает на тип объекта, но вы всегда сможете догадаться из контекста.


ООП во Flash 5: Оператор __proto__

Упаковываем классы, используя __proto__

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

Итак, когда объект-экземпляр запрашивает некоторое свойство, не имея его при этом в наличии, автоматически проверяется прототип его конструктора (прототип класса, который его создал). Даже если и прототип не содержит нужных данных, история на этом не заканчивается. Этот самый прототип, являясь полноправным объектом, уж будьте уверены, проверит прототип класса, который создал его - Object. Короче говоря, он проверит Object.prototype. Трудно сразу проглотить такой кусок информации, давайте его пережуем.

Обратимся к очередной метафоре. Представьте себе, что владелец дома начинает требовать с вас плату за жилье. И у вас нет ни копейки (вполне возможно, что это и представлять не надо). Ваши действия? Скорее всего, обычный человек обратится за помощью к объекту (объектам), которые его создали - к родителям. Вы точно знаете, что собираетесь расплатиться именно деньгами, взятыми у родителей (вряд ли хозяин квартиры согласится принять в уплату самих родителей), поэтому вы и просите их порыться в закромах (в прототипе). Что они сделают, окажись, как и вы, в затруднительном положении? Очень может быть, что, после коротких переговоров, посоветуют вам обратиться к объектам, которые создали их - к вашим бабушкам и дедушкам. Забыв на время о том, что ничто не вечно (а также об инфляции), мы сможем представить себе картину, где вы от бабушек-дедушек идете к прабабушкам-прадедушкам, от них - к пра-пра-бабушкам-дедушкам и т.д. И так будет продолжаться до тех пор, пока вы либо не найдете нужную сумму, либо не доберетесь до Создателя Всего Сущего. В зависимости от того, кто хранитель последней надежды в системе ваших убеждений, это может быть либо Бог, либо правительство, либо, в конце-концов, Object.prototype. Что если случится так, что вы обспросили всех, молились ночи напролет, но - "Ом мани падме хум" - "Денег нет и не будет"? Что ж, у вас появится реальный шанс остаться без крыши над головой. Если же хоть кто-то даст вам нужную сумму, вы вновь обретете счастье и уверенность в себе и прекратите дальнейшие поиски. В ActionScript-е точно так же, как только затребованное свойство получено, оно немедленно передается местному хозяину и работа заканчивается. Полезно думать, пока не привыкнешь, о цепочке прототипов таким образом: куда-мне-обратиться-если-вы-тоже-сидите-без-денег. По крайней мере, это научит вас совершать определенные действия к концу каждого месяца.

Откуда объекты узнают, к кому обращаться в затруднительном положении? Им об этом рассказывают после того, как они будут созданы. Кто? Конечно, их родители. Что они при этом говорят? Они говорят: "Милый ребенок, запомни хорошенько, что если когда-нибудь в этом холодном и бесприютном мире ты будешь испытывать нужду, знай, что ты не одинок. В первую очередь обращайся к нам и мы постараемся что-нибудь придумать для тебя". Итак, пока мы еще в светлой памяти, давайте хорошенько усвоим, что классы хранят свои свойства в своих прототипах, а это значит, что объекты, не имея некоторых свойств, проверяют их наличие в прототипе класса, которым были созданы. Уф!

Здесь есть одна тонкость: прототип есть объект (именно поэтому вы могли раньше слышать словосочетание "объект прототип"). Что это значит? Это значит, что если прототип не имеет нужного свойства, он проверяет прототип класса, который его создал. И последний вопрос: как класс создает прототипы? Вы, возможно, уже знаете ответ - вспомните, что прототипы создаются автоматически, когда определяются функции и их тип "объект". Ладно, может вы не в курсе, но в самом деле прототипы создаются классом Object. Точно так же и экземпляры классов (ведь они тоже объекты) слышат в момент своего рождения от родителей: "Если что, посмотри в моем прототипе сынок". Вот так все прототипы и экземпляры в конечном итоге добираются до Object.prototype, если нигде не найдут нужного свойства. Это и есть третий лист стеклянного пакета - Object.prototype.

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

- Слушай, Боб, как ты думаешь, ведь никто из нас не наложит в штаны,
если мы тот класс назовем просто "Object"?
- Ясное дело, Пит, о чем разговор!

Вот так его и окрестили Object-ом, с большой буквы "О" (ведь имя класса всегда начинается с заглавной буквы). Существует множество типов объектов, некоторые из них являются экземплярами Object, другие лишь производные от него, но так или иначе, все они являются его наследниками. Хоть Боб с Питом давно канули в лету, дело их живет. Если вы хотите, вы можете перекрестить Object хоть в Верховного Пса (TopDog) - попробуйте и вы испытаете непередаваемое ощущение. Эх, где ж я был в тот момент...

Не все то Объект...

Вы можете подумать, что числа и строки объекты более общего вида, чем объекты, непосредственно порожденные классом 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
У Object.prototype есть маленькая табличка с надписью: "Дальше дороги нет!".

Ну ладно, с этим мы разобрались. Давайте рассмотрим сам механизм того, откуда объект узнает о направлении поисков. Эти связи совершаются при помощи __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" (Любимец). Всех зверюшек (вы можете добавить своих) породим от этого класса и в него же засунем описание всех этих звериных свойств - имя, количество лап, цену и флажок любим/нелюбим

  • name - имя - у всех зверьков есть имя, потому оно должно быть членом класса Pet. Каждый зверь имеет свое имя, поэтому давать его нужно экземпляру. Логично поместить его в конструктор класса Pet, при этом в экземпляре класса создается свойство name, давая право самому экземпляру решать, как оно будет звучать. Если вы хотите, чтобы по умолчанию зверю давалось имя "Безымянный", это значение должно находиться в Pet.prototype, тогда оно будет оставаться таким, пока вы его не замените на нужное.
  • legs - лапы - у всех зверей они есть, причем у всех по четыре, поэтому логично это свойство поместить в Рet.prototype со значением 4. Однако перед этим четко определитесь, не решите ли вы в дальнейшем завести себе змею или паука. Если почувствуете, что способны на это, у вас будет три способа решить проблему.
    1. Обязанность хранить информацию о собственных лапах возложить на каждое животное. Это делает код легким для понимания и удобно в случае большого разброса количества лап.
    2. Установить по умолчанию в Pet.prototype количество лап, равное четырем и в каждом особом случае переопределять это свойство.
    3. Наконец, при создании экземпляра можно просто передавать в Pet.constructor количество лап как аргумент. Этот путь возлагает кое-какую ответственность на пользователя класса, однако является идеальным решением для какого-нибудь зоомагазина.

    Ну и совет, скажете вы. Что ж, занимаясь ООП вы поймете, что часто не бывает идеального решения проблемы, особенно если вы не располагаете всей, порой специфической, информацией. К счастью, все можно легко поправить в будущем, если вдруг изменятся требования.

  • price - цена - у всех животных она есть, причем у каждого своя. Это свойство должно остаться в Pet.prototype (с возможностью freeware). Еще одно замечание: если различные собаки и коты имеют различные цены, тогда она должна быть установлена в Pet.constructor, подобно имени.
  • 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". Конструктор старшего класса перепишет конструктор младшего.

Вторая проблема состоит в том, что на данный момент у вас существуют две ссылки на родительские классы. Программирую в ООП вы часто будете тасовать классы в поисках лучшего решения и у вас всегда есть шанс что-то упустить. Если у вас прототип связан с одним классом, а конструктор с другим - да поможет вам Бог. Очень трудно отловить баг. ООП требует представления программ в виде блоков и поскольку класс тоже является отдельным блоком, он должен иметь единственную цепочку наследования (как, впрочем, и в других языках ООП). Возможно, это и не кажется чем-то важным, однако, когда программа возрастает, вышесказанное становится непомерной ношей.

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


ООП во Flash 5:

Массив аргументов

Недокументированная особенность!

Необходимо сказать, что массив аргументов является недокументированной особенностью со всеми вытекающими из этого последствиями. Но, поскольку очень сильно похоже на то, что эта особенность будет работать и в будущих версиях Flash, хотя я и не даю никакой гарантии (она есть в дальнейших версиях ECMAscript и позволяет сделать то, чего невозможно добиться другими способами), используйте ее только тогда, когда без нее не обойтись. В таком случае если из Flash6 эту штуку все-таки вырежут, то вам придется поменять минимум кода.

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

На самом деле аргументы метода в ActionScript находятся в двух местах. Во-первых, создается новое локальное свойство (локальное по отношению к запущенному активируемому объекту) для каждого имени, которые перечислены в определении метода между скобками (arg1, arg2). Второе же место где это добро лежит - массив с именем arguments (будем его обозначать, как arguments[], чтобы было понятно, что это массив). В этом массиве аргументы хранятся в том же порядке, в котором они переданы в метод. То есть в вышеуказанном примере массив будет состоять из 2-х значений, сначала arg1 (в arguments[0]), а потом arg2 (в arguments[1]). Вот пример:

test = function( arg0, arg1 )
{
  trace( arg0 +" "+ arg1 );      // вывод - "1ый 2ой"
  this.( arguments[0] +" "+ arguments[1]); // вывод - "1ый 2ой"
}
test( "1ый", "2ой" );

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

getAverage = function( _args_ ) {
  var total = 0;
  for(var i = 0; i < arguments.length; i++)
  {
    total += arguments[i];
  }
  return total / arguments.length;
}
this.( getAverage(4,5,3,2,6,10) ); // вывод - "5"
this.( getAverage(54,45,96) );     // вывод - "65"

Это главный аргумент "за" использование такой интересной особенности. Примите во внимание, что слово _args_ не обязательно, но я таким образом самому себе напоминаю, что в этот метод должно что-то передаваться. При выходе из метода эта переменная уничтожится, следовательно, никакого вреда она не несет, а склероз - вещь распространенная...

Есть правда и сюрпризы. Оказывается, массив аргументов - это НЕ МАССИВ! Можно узнать его длину, но нельзя использовать такие методы, как pop() или push(). Другой сюрприз, а возможно, что просто жучок - arguments[0] и первая переменная в списке переменных это не один и тот же объект. Поэтому, если в методе с аргументами (arg0, arg1) мы поменяем значение arg0, то arguments[0] все равно будет иметь старое значение. Имейте это в виду при написании программ... (может, это не баг, а фича! Интересно, а почему ничего не сказано про остальные аргументы? - прим.пер.)

Ну и последнее, что надо бы знать про этот "массив", это то, что у него есть одно интересное свойство с именем callee. Понятное дело, что набрать слово callee проще, чем prototype или __proto__, но про него читайте в следующем разделе...


ООП во Flash 5: метод Callee

Метод Callee

Внутри активированного конструктора, вам автоматически предоставляется доступ к двум объектам. Именно там, в области видимости активированного объекта, исполняются команды. Код также включает ссылку на второй экземпляр, созданный ключевым словом this, (по природе Flash'a, вам также предоставляется доступ к _root линейке - его можно спокойно проигнорировать). Этим обычно и исчерпываются ваши потребности. Однако, чего здесь не хватает, так это ссылки на задействованный класс. Сначала вам может показаться, что она присутствует в текущей области видимости, но помните, что эта область является отдельным временным активированным объектом и соответственно, все, что создано в этой области будет удалено как только она прекратит работу. Очевидно, что это - не класс. Можно попытаться найти его через ключевое слово this (this.constructor или this.__proto__). Хотя this является временным и безымянным контейнером, которому еще только предстоит стать экземпляром, его свойства constructor и __proto__ уже известны и заранее заданы. Таким образом, this.constructor указывает на class, а this.__proto__ указывает на class.prototype. Следует помнить, однако, что это - указание на тот class (или class.prototype), который создал данный экземпляр. Таким образом, если вы пользуетесь наследованием и одновременно используете множественные уровни конструкторов, вам понадобится кое-что еще.

К счастью, эта проблема решается использованием arguments.callee. К несчастью, это недокументированная функция и это влечет за собой все, что с такими ситуациями обычно связано. Возможно, вам будет проще избежать необходимости вызова callee путем изменения логики вашей программы. Как бы то ни было, она остается очень полезным инструментом при тестировании различных объектно-ориентированных структур, которых вы наверняка будете создавать сотнями. Она также является очень полезным инструментом отладки. На что же именно она указывает?

Arguments.callee указывает на функцию (да, класс или метод) к которому прикреплен текущий активированный объект. Таким образом, для того, чтобы конструктор нашел свой класс, в качестве пути достаточно просто указать arguments.callee. Это будет работать всегда, независимо от того, насколько далеко конструктор находится от экземпляра. Далее обычно каждому классу присваивается имя (например, Dog.name="Dog") и для определения того, какой конструктор работает в каком классе применяется команда (из активированного объекта Dog).

trace( arguments.callee.name );

Это может быть очень полезно для отладки. Нахождение прототипа класса изнутри активированного объекта тоже не представляет трудности - arguments.callee.prototype. Arguments.callee также показывает метод, которым можно найти имя его метода, опять-таки простым использованием arguments.callee. Это не так полезно, поскольку свойства у методов обычно не прикрепляются ни к объекту с именем метода, ни к объекту прототипа, поэтому ссылки на эти объекты обычно не требуется. Кроме того, не существует надежного способа, которым метод находил бы объект с именем класса, к которому он принадлежит (за исключением прописывания ссылки вручную), что значительно ограничивает полезность применения arguments.callee в конструкторе. (Получить имя класса метода изнутри метода можно с использованием this.__proto__.constructor.x или, если наследование имеет два уровня глубины, с использованием this.__proto__.__proto__.constructor.x, но при этом у вас нет возможности убедиться в том, сколько уровней глубины имеет ваш метод.)

Ниже приводится пример кода для экспериментирования. Вероятно, callee покажется вам наиболее полезной при проверке структур и отладке.

Class = function()
{
  // найти класс из конструктора
  trace( "Class: " + arguments.callee.x );
  // Вывод: Class: объект с именем класса

  // найти прототип класса из конструктора
  trace( "Class.prototype: " + arguments.callee.prototype.x);
  // Вывод: Class.prototype: прототип класса
}

Class.x = "Class name object";
Class.prototype.x = "Class prototype";

Class.prototype.method = function()
{
  // найти имя метода из внутреннего метода
  trace("method: " + arguments.callee.x);
  // Вывод: method: объект с именем метода

  // найти статические свойства класса из метода
  trace("method: " + this.__proto__.constructor.x);
  // (работает, только если класс на один уровень выше экземпляра)
}
Class.prototype.method.x = "method name Object";

aa = new Class(); // Конструктор теста
aa.method();      // Метод теста


ООП во Flash 5: Оператор New

Оператор New - под микроскопом

Сделать: короткое описание, как оператор new создает объекты, устанавливает __proto__, и т.д.


ООП во Flash 5: Extends

Extends. Специальное ключевое слово

Теперь, когда мы уже так много узнали, пришло время решить, чего же мы хотим. Все наши новые концепции мы попытаемся теперь выстроить в одну простую и логичную систему. После того, как нам несколько раз не удастся этого добиться, мы будем вынуждены остановиться на некоторых новых ключевых словах. На первый взгляд они могут показаться немного непонятными, но, в конце концов, помогут достичь нашей цели. Итак, какой бы мы хотели видеть нашу систему?

  1. Мы должны иметь возможность прикреплять все свойства и методы туда, где им место: свойства класса - к прототипу класса, свойства экземпляра - к экземпляру.
  2. Все свойства и методы в прототипах класса должны быть доступны экземплярам во время их создания.
  3. Свойства в границах одного прототипа должны иметь доступ к методам в границах их прототипов (а также прототипов более высокого уровня) в то время, когда мы их определяем (перед тем как создаются экземпляры).
  4. Когда создаётся экземпляр, конструкторы должны запускаться "сверху вниз" для того, чтобы идентичные имена свойств имели правильное старшинство. Это также даст нам гарантии того, что свойства, установленные в конструкторах более высоких уровней, останутся доступными для конструкторов более низких уровней.
  5. Система должна уметь передавать аргументы "вверх" конструкторам более высоких уровней, причём желательно, чтобы мы могли использовать столько аргументов, сколько нам необходимо - без ограничений.
  6. Система должна поддерживать неограниченное количество уровней наследования.
  7. Сверхкласс должен уметь "справляться" с неограниченным количеством подклассов, у каждого подкласса должно быть не больше одного сверхкласса.
  8. В конечном файле не должно быть никаких конфликтов. Также нужно исключить всякую возможность случайной потери информации - удаление или перезапись системы.
  9. В коде должно быть минимальное количество ссылок. Ссылка на сверхкласс, используемая для установления наследования, должна быть только одна. Мы должны иметь возможность безболезненно менять классы местами.
  10. Система должна использовать стандартный синтаксис "И/ИЛИ" (and/or). Она не должна использовать никаких слов или команд, которые бы могли противоречить коду пользователя. Она также не должна использовать слов, зарезервированных для более поздних версий, насколько это вообще возможно.

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

Первое, что нам нужно, это способ связи одного класса с другим, способ создания взаимоотношений подкласса (то есть дочернего класса) со сверхклассом (то есть родительным классом). Это довольно просто. Всё, что для этого требуется, это специальный метод для передачи двух объектов: объект-имя сверхкласса и объект-имя подкласса. С помощью такого метода мы связываем подкласс со сверхклассом посредством изменения __proto__ - свойства прототипа. Видите, всё действительно просто!

Нам также нужно подумать, как назвать этот специальный метод. Прежде чем придумывать что-либо новое, давайте посмотрим, как обстоит дело с другими объектно-ориентированными системами в подобной ситуации. Есть много типов наследования. Два из них, это так-называемые "ограничивающее наследование" и "расширяющее наследование". Ограничивающее наследование определяет всё, что есть "вверху". Далее оно начинает ограничивать или скрывать элементы по мере того, как вы продвигаетесь "вниз" по экземпляру (например, квадрат - это прямоугольник, ограниченный одной измеряемой стороной). Расширяющее наследование - понятие противоположное ограничивающему наследованию. Здесь изначально верхние классы имеют только самые общие элементы, а более частные добавляются в процессе продвижения к экземпляру. Вообще, в ActionScript, классы получают всё больше и больше элементов по мере того, как становятся подклассами. Таким образом, они дополняются, то есть детализируют свои сверхклассы.

Java использует слово extends в виде SubClass extends SuperClass (подкласс расширяет суперкласс) и это именно то, что подошло бы нам лучше всего. Однако в силу некоторых причин, о которых разговор пойдёт позже, мы ограничены возможностью лишь передавать эти два класса нашему спецметоду, как аргументы. Таким образом, мы можем написать что-нибудь вроде:

extends( SuperClass, SubClass )

К сожалению, догадаться о таком синтаксисе непросто. Его нужно заучить. Мы конечно могли бы поменять местами аргументы и написать extends( SubClass, SuperClass ), но разница была бы небольшая. Просто решим и запомним, что сперва следует высший класс ( extends(Pet, Dog) ) и не будем больше об этом думать. К счастью сам метод оказывается на редкость простым (по крайней мере, до тех пор, пока нам не откроется страшная правда!).

/** Extends ------------------------*/
extends = function( superClass, subClass )
{
    // связь с родителем
    subClass.prototype.__proto__ = superClass.prototype;
}

Кто-то может сказать: "Много шума из ничего". Но вписывать такую строку после каждого класса так же удобно, как и вызывать метод extends. Нужно только запомнить несколько правил. Во-первых, эта часть всего лишь создаёт наследование и не занимается обработкой запущенных конструкторов. Во-вторых, она работает лишь частично. Первая проблема легко преодолима, но вот вторая серьёзна, глубока и загадочна. Начнём с десерта - того, что проще всего.

Вообразите себе следующий сценарий: Жучка очень часто пользуется вашим методом extends, ей это нравится. Однажды она использовала его в клипе, загруженном в _level0. Метод extends был уничтожен, программа не работает, в общем, Жучка за внучку, внучка за бабку, бабка за дедку, дедка за репку - тянут-потянут, вытянуть не могут. Значит, по всей вероятности, _root - не самое подходящее место для спецфункции. Решений может быть несколько и как всегда у всех есть свои "плюсы" и "минусы":

Тиха украинская ночь, но сало нужно перепрятать: Мы можем поместить спецфункию в _level44497.

  • Плюсы: Это просто запомнить, так как это наибольшее простое число перед 0xFFFF (65535). А также нашу спецфункцию можно будет вызывать простым выражением _level44497.extends(Pet, Dog);
  • Минусы: Кто-нибудь всё равно может загрузить клип в _level44497, раз уж это место такое примечательное.

Если хочешь получше спрятать, положи на самое видное место: Мы можем запихнуть её в Object.prototype.

  • Плюсы: Её очень легко оттуда вызвать - простым выражением extends( Pet, Dog ) в любом месте и всё сработает. Если ещё немного усложнить выражение, то может получиться даже что-то вроде Dog.extends( Pet ), а это выглядит намного более привлекательно.
  • Минусы: Вот именно, "откуда угодно". После того, как мы так долго учились "класть всё на свои места", теперь у нас появится функция, доступная любому объекту, но имеющая отношение только к классам. Это всё равно, что разрешить заниматься сексом везде, в том числе и в общественных местах - в ресторанах, например. Кроме того, так наша спецфункция станет конфликтовать с циклами for-in (об этом мы поговорим позднее).

После шведского стола бывает шведский стул: А что если мы поместим её в Function.prototype?

  • Плюсы: Так она будет доступна только функциям, а другим элементам, таким, например, как строка "hello", она доступна не будет. И мы можем использовать всё тот же легко запоминающийся синтаксис: Dog.extends( Pet ). Даже если она и будет доступна методам, которым не положено её видеть, мы всё равно уже решили игнорировать адресное пространство имени метода, а также его прототип, так что это не важно. Ну прямо как в рекламе, помните? "ОН вырастет на целых три дюйма всего за одну неделю"?
  • Минусы: В ActionScript у Function не может быть прототипа. А счастье было так близко! Никогда не верьте рекламе...

Объект... объект??? объект!!!: Наконец, можно просто "прицепить" её к Object.namespace.

  • Плюсы: Снова есть доступ откуда угодно, если использовать ключевое слово Object. К тому же здесь не будет такой проблемы, как в случае с использованием Object.prototype, когда абсолютно всякий объект может беспрепятственно её наследовать. Кроме того, вы не сможете случайно её удалить. Не будет и конфликта имён, как в случае с _root. Если вы хотите обезопасить свою программы, можете составить её прямо здесь.
  • Минусы: Синтаксис снова усложняется: Object.extends( Pet, Dog ). После такой находки, как Function.prototype - это шаг назад, но шаг не смертельный.

Ну так "повесим" её на Object. Ничего страшного в этом нет, нужно просто немного привыкнуть. После всей этой канители мы можем наконец-то добавить одно-единственное слово к нашему новому методу extends (если мы и дальше будем продвигаться такими же темпами, то боюсь, что мы ещё долго провозимся):

/** Extends ------------------------*/
Object.extends = function( superClass, subClass )
{
    // связь с родителем
    subClass.prototype.__proto__ = superClass.prototype;
}

Помните, что теперь мы должны всегда использовать выражение Object.extends вместо того, чтобы просто писать extends (или _level0.extends). Хорошо, что мы вспомнили об этом прежде, чем создали 5000 строк кода.

Памятка по Object.prototype

Всякий объект имеет связь с Object.prototype по цепи своего прототипа. Что произойдёт, если мы назначим здесь свойство? В этом случае, как вы, наверное, уже догадались, все элементы будут иметь доступ к такому свойству, все, включая и его родителя. Это обстоятельство идёт вразрез с самой концепцией объектно-ориентированного программирования, ведь мы всегда старались упорядочить информацию и функциональность нашей программы, создавая унифицированную логическую систему (объектно-ориентированные языки программирования по определению не могут иметь глобальных переменных!). Если у нас появится фрагмент информации, который необходимо сделать доступным всем типам объектов в вашей программе (включая, например "hello".xxx), то возникает другая проблема. А именно, это свойство будет также доступно всем используемым циклам for-in. Системные методы или свойства, такие как array.pop или object.toString могут иметь особое свойство don't enumerate - они относятся к разряду внутренних и не подчиняются общим правилам. Таким образом, если вы пишите for(var i in Object.prototype), что означает "покажи мне всё, что ты видишь" (enumerate), то не вернётся ничего. Эти свойства действительно находятся там и могут быть использованы, однако они невидны в листе свойств объекта. А вот свойства, которые вы сами добавляете, очень даже видны и, чем выше по цепи вы их добавляете, тем большее количество элементов их видит. Если вы думаете, что было бы очень удобно добавить что-то к Object prototype, то помните, что бесплатный сыр бывает только в мышеловке. Лучше делать всё правильно, а не как проще всего.

Другая проблема, связанная с использованием метода extends, ещё более серьёзна. Прежде чем приступать к решению этой проблемы, давайте получше разберёмся в её сущности, с помощью слов и строчек кода постараемся максимально точно её определить, а уж потом приступим к поиску решения.

Свойства и методы могут принадлежать либо классу, либо экземпляру. Если они принадлежат классу, они идут в прототипе. Иногда у вас может возникнуть надобность устанавливать такие свойства, используя методы в том же прототипе или из прототипа классом выше. Это добавляет проблем. Во-первых, у свойства нет возможности доступа к методу в своём собственном прототипе или в прототипах более высокого уровня, на это способны только экземпляры (да и то только благодаря ключевому слову this). Во-вторых, из-за того, что в первую очередь обычно определяются свойства (и это правильно!) у них нет доступа к методам, которые ещё не определены, потому что форвард-ссылки недопустимы. Если бы мы стали в первую очередь определять методы, тогда у тех из них, которые мы вызываем, не было бы доступа к другим свойствам в прототипе (опять же потому что, возможно, мы их ещё не определили). Так дело не пойдёт! У нас возникло сразу две проблемы: отсутствие доступа к методам во время установления свойств прототипа и отсутствие форвард-ссылок.

Те, кто думает, что без этих функций можно обойтись, глубоко заблуждаются. Без них единственным выходом из создавшегося положения будет перемещение всех свойств (не методов) в экземпляры. Это типичная книжная рекомендация для пользователей JavaScript и ActionScript и даже для Flash'еров. Однако, поступая таким образом, мы лишаемся многих преимуществ объектно-ориентированного программирования. Это всё равно, что возить санки в гору, но с горы на них не кататься. Просто какой-то эксгибиционизм в безлюдном месте. Программа получается организованной неряшливо и кое-как. Именно так и выглядит большинство программ, написанных на ECMA, где по мере увеличения шкалы воцаряется всё больший и больший хаос. Наследование должно упорядочивать элементы, а не разбрасывать их где попало!

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

Cat.prototype.petType      = "cat";
Cat.prototype.defaultState = ???.setState("sleep");
Cat.prototype.setState = function( state )
{
  if( state == "sleep" ) return "The " + ???.petType + " is sleeping.";
  if( state == "eat" )   return "The " + ???.petType + " is eating.";
}

Если делать это таким вот образом Cat.prototype.setState( ), то может, конечно, что-то получиться (хотя в данном случае не получится, догадываетесь почему?). Однако, теперь у вас появилась фиксированная ссылка на определённый участок. Если вы когда-либо измените имя класса, вам гарантировано как минимум утомительное "выкапывание", а иногда и многочасовая отладка. Вы можете возразить, что класс Cat уже фиксирован в левой части выражения (Cat.prototype.xxx), так в чём же проблема?.. Разумеется, ссылка в правой части создаёт намного больше проблем, но дело даже не в этом. Любая ссылка в коде - это плохо, так что вместо того, чтобы оправдываться, оглядываясь на чужие ошибки, лучше честно признать, что в списке наших бед появилась ещё одна. Теперь нам уже будет не так легко переименовывать классы, а ведь в процессе разработки структуры часто возникает необходимость в подобной операции. На худой конец и с этим можно было бы как-то мириться, если бы мы пришли, наконец к решению нашей проблемы. Но если теперь один из этих методов попытается вызвать свойство из своего прототипа (или из прототипа классом выше), как это делают возвратные значения, то всё окончательно запутается. Теперь, если мы напишем return Cat.prototype.petType то до поры, до времени всё это, конечно, будет работать, но что случится, если когда-нибудь свойство petType будет перезаписано? Что если подкласс или экземпляр выдадут нам что-то вроде xxx.petType = "siamese cat"? Тогда, будучи вызван из экземпляра SiameseCat, метод setState вернёт нам значение "The cat is eating", вместо "The siamese cat is eating". Это огромная проблема и нет никакой возможности её решить.

Вторая проблема - проблема форвард-ссылок. Она тоже возникает в коде, подобном вышеприведённому. Как уже упоминалось ранее, файлы SWF не поддерживают форвард-ссылки. Они не скомпилированы и просто не имеют представления о том, что за всем этим может последовать (а ведь это "последующее" часто бывает даже не загружено на тот момент). Таким образом, мы вызываем метод setState ещё до того, как создали его. Совершенно очевидно, что этот путь тупиковый. Если даже вы и определились уже с именами ваших будущих "детей", кто вам сказал, что они станут отзываться на эти имена? Вот так-то вот. Ну а что если мы просто поставим определения свойств перед методами? Тогда всё почти получится. Да, именно "почти", потому что тогда методы не будут иметь доступ к свойствам, которые ещё не определены. Какие ещё есть идеи? Разделить их на части? Или просто запоминать, "кто куда пошёл"? Всё это ни к чему не приведёт.

Итак, пришло время кратко изложить суть накопившихся проблем:

  • Свойства прототипов и методы не имеют доступа друг к другу в момент создания класса, если только не использовать в коде фиксированных ссылок..
  • Прототипы определяются ссылками на классы (в левой части), что затрудняет работу с такими классами.
  • Свойства не могут вызывать методы того же прототипа, если только не запускать их после определения методов, (что всё равно не решает проблему, поскольку даже в этом случае, в силу отсутствия форвард-ссылок, методы не будут иметь доступа ко всем свойствам).

Так как же быть?

Проблема с форвард-ссылками похожа на старый как мир вопрос: "Что было раньше - курица или яйцо?". Мы знаем, что методы нужно определять раньше свойств. Мы так же знаем, что свойства нужно определять раньше методов. К счастью, на этот случай есть маленькая хитрость. Методы действительно должны быть определены первыми, но это не означает, что мы должны запускать их первыми. А вот свойства действительно должны быть обработаны в первую очередь. Вот ещё небольшая, но существенная уловка, прототип, как и любой другой элемент, это не класс, а объект. Мы знаем, что по сути объекты, это экземпляры, созданные из образца. А значит, мы можем создать образец, который будет определять прототип, точно так же, как мы делали в случае с другими экземплярами. Таким образом, все свойства и методы определяются в первую очередь, прежде, чем их вызывают. Если мы создадим такой образец внутри прототипа, то при его вызове ключевое слово this будет приравнено к прототипу класса. Так что все эти свойства и методы будут скопированы в прототип. Огромное преимущество этого способа заключается в том, что ключевое слово this приравнивается к прототипу, пока подготавливаются методы и свойства. Это означает, что свойства будут иметь доступ к методам, а методы в свою очередь будут иметь доступ к свойствам. Чтобы быть уверенным, что методы определяются до того, как свойства их вызывают, можно просто разделить их (методы и свойства) на две группы и первыми запускать методы. Потеряют ли в этом случае методы доступ к свойствам? Нет. Потому, что, как мы помним, методы в это время только определяются, а не запускаются. Таким образом, несмотря на то, что к моменту определения метода, свойство, которое он вызывает, ещё не создано, оно будет создано к тому моменту, как будет вызван метод. А как насчёт экземпляров? Если методы ссылаются на другие свойства в прототипе, не означает ли это, что, когда мы создаём экземпляр данного класса, он будет напрямую ссылаться на прототип? Нет. Потому, что значение слова this изменяется в момент доступа из экземпляра. Будучи вызвано из прототипа класса, оно ссылается на прототип. При вызове метода класса из экземпляра, this будет ссылаться на экземпляр. Красотища!

Как следствие таких ухищрений, мы получаем дополнительные удобства. Ваш код сам собой становится более организованным и лучше читается. Все свойства класса определяются вместе, равно как и все методы. Ссылок на имя класса становится всё меньше: одна для класса, одна для свойств/экземпляров, а затем ещё одна для установления наследования (если нужно, мы можем ещё уменьшить их количество). Нет никакой путаницы. Ничто ни с чем не конфликтует, ни одна пара свойств, ни одна пара определений методов, потому что после того, как они запускаются, мы можем (и даже должны) их удалять. А как же быть тем, кто любит устанавливать свойства и методы прямо в прототипе? Может быть вы просто привыкли всё делать именно так или у вас есть уже готовый код, который вы хотите использовать в дальнейшем или вы просто слишком упрямы, чтобы изменить своим привычкам? Никаких проблем! Вас никто не заставляет пользоваться этими методами, если вас не прельщают вышеперечисленные преимущества и удобства. Ведь, в конце концов, не происходит ничего кроме установления прототипа, так что если делать это напрямую, то работать всё будет точно так же, как и раньше (хотя и не настолько хорошо, как могло бы, но если вам так нравится, то пожалуйста...). Кроме тех, кто мечтал о трёх лишних дюймах, все получают что хотели.

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

// Класс
B = function( ){ }
// Свойства
B.prototype.classProperties = function ( )
{
  this.prop1 = 55;
  this.prop2 = this.double( this.prop1 );
}
// Методы
B.prototype.classMethods = function ( )
{
  this.double = function ( ) {return this.prop1 * 2;}
}
// так B становится подклассом A
Object.extends( A, B );

Так как же тогда будет выглядеть наш специальный метод 'extends'? Попробуйте отследить его работу в нижеприведённом коде, игнорируя (пока что) ту часть, которая связана с customKeyword.

// Extends ------------------------
Object.extends = function( superClass, subClass )
{
  // связываем с customKeyword, если это верхний уровень
  // работает, даже если позднее добавить более высокие уровни
  if( (superClass.prototype.__proto__ == Object.prototype)
  && (superClass.prototype <> Object.customKeyword.prototype) )
  {
    superClass.prototype.__proto__ = Object.customKeyword.prototype;
    // устанавливаем прототип сверхкласса, если он ещё не установлен
    if( typeof(superClass.prototype.classMethods) != undefined )
    {
      superClass.prototype.classMethods();
      delete superClass.prototype.classMethods;
    }
    if( typeof(superClass.prototype.classProperties) != undefined )
    {
      superClass.prototype.classProperties();
      delete superClass.prototype.classProperties;
    }
  }

  // связь с родителем
  subClass.prototype.__proto__ = superClass.prototype

  // Устанавливаем прототип подкласса. Чтобы избежать конфликтов
  // или повторного запуска, эти методы должны быть
  // удалены после того, как они своё отработают
  if( typeof(subClass.prototype.classMethods) != undefined )
  {
    subClass.prototype.classMethods();
    delete subClass.prototype.classMethods;
  }
  if( typeof(subClass.prototype.classProperties) != undefined )
  {
    subClass.prototype.classProperties();
    delete subClass.prototype.classProperties;
  }
}

Ну всё, я хочу кофе! А вы? После такого определённо пора отдохнуть. Выпьем по чашечке, подождём минут пятнадцать, пока кофеин не подействует на наши усталые мозги и начнём разбираться с тем, как конструкторы передают аргументы вверх по цепи. Возможно, примеры кодов покажутся вам ещё более сложными, но зато не будет уже больше таких неожиданностей и подводных камней как в случае с extends, так что в конце вам всё это покажется простым и понятным.


ООП во Flash 5: Super

Super - запуск конструкторов

После функции extends, использовать конструктор - как два байта переслать. Главное сделать следующее: передать аргументы конструкторам, которым они нужны, а затем запустить конструкторы в порядке сверху вниз по списку. Единственное, о чём нужно помнить, это то, что конструкторы по своей сущности не связаны друг с другом, в отличие, например, от прототипов. Так что нам придётся "взбираться" вверх по цепочке прототипов и на каждом уровне "возвращаться" назад к конструктору.

Как я уже говорил раньше, иногда полезно посмотреть на работу других объектно-ориентированных языков программирования, чтобы сравнить, "как там у них". JAVA использует ключевое слово super, которое выполняет в точности то, что нам сейчас нужно, и даже кое-что ещё. Так что думаю, оно заслуживает более детального рассмотрения. В JAVA ключевое слово 'super' используется для вызова конструктора родительного класса - если нужно, то вместе с аргументами. Для этого используется следующий синтаксис: super(arg0, arg1, ...). Есть одно ограничение, эта строка должна стоять самой первой в каждом конструкторе. Если где-то она пропущена, то по умолчанию автоматически принимается она же, но без аргументов. Вместо того, чтобы удалить себя, после того, как действие всех конструкторов истекло, её "жизнь" продолжается, теперь это указатель на родительский класс, что позволяет экземплярам вызывать перезаписанные методы с помощью выражения super.xxx( ).

Наше преимущество в том, что в ActionScript мы обладаем всеми функциями, которые выполняет ключевое слово super в JAVA. Возможно, мы немного озадачиваем пользователя двумя маленькими действиями (может ему и трудно, но нам-то какая разница!) Первое, вам необходимо использовать выражение this.super( ), а не просто super( ). Экземпляры просто обожают слово "this", когда о себе "рассказывают" в пределах обычной программы. Я бы сказал, им это нравится (здесь мне вспоминается одна фраза Оскара Уайлда: "Но довольно об этом. Что вы думаете обо мне?") В нашем случае они "разговаривают" о своей "сверх"-миссии. Так что придётся всё-таки писать this.super. Привыкайте! Смотрите на это, как на дополнительную функцию. Если бы не ключевое слово this, то процедура "super" в ActionScript была бы настолько похожа на аналогичную процедуру в JAVA, что пользователи стали бы забываться, а кое-кто и биться головой о стену.

Теперь о втором "усложнении". Пользователь ДОЛЖЕН ставить this.super( ... ) первой строкой любого конструктора. Никакого волшебства. Есть много способов проверить, есть ли у того или иного конструктора вызов super или нет и, в случае, если такового не наблюдается, вставить его. К сожалению, нам никак не обойтись без того, чтобы запустить этот конструктор, а в некоторых случаях это может привести к отклонению от нужного нам курса - представьте себе, что мы ввели новую статичную переменную, следящую за тем, сколько всего экземпляров было создано. Ну да ладно, мы ещё поквитаемся! Теперь необходимо, чтобы пользователь вставлял выражение this.super( ) первой строкой каждого конструктора. Вы спросите: "возникнет ли ошибка, если мы не станем этого делать?" (подобно тому, как это организовано в JAVA) Ну что тут скажешь! Работа с многоцелевыми библиотеками ActionScript требует жертв.

Итак, перейдём к самому процессу. Сначала передаём аргументы вверх по цепи конструктора, а затем запускаем конструкторы в порядке сверху вниз. После этого super немного изменяется. Теперь выражение выглядит так - parent.prototype, совсем как в JAVA, так что у вас есть доступ к перезаписанному свойству или методу посредством использования выражение this.super.xxx( ). Проверьте, понимаете ли вы всё что происходит в этой цепи. (Я - почти понимаю, но ведь это всё я написал...) (В будущем: я должен объяснить это немного лучше :) ).

// SUPER ----------------------
Object.customKeyword.prototype.super = function()
{
    // this.super(); должно стоять первой строкой всех конструкторов

    // счётчик get chain - сообщает нам (по мере продвижения вверх
    // по цепочке конструкторов), сколько раз был
    // вызван super с тех пор, как был создан новый пример
    // Это также увеличивает счётчик.

    var count = Object.customKeyword.prototype.super.prototype.cnt++;

    // находим следующий конструктор
    var fn = this.__proto__.__proto__;
    while(count-- > 0)
    {
        fn = fn.__proto__;
    }
    fn = fn.constructor;

    // вызываем этот конструктор в экземпляр, передаём аргументы
    this.$_base = fn;
    this.$_base(  arguments[0],arguments[1],
        arguments[2],arguments[3],
        arguments[4],arguments[5],
        arguments[6],arguments[7] );
    delete this.$_base;

    // проверяем, действительно ли мы в начале цепи
    if(fn.prototype.__proto__ == Object.customKeyword.prototype)
    {
        // если да, перезаписываем значение super
        // для этого экземпляра,
        // теперь super указывает на родительный прототип,
        // таким образом можно вызвать перезагруженные
        // функции и вернуть их аргументы
        this.super = this.__proto__.__proto__.prototype;
        // обнуляем счётчик цепи для следующего экземпляра
        Object.customKeyword.prototype.super.prototype.cnt = 0;
    }
}
Object.customKeyword.prototype.super.prototype.cnt = 0;


ООП во Flash 5: Особые методы

Особые методы

Наш метод extends автоматически помещает слой между Object.prototype и самым верхним уровнем класса. Но зачем? Видите ли, судя по имени, customMethods, это и есть самое подходящее место для хранения особой функциональности. Теперь о том, что же это такое и с чем его есть... Эта группа методов доступна всем классам и экземплярам, которые используют систему, доступна без вашего дополнительного участия (вам самим не нужно вставлять их).

Кроме того, это место также подходит для инструментов отладки или для расширений к ActionScript. Что бы это ни было, достаточно вставить это в customMethod.prototype и оно станет доступным. Отметьте, что когда вы помещаете сюда объекты, это не вносит никакой дополнительной путаницы в действия Object.prototype. Эти методы (или свойства, если угодно) будут доступны только для классов и экземпляров в вашей системе и не будут доступны никаким другим типам объектов. И ещё, у метода класса нет конструктора (потому, что если бы он у него был, то метод бы просто не работал), так что он сам не может устанавливать значения в экземплярах. Он предназначен только для наследования.

Вот здесь вы найдёте пример особого метода, который отслеживает содержание каждого уровня (в объекте). Он может быть вызван из любого экземпляра следующим выражением: xxx.traceObject(). Теперь можете попытаться создать свой собственный экземпляр. Для этого используйте следующий синтаксис:

Object.customObject.prototype.xxx = function(){ }

Попробуйте!


ООП во Flash 5: Заключение

Заключение

Скачайте объектно-ориентированную систему

Здесь мы подведём итог по использованию объектно-ориентированной системы в ActionScript, не рассматривая подробно то, как это всё работает. Вообще всё просто. Сначала начнём с рекомендаций по использованию, а потом разберёмся с тем, что обязательно, а что опционально. Вот структура типичного класса:

Пример использования

// имя класса и конструктор
ClassName = function( arg0, arg1, arg3 )
{
  this.super( arg1, arg2 );
  this.instanceProp0 = arg0;
}
// установим свойства класса (это повлияет на прототип)
ClassName.prototype.classProperties = function()
{
  this.prop1 = "hello";
  this.prop2 = "world";
}
// установим методы класса (это повлияет на прототип класса)
ClassName.prototype.classMethods = function()
{
  this.method1 = function(){ ... }
  this.method2 = function(){ ... }
}
// установим наследование
Object.extends( SuperClassName, ClassName );

Требования

Первая строка любого конструктора должна быть this.super( ... ). Любые аргументы, которые бы вы хотели задать конструкторам, должны быть переданы вверх сверхметоду.

Процесс назначения наследованию дополнителя должен происходить в самом конце, если только вы не используете classMethods, а просто устанавливаете прототипы вручную.

Есть много преимуществ в определении прототипа с помощью classProperties и classMethods, хотя это и необязательно.

Опции

Особые методы и свойства (расширения ActionScript, инструменты отладки) могут быть помещены в Object.customMethods.prototype.xxx В этом случае они автоматически станут доступными для всех классов и экземпляров (путаницы не будет).

Вы можете отказаться от использования classProperties и classMethods при установлении прототипов. Но тогда свойства прототипов не будут иметь доступа к методам прототипов в процессе назначения и, таким образом, свойства станут "наезжать" на экземпляры. Если вас это не смущает, то можете поступать, как считаете нужным (советую перечитать раздел о this!).

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

Лучше, если ключевое слово super будет выглядеть, как _super, а extends, как _extends. В последующих версиях Flash эти слова могут стать резервными. Наверное, скоро это изменится, хотя...


ООП во Flash 5: Глоссарий

Глоссарий

Что здесь надо изменить: Очень старо, глупо и неточно, надо бы обновить или переписать! Описания должны быть более короткими, с одним примером и в сравнении с JAVA, при необходимости...

  • Класс (Class)

    В JAVA, сначала создается дизайн для объекта, а уже потом на основе этого дизайна создается объект. Дизайн - это класс, а объекты - экземпляры. В ActionScript функция может быть использована, как шаблон дизайна или может быть запущена напрямую. Технически в ActionScript нет классов, но класс это нечто вроде кастрированной функции, такая функция может быть использована только в качестве шаблона (template). Таким образом, если используете функцию ТОЛЬКО, как шаблон, то можете думать о ней, как о классе.

    Проводя параллель с Flash, можно сказать, что символы в библиотеке, это ваши классы (то есть ещё не использованные шаблоны). Каждый раз, когда вы перетаскиваете один из них на вашу "рабочую площадку", он становится экземпляром (то есть новым объектом, созданным с помощью нового шаблона). Вы не можете видеть их в конечном SWF файле, если вы не "вытащили" их из библиотеки на рабочую площадку (кстати, они даже не экспортируются, до тех пор, пока вы не перетащите их на рабочую площадку или пока не установите для них привязку к библиотеке).

    Пример:

    // ********** Class School **********
function School( name )
{
    this.name = name;
}
school1 = new School( "UTOO" ); // экземпляр, названный school1
school2 = new School( "FLU" );  // экземпляр, названный school2
school3 = new School( "TukU" ); // экземпляр, названный school3

    В данном куске кода School, это класс, а school1, school2, и school3 - экземпляры класса School. Согласно общепринятым правилам кодирования, имена классов начинаются с заглавной буквы, так что вы можете легко отличить их от экземпляров, имена которых обычно начинаются со строчных букв. (Наверное, так получилось из-за того, что классы более "важны")

    school1.name is "UTOO"
school2.name is "FLU"
school3.name is "TukU" (University of Tuktoyuktuk!)

    а в этом примере мы решили ни о чём не "спрашивать" School напрямую, потому что School, это класс и следовательно, это не реальный объект, только шаблон. Ведь мы же не станем пытаться найти _x координату чего-либо в библиотеке.

  • Экземпляр (Instance)

    Экземпляр - объект, созданный из шаблона. Шаблон, это функция, которую часто называют классом. Во Flash, всё, что у есть вас в конечном SWF файле, всё это экземпляры чего-нибудь из библиотеки (только не говорите, что делаете фон из больших прямоугольников, не являющихся символами).

    Ключевой момент: каждый экземпляр может иметь различные свойства, как во Flash, где экземпляры могут быть масштабированы, раскрашены и озаглавлены по-разному или иметь разные _x и _y координаты.

    function Point( x, y )
{
    this.x = x;
    this.y = y;
}
pt0 = new Point( 5, 7 ); // pt0 теперь экземпляр класса Point'
pt1 = new Point( 3, 9 ); // каждый из них имеет разные свойства each
pt2 = new Point( 9, 1 ); // как видите, pt2.x равен 9,а pt2.y равен 1

  • Аргументы (Arguments)

    Аргументы - это то, что передаётся в функции через круглые скобки. Всё, что передаётся таким образом, выровнено один к одному с аргументами, определёнными в функции. Всегда старайтесь давать "говорящие" имена с обеих сторон. Помните, что ситуация подобная той, что приведена в примере ниже, не приравняет x к нулю, а y к 30:

    function Point( x, y )
{
    this.x = x;
    this.y = y;
}

      var y = 30;
p = new Point( y ); // так "x" будет равен 30, а "y" нулю

  • this

    this - относится к созданному экземпляру, а не к классу, из которого он был создан. А как во Flash? Допустим, у вас есть мувиклип под названием Graphic1 (почему бы нет?), вы использовали его на рабочей площадке дважды и дали экземплярам имена mc0 и mc1. Если вы используете слово this внутри мувиклипа Graphic1, то оно будет обращаться к экземпляру mc0 (если это mc0), и к экземпляру mc1, (если это mc1). Если вы используете trace ( this._x ); в окне мувиклипа Graphic1, то вы отследите _x расположение каждого клипа.

    В ActionScript всё работает точно так же. this обращается к текущему экземпляру, а не к классу. Если рассмотреть пример на Flash'е, то:

    function Graphic1 ( x, y, name, xScale, yScale, rotation )
{
    this.x = x;
    this.y = y;
    this.name = name;
    this.xScale = xScale;
    this.yScale = yScale;
    this.rotation = rotation;
}

instance0 = new Graphic1( 100, 100, "mc0", 100, 100, 45 );
instance1 = new Graphic1( 444, 222, "mc1", 200, 200, 30 );

    Как видите, теперь есть два экземпляра, каждый со своими свойствами. Вы можете сопоставить каждый проходящий аргумент с именем каждого аргумента в классе Graphic1 (обратите внимание, что пробелов в именах функций быть не может!). Само по себе это конечно ещё не создаёт мувиклипа. Это просто несколько экземпляров класса в ActionScript, ничего конкретного. Двигаемся дальше, ребята. Поехали!..

  • Область видимости (Scope)

  • Массив аргументов (Agruments array)

    Ещё немного об аргументах. В ActionScript вы можете оперировать сразу несколькими аргументами, различающимися в функции. Вы спросите: "А зачем нам это?" Ну, скажем, у вас есть класс, который хранит имена людей. Так как полное имя человека содержит фамилию, имя и отчество, ваш класс скорее всего будет выглядеть вот так:

    function setName( first, middle, last )
{
    this.firstName = first;
    this.middleName = middle;
    this.lastName = last;
}
person0 = new setName( "John", "Hopkins", "University" );

    ... никаких проблем. Хотя постойте! А как же быть с теми, чьи отчества нам не известны? Вот незадача! Ну да ладно. Мы можем оперировать двумя аргументами, если нет отчества и тремя, если оно есть. Просто проверим, обработано ли последнее значение перед тем, как вписать отчество:

    function setName( last, first, middle )
{
    this.lastName = last;
    this.firstName = first;

    if (middle <> null)
    {
        this.middleName = middle;
    }
}
person0 = new setName( "university", "John", "Hopkins");
person1 = new setName( "Catraz", "Al" );
person2 = new setName( "Kahaule", "Al" );
person3 = new setName( "Krisnia", "Harry" );

    Замечательно! Работает!.. Но как же быть с именами японцев и других "алиенов", которые пишут имя в конце, а фамилию в начале? Наверное, вы попробуете поменять "имя" на "фамилию". Теперь самое время вспомнить французов, а заодно и их ближайших родственников :))) Такие, как Жан Пьер Жозе деБрюйи или ещё лучше вот такое имя Гастон Бернар Жозе Рене даЛак ауФонтен Баге деБрюйи. Похоже, у нас появились проблемы. Непонятно, сколько имён мы передаёт в setName(). К счастью в каждой функции есть встроенный объект под названием (как вы, наверное, уже догадались) "аргументы" (arguments). Это объект (а не группа), хранящий прошедшие аргументы в форме:

    arguments[0]
arguments[1]
arguments[2]...

    Заметьте, что объект "аргументы" доступен только внутри функции, что логично, так как прошедшие аргументы, локальны по определению. Теперь вы можете сотворить что-нибудь вроде:

    // поставим отчество после фамилии и имени
function setName( family, given )
{
    this.familyName = family;
    this.givenName = given;
    if (arguments[2] <> null)
    {
        this.middleName = new Array();
        for( var i = 2; i < arguments.length; i++)
        {
            this.middleName[i-2] = arguments[i];
        }
    }
}
person0 = new setName( "Robin", "Sean", "Debreuil"
);
person3 = new setName( "Debreuil", "Summer" );
person4 = new setName( "Debreuil", "Jean", "Pierre", "Josheph" );
person5 = new setName( "Debreuil", "Gaston", "Bernard", "Josheph",
                       "Rene", "d'Lac", "au'Fontaine", "Baguette" );

trace( person5.middleName.toString() );
// возврат будет выглядеть так:
// Bernard, Josheph, Rene, d'Lac, au'Fontaine, Baguette

    Если ваш setName остаётся вообще без аргументов, то можно прикупить их или покопаться в Monty Python, кстати, не забудьте взять чек. Если вы думаете, что проблема уже решена, то вспомните об именах китайцев. У них последовательность такова: фамилия, имя, имя. Наверное, в этом случае вам придётся разрешить пробелы в именах (обычно они пишутся в кодировке Unicode, которую Flash 5 не поддерживает). Ну а как быть, например, с "артистом, которого звали 'Принц'"? Нравится он вам или нет, но как не восторгаться огромным списком таких имён во всех базах данных мира! Интересно, придуман ли для этого уже новый символ в Unicode?

    Ну хорошо, а как обстоит дело с Flash? Какова альтернатива? Вот вы создаёте новый символ в библиотеке, передаёте ему аргументы, например, _x, _y и _level в зависимости от того, куда вы его помещаете. Такие параметры, как _xScale не установлены (и получают значения по умолчанию, в нашем случае - '100'). Однако, если дублировать экземпляр символа, то вся информация экземпляра передаётся новому экземпляру, как будто вы передаёте одну и ту же функцию (createInstanceOfSymbol) и нескольким аргументам, число которых будет зависеть от того, как именно вы решили всё это построить.

  • Прототип (prototype)

    Прототип - место, откуда класс получает дополнительные методы/свойства. Его уровень тот же, что и у класса.

    __proto__

    __proto__ - место, откуда объект наследует методы. Одним уровнем выше класса.

  • callee

    callee - это часть объекта arguments, который есть у любой функции. Вы обращаетесь к нему вот так: arguments.callee. Он возвращает текущую функцию (а не <строка> текст текущей функции, как это реализовано в JavaScript). Это логично, так как во Flash нельзя оценить скрипт в самом начале. Немного похоже на this, не так ли? Но this указывает на текущий экземпляр, а не на текущую функцию. Кстати, доступ к arguments.callee есть только в пределах функции.

    arguments.caller не поддерживается в ActionScript (это выражение обращается к объекту, который вызвал текущую функцию), JavaScript от него уже тоже отказывается.

    А вот arguments.length в ActionScript поддерживается. Это выражение обращается к числу прошедших аргументов. Его не будет видно в петле (loop'е) 'for in' объекта arguments, а вот callee будет там виден, так что будьте внимательны. (Откровенно говоря, я думаю, что и callee тоже не должен быть виден и похоже, что в этом разработчики JavaScript со мной солидарны).

  • super

    super - (java) указывает на то, от чего унаследован текущий класс. Используется, чтобы указать, какой конструктор наследник должен использовать, вызывая конструктор-родитель (его можно вызвать только первым и всего один раз). Также часто используется для получения метода или свойства, наследуемого, перезаписанного наследником. Вообще очень похож на __proto__ в ActionScript.

  • constructor
  • parent
  • child
  • grandchild
  • subClass
  • superClass
  • method

    • << ООП во Flash 5 >>


ООП во Flash 5: О проекте

О проекте

Авторские права на учебник принадлежат его создателю
Робину Дебройлу (Robin Debreuil).

Первоисточник на английском: http://www.debreuil.com/docs/
Русское зеркало: http://debreuil.flashmaster.ru/

ДАННАЯ РЕДАКЦИЯ УЧЕБНИКА ПРЕДНАЗНАЧЕНА ИСКЛЮЧИТЕЛЬНО ДЛЯ ЛИЧНОГО OFF-LINE ПОЛЬЗОВАНИЯ, ЛЮБОЕ КОММЕРЧЕСКОЕ ИСПОЛЬЗОВАНИЕ И ПУБЛИКАЦИЯ В СЕТИ НЕДОПУСТИМЫ !

Перевод учебника на русский язык сделан группой участников мейл-конференции "RuFlash" в составе:

  • Alexander Babaev [ 15, 17 ]

  • EugeneP          [ 13, 14 ]

  • Nikolay & Alex   [ 4-8, 10-12, 18-22 ] ОСОБАЯ БЛАГОДАРНОСТЬ !

  • VM               [ 1-3, 9, 16, общая редакция ]

    • Оформление: VM Studio

    10.12.2001

    RuFlash NewsLetter Мейл-Конференция "RuFlash" ("RUSSIAN FLASH NEWS")
    существует с мая 1999 года, на данный момент число ее подписчиков составляет более шестисот человек. Конференция предназначена для оперативного обмена новостями, мнениями между русскоязычными пользователями пакета Macromedia Flash, обсуждения технологии работы в нем и других аналогичных или связанных с ним программах. Разрешается обсуждение вопросов близких к тематике конференции или каким либо образом влияющих на развитие и распространение Flash технологии. Подписчиком конференции может быть любой, без каких либо ограничений, "житель" Сети, имеющий адрес электронной почты, если он согласен с ее Правилами и соблюдает их при общении с другими подписчиками.


Навигация

ООП во FLASH 5
Введение
Объекты
Классы
Экземпляры
Область видимости
Наследование
Прототипы
Старшинство
Переназначение
Защита
Аргументы
Конструктор
Методы
Свойство Proto
Массив аргументов
Метод Callee
Оператор New
Метод Extend
Метод Super
Особые методы
Заключение
Глоссарий
О проекте

© 2001
Debreuil Digital Works


ООП во Flash 5

OOP in Flash5
Автор: Robin Debreuil
Источник: www.debreuil.com/docs
© 2001 Debreuil Digital Works

Перевод сделан группой участников мейл-конференции
"RuFlash" и проекта "Flash Master"

RuFlash NewsLetter Flash Master