Казахстан, Алматы, Гимназия им. Ахмета Байтурсынова №139, 2008 |
Наследование во Flash MX
Когда нужно применять явное указание базового класса
Мы теперь знаем, что обратиться к методу базового класса можно, используя ключевое слово super . Но в длинных иерархиях иногда полезно вызвать метод суперкласса более высокого уровня (так что наш класс является его весьма дальним наследником). Как это сделать? Цепочки типа super. super не только неудобны - они еще и не работают, поскольку super не является полем. Вот пример, доказывающий это.
superclass = function(){} superclass.prototype = new Object(); superclass.prototype.say = function(text){trace(text + "superclass")} subclass1 = function(){super.say();} subclass1.prototype = new superclass(); subclass1.prototype.say = function(text){trace(text + "subclass1")} subclass2 = function(){super.say();} subclass2.prototype = new subclass1(); subclass2.prototype.say = function(text){trace(text + "subclass2")} subclass3 = function(){super.say();} subclass3.prototype = new subclass2(); subclass3.prototype.say = function(text){trace(text + "subclass3")} subclass3.prototype.say2 = function(text){super.say(text);} subclass3.prototype.say3 = function(text){super.super.say(text);} subclass3.prototype.say4 = function(text){super.super.super.say(text);} a = new subclass3(); trace("---------------"); a.say("type = "); a.say2("type = "); a.say3("type = "); a.say4("type = ");
Эта программа выводит следующее:
superclass subclass1 subclass2 --------------- type = subclass3 type = subclass2
Мы видим, что все цепочки типа super. super не сработали. Что же делать? В С++ в таких случаях можно было явно указать имя базового класса. Во ФлэшМХ такого механизма нет, но мы знаем, что средствами Флэш разрешима более общая задача - можно вызывать функцию, методом класса вовсе не являющуюся так, как если бы она этим методом была. Как вы, наверное, помните, для этого используются методы класса Function по имени call и apply. В случае, когда мы переопределяем функцию базового класса, бывает удобнее пользоваться методом apply, где аргументы не перечисляются друг за другом, а передаются целиком в объекте arguments.В примере, приведенном в начале параграфа, описание класса subclass3 можно сделать таким образом:
subclass3 = function(){super.say();} subclass3.prototype = new subclass2(); subclass3.prototype.say = function(text){trace(text + "subclass3")} subclass3.prototype.say2 = function(text){subclass2. prototype.say.apply(this, arguments);} subclass3.prototype.say3 = function(text){subclass1. prototype.say.apply(this, arguments);} subclass3.prototype.say4 = function(text){superclass. prototype.say.apply(this, arguments);}
На сей раз вывод программы будет таким:
superclass subclass1 subclass2 --------------- type = subclass3 type = subclass2 type = subclass1 type = superclass
То есть теперь все работает как надо.
Чтобы завершить эту тему, скажем еще о том, что вызов конструкторов далеких предков (если они почему-то не были вызваны своими непосредственными потомками) вам тоже придется производить, пользуясь вышеописанным способом.
"Лишние" вызовы конструкторов
При анализе двух предыдущих примеров вы могли заметить, что на печать выводится несколько строк, которые мы "не заказывали". В частности, над горизонтальной чертой выводится не только строка subclass2, соответствующая рождению объекта типа subclass3 (в иллюстративных целях мы печатали в конструкторе имя базового класса ), но и строки superclass и subclass1. Понятно, откуда они взялись: ведь прототипы мы создавали при помощи оператора new. Значит, при этом отработали конструкторы соответствующих классов. И, с одной стороны, такое поведение представляется логичным. Ведь конструктор предназначен для инициализации объекта; раз мы создаем какой-то объект, его надо инициализировать корректно, а, значит, следует вызвать конструктор. Особенно важным подобное поведение представляется в случае, когда в конструкторе происходит добавление в объект каких-либо новых методов или же делается запрос системных ресурсов (в случае Флэш это может быть создание одного или нескольких клипов). С другой стороны, мы можем рассматривать прототип всего лишь как хранилище методов класса. В таком случае всякие инициализирующие действия будут излишними. Наоборот, если вы рассчитываете, что объекты определенного класса будут создаваться к тому времени, когда будут выполнены некоторые инициализирующие действия, вас, возможно, неприятно удивят сообщения об ошибках при создании объектов этого класса, которые записываются в прототипы в процессе наследования. Ведь иерархия классов обычно выстраивается в самом начале. Можно ли обойтись без вызова конструкторов при наследовании? Оказывается, есть обходные пути. Мы обсудим их в параграфе, посвященном альтернативному наследованию и "альянсу мятежников".
Проверка типа
В некоторых случаях бывает необходимо проверить, является ли данный объект наследником некоторого класса. Хотя в грамотно спроектированной иерархии классов подобная надобность возникает нечасто. Но иногда вы пользуетесь чужими классами, менять которые нет ни желания, ни необходимости. Тогда на помощь приходит оператор instanceof. Пример: if (arg instanceof MovieClip) arg.x = 10;
Оператор instanceof - это один из способов отличить примитивные типы от объектных (второй способ - попробовать завести у объекта данного типа поля). Рассмотрим такой код:
a = new Number(5); b = 5; a.x = 3; b.x = 4; trace(a instanceof Number); trace(b instanceof Number); trace(a.x); trace(b.x);
То есть мы производим одни и те же действия над объектами а и b, причем первый из них - объектного типа, а второй - примитивного. А вот и результат:
true false 3 undefined
Мы видим, что объект b к типу Number не принадлежит и попытка завести у него поля хотя и не приводит к ошибке, но и результата никакого не дает.
Наконец, чтобы отличить переменную, в которой записано примитивное значение 5 от Number(5), мы можем проверить, на что указывает поле __proto__ нашего объекта. В случае Number оно указывает на Number.prototype, в противном случае его значением будет undefined. Эти рассуждения наводят на мысль о том, что функцию, подобную instanceof, можно сделать самостоятельно. И действительно, в онлайн-документации Флэш МХ приведен следующий пример (мы меняем только название функции, чтобы оно не совпадало с ключевым словом):
function emulated_instanceof (theObject, theClass){ while ((theObject = theObject.__proto__) != null) { if (theObject == theClass.prototype) { return true; } } return false; }
Давайте разберем этот пример подробнее, поскольку на нем вполне можно проверить, правильно ли вы представляете себе суть прототипной модели и наследования в ней. Итак, для начала выясним, объекты каких типов передаются в нашу функцию. Первый из них - скорее всего, наследник Object, некий произвольный объектный тип. Хотя, конечно, передать туда можно все, что угодно, но ожидает наша функция именно наследника Object. А вот второй аргумент имеет тип Function. Это - функция-конструктор класса, который мы предполагаем базовым для аргумента theObject (что и будем проверять). Теперь посмотрим на условие остановки цикла. На первый взгляд, всякий объект имеет свой прототип... Всякий - кроме самого первого, то есть Object.prototype! Действительно, оператор trace(Object.prototype.__proto__ == undefined) выдает true (как, впрочем, и операт ор trace(Object.prototype.__proto__ == null). Таким образом, мы собираемся добраться до самого низа иерархии и там остановиться (если по дороге ничего не найдем). Как же выглядит этот спуск? Строка theObject = theObject.__proto__ в условии цикла всякий раз заменяет объект его прототипом. Затем следует проверка - не является ли этот прототип также прототипом некоторого класса? Если да, то все в порядке, это и значит, что наш объект этому классу принадлежит. Если же нет, то следует проверить не сам текущий объект, а уже его прототип, и так далее, пока цепочка не приведет нас к Object 'у. Таким образом, рассматриваемая функция действительно полностью идентична оператору instanceof.
Изменение базовых классов
Прототипная модель наследования (вкупе с интерпретируемым языком) имеет одно неожиданное свойство. Мы можем вносить какие-то изменения в базовые классы уже после того, как объекты производных классов созданы и используются. После того, как эти изменения будут внесены, поведение всех объектов производных классов сразу же поменяется. Такую интересную возможность грех не использовать. И сейчас мы покажем, как это делается. (Хотя самые яркие примеры вы увидите позже в лекции об эмуляции множественного наследования ).