voliuf.narod.ru

главная

друзья

помощь сайту

Flash MX для профессиональных программистов

Бесплатные учебники по темам

Партнерская программа

1.Введение

2.Базовые понятия Action Script

3.Клипы и ролики. Специальные возможности языка

4.Контейнеры

5.Функции

6.Классы

7.Наследование во Flash MX

8.Эмулируем множественное наследование

9.Классы и клипы

10.Программное рисование во Flash MX

11.Компоненты: готовые и "самодельные"

12.Средства документирования во Flash MX

13.Методика организации командной работы над Flash-проектом

14.Работа флэш-программ в Internet


 


Flash MX для профессиональных программистов
7.Наследование во Flash MX
  
 

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

 

Наследование как модификация прототипа

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

 
function MyClass(){}
MyClass.prototype = new MyBaseClass();
   

и затем уже модифицируем имеющийся прототип. В результате многократного применения подобной операции возникает структура, подобная изображенной на рис.7.1.

 

 


Рис. 7.1. 

 

 

Вертикальные стрелки символизируют создание нового объекта при помощи вызова оператора new.

 

Нетрудно убедиться в том, что у нас получилось самое настоящее наследование. Сейчас мы по пунктам проверим, что все условия, необходимые для наследования, в данном случае выполнены.

 

copy on write и цепочка поиска

Предположим, что мы написали obj.a, то есть запросили значение поля a у объекта obj. Где Флэш будет искать значение этого поля? Сначала, разумеется, он посмотрит среди полей, которые мы непосредственно завели в объекте obj. Если ничего не найдено, то, как мы знаем, Флэш принимается за поиски в прототипе, который он находит с помощью ссылки __proto__. Но поиски в прототипе идут по той же самой схеме, то есть если непосредственно в прототипе искомая ссылка не нашлась, то мы идем в прототип прототипа (относительно исходного объекта это __proto__.__proto__) и так далее вплоть до прототипа типа Object (который автоматически попадает в "корень" этой цепочки, даже если у самого базового класса прототип был вовсе не указан). Очевидно, что такой механизм полностью соответствует наследованию в обычном понимании этого слова. Более того, если в производном классе создана какая-то функция с тем же именем, что и в базовом, то при поиске по цепочке прототипов будет сначала найдена функция из производного класса (и на этом поиск прекратится). То есть таким образом реализовано переопределение виртуальных функций. Мы видим, что все функции являются виртуальными. Более того, виртуальными являются также и поля. (Временами случается пожалеть об отсутствии возможности делать виртуальные поля при работе на С++ или Java.)

 

Можно ли как-нибудь узнать, добавлено ли некоторое поле непосредственно в исследуемый объект или же оно найдено где-то в цепочке __proto__? Оказывается, можно, хотя и приходится для этого использовать недокументированную функцию Object.hasOwnProperty. Вот код, который демонстрирует все, что нам нужно.

 
prot = {a: 5, b: 6};
constr = function(){}
constr.prototype = prot;
obj = new constr();
obj.a = 7;
trace("obj.a = " + obj.a);
trace("obj.b = " + obj.b);
trace("Has own property a ? " + obj.hasOwnProperty("a"));
trace("Has own property b ? " + obj.hasOwnProperty("b"));
obj.b = undefined;
delete obj.a;
trace("-------------");
trace("obj.a = " + obj.a);
trace("obj.b = " + obj.b);
trace("Has own property a ? " + obj.hasOwnProperty("a"));
trace("Has own property b ? " + obj.hasOwnProperty("b"));
       

Вот что получается в результате:

 
obj.a = 7
obj.b = 6
Has own property a ? true
Has own property b ? false
-------------
obj.a = 5
obj.b =
Has own property a ? false
Has own property b ? true
       

Итак, что же произошло? Мы создали объект prot, который использовали в качестве прототипа объекта типа constr. Собственно говоря, тут еще не было наследования как такового, ибо prot - это единичный объект, не описывающий тип. Если бы мы непременно хотели устроить наследование, мы могли бы сделать этот объект прототипом некоего класса Base, а затем объект этого класса использовать в качестве прототипа класса constr. Впрочем, мы пока что иллюстрируем не наследование, а цепочку поиска. Поэтому мы нарочно подчеркнули в названии constr тот факт, что мы используем эту функцию в качестве конструктора, а то, что эта функция-объект задает класс, нам не очень важно. В конце концов, мы могли обойтись вовсе без конструктора, а написать obj.__proto__ = prot. Можете убедиться, что этот вариант тоже работает; более подробно мы его разберем, когда будем говорить об альтернативном наследовании (в предпоследнем параграфе этой лекции).

 

Так или иначе, мы получили объект со своими собственными полями и с полями, унаследованными из прототипа. Мы видим, что функция Object.hasOwnProperty работает так, как ожидалось. Заодно убеждаемся в том, что уничтожить поле и присвоить ему значение undefined - это разные вещи. В последнем случае значения из прототипа скрываются свежеприсвоенным значением undefined.

 

Работа с прототипами различных уровней

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

 

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

 

Как работает for...in, если есть базовые классы

Прежде всего, еще раз напомним одну особенность работы оператора for...in, о которой мы уже писали во второй лекции. А именно, поля, созданные позже, встречаются при переборе раньше (за исключением созданных в фигурных скобках в процессе инициализации объекта). Логично, что и естественный порядок перебора прототипов (то есть сначала находим все свойства в самом объекте, потом ищем в прототипе, потом в прототипе прототипа и так далее) меняется на противоположный. А, возможно, в начале работы оператора for...in Флэш производит перебор полей в прямом порядке, складывает все имена в какой-нибудь буфер и лишь потом начинает выполнять тело цикла, вынимая имена из буфера в обратном порядке. Удивительно, что в онлайн-документации ни слова про этот обратный порядок не сказано. Но, как бы то ни было, мы сейчас продемонстрируем все вышеописанные свойства оператора for...in на простом примере. Запускаем следующий код:

 
obj1 = {a: 1, b: 2};
obj2 = {b: 3, c: 4};
obj3 = {c: 5, d: 6};
   // Вместо нормального наследование просто
   // определяем __proto__ - в данном случае
   // это ничего не меняет
obj3.__proto__ = obj2;
obj2.__proto__ = obj1;

for(name in obj3){
   trace("obj3." + name + " = " + obj3[name]);
}
       

и получаем в результате:

 
obj3.a = 1
obj3.b = 3
obj3.c = 5
obj3.d = 6
       

Итак, мы видим, что соответствующие переменные взяты из тех объектов, где они впервые появились в цепочке __proto__ - считая от того объекта, у которого мы запрашиваем переменную. Можно сказать и по-другому: переменные берутся из объекта "самого производного" класса, в котором они были переопределены. Все же первый вариант лучше, ведь мы сейчас, фактически, не создавали никаких классов, а сделали только несколько объектов, связанных цепочкой ссылок __proto__. Также мы видим и описанный выше "обратный порядок перебора": до поля a дольше всего добираться по цепочке __proto__, а выводится оно первым. (И алфавитный порядок здесь ни при чем; вы можете переименовать переменные и убедиться, что от их имен порядок вывода не зависит.) Забавно, что инициализировать ссылки __proto__ для создания цепочки можно прямо при создании объектов при помощи примерно такого кода:

 
obj1 = {a: 1, b: 2};
obj2 = {__proto__: obj1, b: 3, c: 4};
obj3 = {__proto__: obj2, c: 5, d: 6};
for(var name in obj3){
   trace("obj3." + name + " = " + obj3[name]);
}
       

Этот код дает точно такой же результат, что и предыдущий.

 

Пример: копирование массива с помощью рекурсивного снятия защиты

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

 

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

 
   // Эта функция снимает защиту со всех полей
   // всех объектов в цепочке __proto__ аргумента.
_global.unhideAll = function(obj){
   ASSetPropFlags(obj,null,0,1);
      // На null можно не проверять, поскольку
      // null == undefined, хотя и null !== undefined.
   if (obj.__proto__ != undefined) unhideAll(obj.__proto__);
}
_global.copyObj = function(toObj, fromObj){
      // снимаем защиту от for...in
   unhideAll(fromObj);
      // и затем копируем все, что видит for...in -
      // то есть все, кроме переопределенных переменных
      // или функций
   for (var fieldName in fromObj){
      toObj[fieldName] = fromObj[fieldName];
   }
}
   // Проверяем, удается ли скопировать таким образом массив.
someObj = {};
arr = [2, 6, 0];
copyObj(someObj, arr);
   // Убеждаемся, что массив не только скопировался, но и
   // работают его методы.
trace("Отсортированный массив = [" + someObj.sort() + "]");
       

На выходе получим

 
Отсортированный массив = [0,2,6]
       

Так что у нас действительно произошло полноценное копирование. Все же напомним, что скопировать массив можно быстрее и удобнее при помощи метода slice, написав примерно следующее: copy_array = source_array.slice(0, source_array.length);

 

Вывод полного содержимого объекта

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

 

Для системных объектов вывести все их содержимое в правильном порядке довольно просто (поскольку содержимое прототипа всегда скрыто). Алгоритм здесь такой: при помощи for...in выводим поля, непосредственно принадлежащие объекту. Возможно, их надо будет сначала раскрыть при помощи ASSetPropFlags. Затем в качестве текущего объекта выбираем прототип, то есть содержимое ссылки __proto__. И повторяем вышеописанную операцию. Так действуем до тех пор, пока в __proto__ хоть что-нибудь есть (на деле мы всегда упираемся в Object). C произвольным объектом такая операция не пройдет, поскольку в for...in сразу же обнаружатся поля из прототипов различных уровней вложенности. Можно, конечно, разыскать все эти прототипы заранее и скрыть все присутствующие в них поля и методы. Но если уж применять подобные "силовые приемы", то лучше воспользоваться временным "отцеплением цепочки", о котором мы сейчас расскажем.

 

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

 
   // Печатаем имя и значение поля, но не смешиваем
   // пустую строку и undefined
_global.printField = function(name, value){
   if (value == undefined) value = "undefined";
   trace(name + ": " + value)
}
   // Печатаем все, что доступно функции for...in
   // Чтобы увидеть скрытые поля, снимаем с них защиту
   // Если не предпринять никаких мер, эта функция
   // выведет как непосредственные поля и методы объекта,
   // так и открытое содержимое его прототипов всех уровней.
_global.printFieldsByForIn = function(obj, str, tempProto){
   trace(":::::::::::  " + str + "  ::::::::::::");
trace(":::::::::::::::::::::::::::::::::::::::::::::::::::::::::");
      // Снимаем "защиту" со скрытых полей.
   ASSetPropFlags(obj, null, 0, 1);
   for(var name in obj){
         // Принимаем меры для того, чтобы наши действия
         // с обнулением __proto__ не отражались на выводимой
         // информации.
      if (name == "__proto__") printField(name, tempProto);
      else printField(name, obj[name]);
   }
   trace(":::::::::::::::::::::::::::::::::::::::::::::::::::::::::");
}
   // В этой рекурсивной функции мы, собственно, и реализуем
   // вышеописанный фокус с "отцеплением цепочки" __proto__ .
   _global.printAllFields = function(obj, name){
   var tempProto = obj.__proto__;
   obj.__proto__ = null;
   printFieldsByForIn(obj, name, tempProto);
   obj.__proto__ = tempProto;
      // Проверка на null не нужна: null == undefined, хотя
      // отличить их и можно при помощи оператора ===.
   if (obj.__proto__ != undefined)
      printAllFields(obj.__proto__, name + ".__proto__");
}
// А эта функция просто вызывает основную рабочую функцию и
// добавляет "элементы оформления" (в текстовом виде, разумеется).
_global.dumpObj = function(obj, name){
   trace("================================================");
   if (name == undefined) name = "<Dumped object>";
   printAllFields(obj, name);
   trace("================================================");
   trace("");
}
   // Теперь тестируем функцию dumpObj на разных объектах.
   // Сначала на простейшем объекте с одним полем.
myObject = new Object();
myObject.a = 10;
dumpObj(myObject, "myObject");
   // Затем тестируем на массиве.
someArray = [1, 2, 4, 7];
dumpObj(someArray, "someArray");
   // Далее тестируем на примитивной сроке.
a = "Примитивная строка";
dumpObj(a, "Примитивная строка");
   // И, наконец, на системном объекте _root, который
   // представляет из себя корневой MovieClip.
dumpObj(_root, "_root");
   

Выводит этот код очень большое количество текста, но текст этот того стоит - ведь это внутренности разных системных объектов, что может быть интереснее? Вот что выводится в консоль:

 
==========================================================
:::::::::::  myObject  ::::::::::::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
a: 10
__constructor__: [type Function]
constructor: [type Function]
__proto__: [object Object]
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:::::::::::  myObject.__proto__  ::::::::::::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
__proto__: undefined
toLocaleString: [type Function]
isPropertyEnumerable: [type Function]
isPrototypeOf: [type Function]
hasOwnProperty: [type Function]
toString: [type Function]
valueOf: [type Function]
addProperty: [type Function]
unwatch: [type Function]
watch: [type Function]
constructor: [type Function]
==========================================================
:::::::::::  someArray  ::::::::::::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
3: 7
2: 4
1: 2
0: 1
__proto__:
constructor: [type Function]
length: 4
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:::::::::::  someArray.__proto__  ::::::::::::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
sortOn: [type Function]
reverse: [type Function]
sort: [type Function]
toString: [type Function]
splice: [type Function]
join: [type Function]
slice: [type Function]
unshift: [type Function]
shift: [type Function]
concat: [type Function]
pop: [type Function]
push: [type Function]
__proto__: [object Object]
constructor: [type Function]
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:::::::::::  someArray.__proto__.__proto__  ::::::::::::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
__proto__: undefined
toLocaleString: [type Function]
isPropertyEnumerable: [type Function]
isPrototypeOf: [type Function]
hasOwnProperty: [type Function]
toString: [type Function]
valueOf: [type Function]
addProperty: [type Function]
unwatch: [type Function]
watch: [type Function]
constructor: [type Function]
==========================================================
:::::::::::  Примитивная строка  ::::::::::::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:::::::::::  Примитивная строка.__proto__  ::::::::::::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
substr: [type Function]
split: [type Function]
substring: [type Function]
slice: [type Function]
lastIndexOf: [type Function]
indexOf: [type Function]
concat: [type Function]
charCodeAt: [type Function]
charAt: [type Function]
toLowerCase: [type Function]
toUpperCase: [type Function]
toString: [type Function]
valueOf: [type Function]
__proto__: [object Object]
constructor: [type Function]
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:::::::::::  Примитивная строка.__proto__.__proto__  ::::::::::::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
__proto__: undefined
toLocaleString: [type Function]
isPropertyEnumerable: [type Function]
isPrototypeOf: [type Function]
hasOwnProperty: [type Function]
toString: [type Function]
valueOf: [type Function]
addProperty: [type Function]
unwatch: [type Function]
watch: [type Function]
constructor: [type Function]
==========================================================
:::::::::::  _root  ::::::::::::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
toLocaleString: [type Function]
isPropertyEnumerable: [type Function]
isPrototypeOf: [type Function]
hasOwnProperty: [type Function]
toString: [type Function]
valueOf: [type Function]
addProperty: [type Function]
unwatch: [type Function]
watch: [type Function]
a: Примитивная строка
someArray: 1,2,4,7
myObject: [object Object]
__proto__: [object Object]
constructor: [type Function]
$appPath: file:///K|/Program%20Files/Macromedia/Flash%20MX/
$version: WIN 6,0,21,0
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:::::::::::  _root.__proto__  ::::::::::::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
createTextField: [type Function]
clear: [type Function]
endFill: [type Function]
lineStyle: [type Function]
curveTo: [type Function]
lineTo: [type Function]
moveTo: [type Function]
beginGradientFill: [type Function]
beginFill: [type Function]
createEmptyMovieClip: [type Function]
stopDrag: [type Function]
startDrag: [type Function]
removeMovieClip: [type Function]
duplicateMovieClip: [type Function]
gotoAndStop: [type Function]
gotoAndPlay: [type Function]
prevFrame: [type Function]
nextFrame: [type Function]
stop: [type Function]
play: [type Function]
setMask: [type Function]
getDepth: [type Function]
attachVideo: [type Function]
attachAudio: [type Function]
getBytesLoaded: [type Function]
getBytesTotal: [type Function]
getBounds: [type Function]
hitTest: [type Function]
globalToLocal: [type Function]
localToGlobal: [type Function]
swapDepths: [type Function]
attachMovie: [type Function]
loadMovie: [type Function]
loadVariables: [type Function]
unloadMovie: [type Function]
getURL: [type Function]
meth: [type Function]
tabIndex: undefined
enabled: true
useHandCursor: true
__proto__: [object Object]
constructor: [type Function]
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:::::::::::  _root.__proto__.__proto__  ::::::::::::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
__proto__: undefined
toLocaleString: [type Function]
isPropertyEnumerable: [type Function]
isPrototypeOf: [type Function]
hasOwnProperty: [type Function]
toString: [type Function]
valueOf: [type Function]
addProperty: [type Function]
unwatch: [type Function]
watch: [type Function]
constructor: [type Function]
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
   

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

 

Во-вторых, вы можете заметить, что наш фокус с обнулением __proto__ не сработал на клипе _root - в него все равно попали методы из класса Object. (Почему только из Object? Дело в том, что непосредственный прототип объекта _root - это MovieClip, но снять защиту с его методов мы к моменту вызова dumpObj для _root еще не успели. А с Object защита уже была снята.) В остальных случаях такого не произошло. Отсюда вывод: все дело в том, что _root - слишком важный системный объект, чтобы позволять его поведению зависеть от того, что лежит у него в __proto__. (А, может, дело в том, как именно было удобнее всего реализовать корневой клип.) Во всяком случае, о том, что за объект является прототипом клипа _root, система Flash MX догадывается без обращения к полю _root.__proto__ и поэтому изменение его значения ничего не дает. О том, что это п оведение не является общим для других клипов, можно узнать, вызвав функцию dumpObj для произвольного клипа. Для этого можно в конец вышеприведенного кода вставить такие строчки:

 
_root.createEmptyMovieClip("newClip", 1);
dumpObj(newClip, "mc");
   

Только что раскрытые функции Object и MovieClip в секции, где выводятся собственные поля и методы клипа newClip, не обнаруживаются.

 

Доступ к базовому классу: ключевое слово super

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

 

Но есть в системе Флэш МХ и механизм, который специально предназначен для решения рассматриваемой задачи. Этот механизм программистам, применяющим в своей работе язык Java, прекрасно известен. Основан он на использовании ключевого слова super. С помощью этого ключевого слова можно вызвать конструктор базового класса - пишем super(arg1, arg2, ...). Список аргументов ничем не отличается от списка аргументов любой другой функции. При этом конструктор вызывается не как обычная функция (в которой this означает объект, в котором лежит ссылка на нее), а именно как конструктор (this указывает на вновь созданный объект). Если же нужно обратиться к какому-либо полю или методу базового класса, используется конструкция вида super.a или super .func() (что, как правило, применяется при вызове базового варианта переопределяемой виртуальной функции).

 

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

 

Последняя особенность "ссылки" super вытекает из предыдущих: перед ней не надо писать this.

 

Когда нужно применять явное указание базового класса

Мы теперь знаем, что обратиться к методу базового класса можно, используя ключевое слово 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.

 

Изменение базовых классов

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

 

Пример изменения работы иерархии

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

 
_global.Base = function(a, b){
   this.a = a;
   this.b = b;
}
_global.Base.prototype.printAll = function(){
   for (var name in this){
      trace("this." + name + " = " + this[name]);
   }
}
_global.Derived = function(a, b, c, d){
   super(a, b);
   this.c = c;
   this.d = d;
}
_global.Derived.prototype = new Base();
b = new Base(10, 20);
d = new Derived(15, 25, 35, 45);
trace("================ b ===============");
b.printAll();
trace("==================================");
trace("================ d ===============");
d.printAll();
trace("==================================");
_global.Base.prototype.printAll = function(){
   trace("Function is obsolete!");
}
trace("================ b ===============");
b.printAll();
trace("==================================");
trace("================ d ===============");
d.printAll();
trace("==================================");
       

На выходе получаем:

 
================ b ===============
this.printAll = [type Function]
this.b = 20
this.a = 10
==================================
================ d ===============
this.printAll = [type Function]
this.d = 45
this.c = 35
this.b = 25
this.a = 15
==================================
================ b ===============
Function is obsolete!
==================================
================ d ===============
Function is obsolete!
==================================
       

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

 

Добавление функций в Object

Чаще всего динамическое изменение иерархии применяется для заведения функций, в чем-то аналогичных глобальным. Точнее, для создания методов, применимых к любому объекту. Для этого приходится менять самый базовый класс иерархии, то есть Object.

 

В качестве простого примера мы сейчас внедрим в Object слегка модифицированную функцию printAll(), которая фигурировала у нас в предыдущем подпараграфе. Мы лишь добавим ей аргумент - строку с именем объекта, поля которого она выводит. Вот каким в результате будет код нашего примера:

 
Object.prototype.printAll = function(obj){
   if (obj == undefined) obj = "[Object]";
   for (var name in this){
      trace(obj + "." + name + " = " + this[name]);
   }
}

a = [1, 3, 5, 7];
a.printAll("a");
   

На выходе получим следующее:

 
a.printAll = [type Function]
a.3 = 7
a.2 = 5
a.1 = 3
a.0 = 1
   

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

 

А теперь пару слов о "правилах хорошего тона". Мы видим, что при перечислении полей произвольного объекта обнаруживается свежедобавленная функция printAll. Ничего хорошего в этом нет; особенно такие вещи будут раздражать программистов, использующих ваш код, у которых вдруг ваши функции начнут появляться в каждом отладочном дампе полей произвольного объекта. Поэтому следует воспользоваться известной нам недокументированной функцией ASSetPropFlags и спрятать новый метод так же, как спрятаны системные. Правильный код будет вот таким:

 
Object.prototype.printAll = function(obj){
   if (obj == undefined) obj = "[Object]";
   for (var name in this){
      trace(obj + "." + name + " = " + this[name]);
   }
}
   // Прячем новую функцию от for...in
ASSetPropFlags(Object.prototype, "printAll", 1);

a = [1, 3, 5, 7];
a.printAll("a");
   

и на выходе мы, разумеется, получим

 
a.3 = 7
a.2 = 5
a.1 = 3
a.0 = 1
   

То есть теперь выводятся только необходимые нам данные, без всяких следов самой функции printAll.

 

Клонирование объектов-функций

Еще один базовый класс, в который часто добавляют новые методы - это Function. Мы с вами уже сталкивались с этим приемом, когда обсуждали способы создания статических и приватных свойств. Теперь мы рассмотрим еще один пример. Предположим, создавая свои функции, вы заводите у каждой функции-объекта поле name (с тем, чтобы использовать его в отладочных целях). Это удобно ровно до тех пор, пока вы не решите записать ссылку на функцию-объект в поле с другим именем. (Зачем такое может понадобиться? Например, вы хотите, чтобы два ваших класса из разных иерархий имели общую функцию, а возиться с множественным наследованием ради одной функции у вас нет охоты. Названия же этих функций могут быть заданы заранее в каждой из иерархий, если эти функции переопределяют некоторые виртуальные функции из базовых классов). Проблема, которая возникает в таком случае, вполне очевидна: поскольку эти (одинаковые) методы называются по-разному, то и поле name у каждой из этих функций-объектов должно быть свое. А ведь мы собрались сделать две ссылки, ссылающиеся на один и тот же объект - понятно, что в таком случае сделать поля name разными будет невозможно. Выходит, объекты нам придется сделать разными. (Но, в конечном счете, должна вызываться одна и та же функция). Конечно, создавать новый объект-функцию при помощи конструкции = function всякий раз вручную вовсе несложно. Тем не менее, вам может показаться более удобным заранее создать в Function.prototype метод clone, который заодно и поле name установит в нужное значение. (Аналогичным образом приходится действовать во многих случаях, когда мы храним какую-либо информацию в полях объектов-функций). Итак, вот код, который реализуем намеченный нами алгоритм.

 
Function.prototype.clone = function(newName, owner){
// Сохраняем сылку на клонируемую функцию
// для использования внутри функции-клона.
   var thisFunc = this;
   var newFunc = function(){
// Добавляем аргументы, чтобы поддержать отладочные возможности.
      return thisFunc.apply(owner,
         arguments.concat(["cloned_func", arguments]));
   }
   for (var fieldName in this){
      newFunc[fieldName] = this[fieldName];
   }
   newFunc.name = newName;
   return newFunc;
}
   // Прячем новую функцию от for...in
ASSetPropFlags(Function.prototype, "clone", 1);
   // Отладочная функция для определения имени и аргументов
   // функции, находящейся в стеке вызовов на 2 уровня выше.
   // (Для того, чтобы узнать, что творится на 1 уровень выше
   // писать специальную функцию не нужно).
   _global.whereWeAre = function(caller_args){
   var funcName, argsNum = caller_args.length;
   var firstCallerArgs = caller_args;
   trace("--------------------------");
      // Ищем объект arguments той функции, которая вызвала
      // отлаживаемую (внутрь отлаживаемой мы поместим вызов
      // функции whereWeAre). Если использовались отладочные
      // аргументы, то найти его просто.
   if (firstCallerArgs[firstCallerArgs.length - 2] == "caller_args"){
      firstCallerArgs = firstCallerArgs[firstCallerArgs.length
         - 1];
     
      // Учтем возможность клонирования функций. Мы должны пройти
      // по цепочке клонов до самой последней функции-клона,
      // имя которой нам и необходимо узнать.
      while(firstCallerArgs[firstCallerArgs.length - 2] == "cloned_func"){
         firstCallerArgs = firstCallerArgs[firstCallerArgs.length
            - 1];
      }
           
         // Найдя нужную функцию, запоминаем ее имя,
         // а также количество аргументов.
      funcName = firstCallerArgs.callee.name + " ";
      argsNum = firstCallerArgs.length;
   }
   else{
   // Однако, возможно, что отлаживаемая функция была вызвана
   // без отладочных аргументов. В таком случае доступно только
   // имя вызывающей функции, но не ее аргументы.
      funcName = firstCallerArgs.caller.name;
      if (funcName != undefined){
         trace("Мы находимся в функции " + funcName);
      }
      else{
         trace("Имя вызывающей функции недоступно.");
      }
      trace("Агрументы вызывающей функции недоступны.");
      trace("==============================================\n");
      return;
   }
   var hasArgs = "с аргументами:";
      // Отладочные аргументы не считаются
   if (argsNum <= 0) hasArgs = "без аргументов."
   trace("Мы находимся в функции " + funcName + hasArgs);
   for(var i=0; i<argsNum; i++){
      trace("arguments[" + i + "]=
" + firstCallerArgs[i]);
   }
   trace("==============================================\n");
}
   // Теперь тестируем, что у нас получилось.
   // Для пробы вызываем whereWeAre просто так.
whereWeAre();
   // Эта функция будет играть роль отлаживаемой.
   // В нее мы помещаем вызов whereWeAre.
function f(){
   trace("Вызвана внутренняя функция f()");
   whereWeAre(arguments);
}
   // Эта функция будет играть роль внешней.
   // Ее имя и аргументы мы будем стараться  вывести с помощью
   // whereWeAre. И ее же мы будем клонировать.
function g(a, b){
   trace("Вызвана внешняя функция");
   f(a+1, b+1, "caller_args", arguments);
}
g.name = "g()";
   // Клонируем.
g1 = g.clone("g1()", this);
   // Вызываем обычную и клонированную функции.
g(1, 2);
g1(3, 4);
   // А в этой функции мы проверим, как whereWeAre справится
   // с отсутствием отладочных аргументов.
strangeFunc = function(){
   trace("Вызвана еще одна функция, в которой не учтены");
   trace("наши новые отладочные возможности.");
   f();
}
strangeFunc.name = "strangeFunc()";
strangeFunc("Some argument");
       

Вот что мы получаем, запустив этот код на выполнение:

 
--------------------------
Имя вызывающей функции недоступно.
Агрументы вызывающей функции недоступны.
====================================================

Вызвана внешняя функция
Вызвана внутренняя функция f()
--------------------------
Мы находимся в функции g() с аргументами:
arguments[0]= 1
arguments[1]= 2
====================================================

Вызвана внешняя функция
Вызвана внутренняя функция f()
--------------------------
Мы находимся в функции g1() с аргументами:
arguments[0]= 3
arguments[1]= 4
====================================================

Вызвана еще одна функция, в которой не учтены
наши новые отладочные возможности.
Вызвана внутренняя функция f()
--------------------------
Мы находимся в функции strangeFunc()
Агрументы вызывающей функции недоступны.
====================================================
       

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

 

Альтернативное наследование ("альянс мятежников")

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

 
superclass = function(){
   trace("constructor of superclass");
}
superclass.prototype.say = function(text){trace(text +
   "superclass")}
subclass1 = function(){
   super();
   super.say();
}
// Вот обычное наследование
subclass1.prototype = new superclass();
subclass1.prototype.say = function(text){trace(text +
   "subclass1")}
a = new subclass1();
trace("---------------");
a.say("type = ");
           

Запустим это код и получим:

 
constructor of superclass
constructor of superclass
superclass
---------------
type = subclass1
           

Мы еще раз убедились, что обычное наследование неудобно из-за двойного вызова конструкторов. Но есть ряд вещей, которые нам помогут избежать этого! Во-первых, учтем, что при создании функции сразу же создается дополнительный пустой объект, на который указывает ссылка prototype. То есть при создании самого базового класса вовсе необязательно писать superclass.prototype = new Object() (и этим фактом мы регулярно пользовались). Во-вторых, нам надо добиться, чтобы объект, на который указывает prototype, воспринимался как объект определенного класса (базового); этот объект мы будем затем модифицировать. Мы знаем, что для этого нужна запись наподобие subclass1.prototype.__proto__ = superclass.prototype. Наконец, мы говорили, что выполнение оператора new не только создает новый объект и устанавливает ему свойство __proto__, но и инициализирует новому объекту еще два свойства по имени constructor и __constructor__. Свойство constructor нам сейчас не понадобится, а вот без __constructor__ не работает вызов базового конструктора через super(). Соответственно, именно на конструктор базового класса и должен указывать __constructor__. Итак, модифицируем вышеприведенный код следующим образом:

 
superclass = function(){
   trace("constructor of superclass");
}
superclass.prototype.say = function(text){trace(text +
   "superclass")}
subclass1 = function(){
   super();
   super.say();
}
// Следующие две строки - это и есть альтернативное наследование
subclass1.prototype.__proto__ = superclass.prototype;
subclass1.prototype.__constructor__ = superclass;
subclass1.prototype.say = function(text){trace(text +
   "subclass1")}
a = new subclass1();
trace("---------------");
a.say("type = ");
           

Запускаем этот код и получаем:

 
constructor of superclass
superclass
---------------
type = subclass1
           

Этот способ наследования действительно помог разрешить нам проблему двойного вызова конструкторов. Если он вам понравился, вы, возможно, захотите записывать наследование при помощи одного оператора вместо двух. В таком случае можете сделать специальную функцию (мы по традиции, принятой в среди сторонников такого наследования, называем ее cExtends, что является сокращением от custom extends), которая будет делать то, что мы написали выше. Эту функцию можно положить прямо в прототип класса Function, чтобы его можно было вызвать из любого объекта-функции. Вот как это делается:

 
Function.prototype.cExtends = function(base){
   this.prototype.__proto__ = base.prototype;
   this.prototype.__constructor__ = base;
}
superclass = function(){
   trace("constructor of superclass");
}
superclass.prototype.say = function(text){trace(text +
   "superclass")}
subclass1 = function(){
   super();
   super.say();
}
// Сокращенная форма альтернативного наследования
subclass1.cExtends(superclass);
subclass1.prototype.say = function(text){trace(text +
   "subclass1")}
a = new subclass1();
trace("---------------");
a.say("type = ");
           

Запустив этот код, мы получим то же самое, что и в прошлый раз - функция cExtends замечательным образом сработала. Но нет предела совершенству - можно еще улучшить нашу замечательную функцию cExtends. Во-первых, неплохо бы сделать, чтобы поле __constructor__ было скрыто - так же, как и в том случае, когда его создает оператор new. Для этого надо в cExtends добавить строчку

 
ASSetPropFlags(this.prototype, "__constructor__", 1);
           

Для __proto__ ничего такого делать не надо - это свойство уже присутствует (и является скрытым) в prototype с самого начала. Во-вторых, вы, наверное, помните, что в прошлой лекции мы обсуждали, как сделать статические (принадлежащие целому классу) свойства. При этом свойства у нас хранились в полях функции-конструктора класса. Можно ли сделать эти статические свойства наследуемыми? Оказывается, да, и для этого функцию cExtends нужно модифицировать следующим образом:

 
Function.prototype.cExtends = function(base){
   this.prototype.__proto__ = base.prototype;
   this.prototype.__constructor__ = base;
   ASSetPropFlags(this.prototype, "__constructor__", 1);
   this.__proto__ = base;
}
           

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

 

Приведенный только что вариант функции cExtends придуман опять-таки T. Гролео. Он же приводит в своей статье http://timotheegroleau.com/Flash/articles/private_static.htm довольно романтическую историю изобретения альтернативного наследования. Мы дополнили эту историю своими изысканиями, и сейчас сообщим ее вам. Группа исследователей Флэш под предводительством Дэйва Янга (чью статью об альтернативном наследовании вы также можете почитать вот по этой ссылке: http://www.quantumwave.com/flash/inheritance.html ), собравшаяся вокруг сайта FlashСoders, поставила себе целью разработать механизм наследования во Флэше, лишенный недостатков стандартного варианта. Эта группа взяла себе название Rebel Alliance ("альянс мятежников") - по мотивам Rebel Alliance из "Звездных войн". Было придумано несколько различных вариантов наследования. Однако все они имели определенные недостатки. Наконец, молодой джедай Питер Эдвардс обнаружил, что единственная серьезная дырка наследования при помощи __proto__ - невозможность вызвать базовый конструктор через super() - затыкается с помощью недокументированной ссылки __constructor__. После этого подвига даже выдвигаласть идея предложить Эдвардсу пост магистра Йоды (невзирая на то, что Эдвардс наверняка не подходит, ибо настоящий Йода должен быть исключительно мал ростом и очень уродлив). Так или иначе, альянс одержал убедительную победу.

 

Однако, похоже, что империя нанесла ответный удар. Сайт FlashСoders http://chattyfig.figleaf.com/flashcoders-wiki закрыт (а ведь на него указывали едва ли не все ссылки Интернета, касающиеся недокументированных возможностей Флэш). Закрыты и остальные сайты подобного плана (например сайт, с которого можно было скачать онлайн-документацию по недокументированным функциям - в формате, пригодном для установки в среду Флэш МХ). Последним прибежищем истинных джедаев остается интернет-архив web.archive.org, на котором, в частности сохранились документы конференции flashcoders-wiki. Архив заглавной страницы находится здесь: http://web.archive.org/web/20040605070406/chattyfig.figleaf.com/flashcoders-wiki/.

 

Еще остался список рассылки Flashcoders, подписаться на который можно здесь: http://chattyfig.figleaf.com/mailman/listinfo/flashcoders, а вот здесь http://chattyfig.figleaf.com/pipermail/flashcoders/ находится его архив переписки.

 

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

 

Наследование от необычных типов

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

 

Пример наследования от Number

Давайте возьмем кусочек приведенного в этой лекции примера наследования и заменим при создании прототипа new Object() на new Number(5) (число 5 здесь, очевидно, выбрано случайно). Посмотрим на две вещи: работает ли наследование и сохранились ли у наследников свойства Number. Пишем следующий код:

 
superclass = function(){}
superclass.prototype = new Number(5);
superclass.prototype.say = function(text){trace(text +
   "superclass")}
subclass1 = function(){super.say();}
subclass1.prototype = new superclass();
subclass1.prototype.say = function(text){trace(text +
   "subclass1")}
a = new subclass1();
trace("---------------");
a.say("type = ");
trace("\nНаследование работает. Теперь тестируем, ");
trace("сохранились ли у наследников свойства Number.\n");
x = new superclass();
trace("x + 4 = " + (x + 4));
trace("x = " + x);
trace("Теперь выводим просто х с помощью trace(x)");
trace(x);
trace("\nА теперь смотрим, как ведет себя x.__proto__");
trace("x.__proto__ = " + x.__proto__);
trace("x.__proto__ + 4 = " + (x.__proto__ + 4));
       

На выходе немедленно получаем:

 
superclass
---------------
type = subclass1

Наследование работает. Теперь тестируем,
сохранились ли у наследников свойства Number.

x + 4 = 4
x =
Теперь выводим просто х с помощью trace(x)
[type Object]

А теперь смотрим, как ведет себя x.__proto__
x.__proto__ = 5
x.__proto__ + 4 = 9
       

Видим, что наследование работает, а вот свойства Number у наследников никак не проявляются. (На всякий случай мы проверили, что у прототипа все в порядке с этими свойствами). Заметьте, что для наследования мы создавали объект Number при помощи new. Только так мы можем получить не read-only объект типа Number, в который можно добавить функции.

 

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

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

 

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

 

Нужно заметить, что нас поджидают две сложности. Первая: оператор new не может создать новый объект типа Function. К счастью, вместо него можно использовать оператор function, создающий функции "на лету". Вторая сложность: до производного объекта-функции нужно добраться в момент вызова самой базовой функции (не ее методов) - иначе мы не сможем реализовать полиморфизм. Цепочка __proto__, даже правильно выстроенная, нам здесь не поможет: она обеспечивает работу this, но сама ссылка this внутри функции указывает не на объект этой функции, а на объект, к которому она принадлежит. Так что ссылку на производную функцию-объект в базовую нужно передавать "вручную". К счастью, этот механизм мы можем встроить в момент создания нового объекта в функции newFunc, которая в этой программе будет выполнять роль оператора new в пр именении к функциям.

 

Вот код, который реализует изложенные выше идеи:

 
/* ==================================================== */
/* === Функция для создания новых объектов-функций ====*/
/* ==================================================== */
   // Функция, аналогичная оператору new
   // Передавать второй аргумент необязательно,
   // это делается здесь только в отладочных целях
_global.newFunc = function(somefunc, name){
   var f = function(){
         // Мы должны передать исходный объект
         // (ведь у нас нет здесь полиморфного this)
      var newArgs = arguments;
         // Проверяем, что мы в самом дальнем подклассе
      if (arguments[arguments.length - 1] !=
         "Derived class arg passed")
            // И передаем в аргументах ссылку на себя
            // и информацию об этом
         newArgs = arguments.concat(
            arguments.callee, "Derived class arg passed"
         );
        
         // При реальном использовании надо убрать эти
         // отладочные вызовы
      trace("------------");
      trace("function: " + arguments.callee.name);
      trace("arguments = " + arguments);
      trace("newArgs = " + newArgs);
      trace("------------");
     
         // Вызываем исходную функцию для нашего объекта
      return somefunc.apply(this, newArgs);
   }
      // Копируем все поля и подобъекты, которые есть у нашей
      // функции (это тоже могут быть функции)
   copyObj(f, somefunc);
      // В отладочных целях присваиваем строковое поле с именем
   if (name != undefined) f.name = name;
   return f;
}
/* ==================================================== */
/* ========= Важная утилита - функция копирования ===== */
/* ===================================================== */

// Усовершенствованная функция рекурсивного копирования объектов
_global.copyObj = function(toObj, fromObj){
   for (var fieldName in fromObj){
      var type = typeof(fromObj[fieldName]);
      // Ссылки на объекты требуют копирования этих объетов
      if (type == "object" ){ // клипы игнорируем
         if ((fromObj[fieldName]) instanceof Array){
            // Если это массив, его надо скопировать с помощью slice
            toObj[fieldName] = fromObj[fieldName].slice(
               0, fromObj[fieldName].length
            );
         }
         else{ // Иначе вызываем copyObj рекурсивно
            toObj[fieldName] = {};
            copyObj(toObj[fieldName], fromObj[fieldName]);
         }
      } 
      else if (type == "function"){
            // Объекты-функции копируем отдельно
         toObj[fieldName] = newFunc(fromObj[fieldName]);
      }
      else{ // Ссылки на примитивные объекты
         toObj[fieldName] = fromObj[fieldName];
      }
   }
}
/* ==================================================== */
/* ========= Определяем базовую класс-функцию ========= */
/* ==================================================== */
   // Исходная ("базовая") функция-объект
_global.twoExprFunc = function(x, funcThis, derPassed){
   // Выводим отладочную информацию о том, куда мы попали
   trace("function: " + arguments.callee.name);
   trace("arguments = " + arguments);
   trace("Original object: = " + funcThis.name);
      // Исходный объект подкласса. Если сюда не передали
      // информацию о производном классе-функции, для которого
      // функция вызвана, значит она вызвана сама по себе.
   if (funcThis == undefined) var funcThis = arguments.callee;
      // Вместо this, который в функциях работает в расчете
      // на обычные объекты, используем полученный в аргументах
      // указатель funcThis - указатель на реальный подкласс,
      // для которого внешний код произвел вызов функции.
   return funcThis.resFunc(funcThis.firstExpr(x), funcThis.secondExpr(x));
}
// Устанавливаем отладочную строчку с именем вручную, поскольку
// эту функцию мы не создавали с помощью newFunc
twoExprFunc.name = "twoExprFunc";
// Методы базового объекта-функции
twoExprFunc.setResFunction = function(resFunc){
   trace("********** settingResFunction: " + resFunc.name + " ***********");
// Выводим пустую строку, чтобы отделить от вывода других функций
   trace("");
   this.resFunc = resFunc;
}

// Строчку с именем ставим вручную
twoExprFunc.setResFunction.name = "setResFunction";
twoExprFunc.setExpressionsFuncs = function(firstExpr, secondExpr){
   trace("************* settingExpressions **************");
// Выводим пустую строку, чтобы отделить от вывода других функций
   trace("");
this.firstExpr = firstExpr;
   this.secondExpr = secondExpr;
}
// Строчку с именем ставим вручную
twoExprFunc.setExpressionsFuncs.name = "setExpressionsFuncs";
/* ==================================================== */
/* ========= Определяем производные класс-функции ===== */
/* ===================================================== */
plusFunc = newFunc(twoExprFunc, "plusFunc");
var resFuncPlus = function(a, b){
   return a + b;
}
// Строчку с именем ставим вручную
resFuncPlus.name = "resFuncPlus";
plusFunc.setResFunction(resFuncPlus);
divFunc = newFunc(twoExprFunc, "divideFunc");
var resFuncDiv = function(a, b){
   return a / b;
}
// Строчку с именем ставим вручную
resFuncDiv.name = "resFuncDiv";
divFunc.setResFunction(resFuncDiv);
particularFunc = newFunc(plusFunc, "particularFunc");
particularFunc.setExpressionsFuncs(Math.round, Math.sin);
// В результате particularFunc вычисляет sin(x) + round(x)
anotherFunc = newFunc(divFunc, "anotherFunc");
anotherFunc.setExpressionsFuncs(Math.abs, function(x)
   {return x*x*x});
// В результате anotherFunc вычисляет sign(x)/x^2, где sign(x)
// дает 1 для положительных чисел и -1 - для отрицательных.
// В случае x == 0 наша anotherFunc не определена (NaN).
/* ==================================================== */
/* =========== Проверка работоспособности ============= */
/* ==================================================== */
trace(
   ":::::::::::::::::::: particularFunc(Math.PI/2) = " +
   particularFunc(Math.PI/2) + " ::::::::::::::::::::"
); // Аргумент здесь чуть больше полутора, синус его равен 1,
   // так что в результате должно получиться 3.
trace("");
// Выводим пустую строку, чтобы отделить от вывода других функций
trace(
   ":::::::::::::::::::: anotherFunc(-2) = " +
   anotherFunc(-2) + " ::::::::::::::::::::"
); // Должно получиться -1/4.
       

Пример 7.1.

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

 
------------
function: setResFunction
arguments = [type Function]
newArgs = [type Function],[type Function],Derived class arg passed
------------
********** settingResFunction: resFuncPlus ***********
------------
function: setResFunction
arguments = [type Function]
newArgs = [type Function],[type Function],Derived class arg passed
------------
********** settingResFunction: resFuncDiv ***********
------------
function: setExpressionsFuncs
arguments = [type Function],[type Function]
newArgs = [type Function],[type Function],[type Function],Derived class arg passed
------------
------------
function: setExpressionsFuncs
arguments = [type Function],[type Function],[type Function],Derived class arg passed
newArgs = [type Function],[type Function],[type Function],Derived class arg passed
------------
************* settingExpressions **************
------------
function: setExpressionsFuncs
arguments = [type Function],[type Function]
newArgs = [type Function],[type Function],[type Function],Derived class arg passed
------------
------------
function: setExpressionsFuncs
arguments = [type Function],[type Function],[type Function],Derived class arg passed
newArgs = [type Function],[type Function],[type Function],Derived class arg passed
------------
************* settingExpressions **************
------------
function: particularFunc
arguments = 1.5707963267949
newArgs = 1.5707963267949,[type Function],Derived class arg passed
------------
------------
function: plusFunc
arguments = 1.5707963267949,[type Function],Derived class arg passed
newArgs = 1.5707963267949,[type Function],Derived class arg passed
------------
function: twoExprFunc
arguments = 1.5707963267949,[type Function],Derived class arg passed
Original object: = particularFunc
------------
function: resFuncPlus
arguments = 2,1
newArgs = 2,1,[type Function],Derived class arg passed
------------
:::::::::::::::::::: particularFunc(Math.PI/2) = 3 ::::::::::::::::::::
------------
function: anotherFunc
arguments = -2
newArgs = -2,[type Function],Derived class arg passed
------------
------------
function: divideFunc
arguments = -2,[type Function],Derived class arg passed
newArgs = -2,[type Function],Derived class arg passed
------------
function: twoExprFunc
arguments = -2,[type Function],Derived class arg passed
Original object: = anotherFunc
------------
function: resFuncDiv
arguments = 2,-8
newArgs = 2,-8,[type Function],Derived class arg passed
------------
:::::::::::::::::::: anotherFunc(-2) = -0.25 ::::::::::::::::::::
       

Пример 7.2.

Разбирать содержимое консоли начнем с конца. Видим, что нужный результат для anotherFunc получен, и что были последовательно совершены вызовы основной функции, начиная от объекта-функции anotherFunc, через divideFunc и заканчивая twoExprFunc. Та в свою очередь вызвала определенный в производном классе divideFunc метод resFunc (а именно, конкретную функцию resFuncDiv). Причем, поскольку этот метод вызывался на самом деле через самого последнего наследника, то есть через anotherFunc, то была вызвана копия, сделанная при помощи newFunc (отсюда и отладочная информация), а эта копия уже, в свою очередь, вызвала оригинальный метод.

 

Примерно то же самое произошло и при вызове particularFunc, который тоже дал правильное значение. Заметим, что вызовы setResFunction и setExpressionsFuncs также предваряются выводом большого количества отладочной информации, так как эти методы были изначально заведены еще в самом базовом классе-функции и затем копировались в наследники при помощи newFunc.

 

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

 

Можно ли здесь использовать альтернативное наследование?

То есть не копировать поля, а просто выстроить цепочку __proto__? Ответ на этот вопрос такой: можно, но осторожно. При использовании наследования с полным копированием мы вправе были не следить за тем, являются ли части нашего выражения обычными функциями или функциями-объектами. Если это функции-объекты, сами представляющие собой сложные выражения, то мы в какой-то момент можем одно из этих подвыражений изменить. Вот тут-то (если мы не использовали полное копирование) и выяснится, что на это подвыражение ссылается какой-нибудь еще объект. Впрочем, подобные проблемы можно решить, путем создания новых объектов (но только таких, которые действительно нужно создавать заново) в конструкторе. (Наш пример наследования функций не предоставляет возможности использовать конструктор, но добавить эту возможность несложно.) Итак, когда мы знаем, что проблем со множеством ссылок на изменяемые объекты у нас не будет, мы можем в функции newFunc вызов copyObj(f, somefunc); заменить на f.__proto__ = somefunc; после чего на выходе получим:

 
********** settingResFunction: resFuncPlus ***********

********** settingResFunction: resFuncDiv ***********

************* settingExpressions **************

************* settingExpressions **************
------------
function: particularFunc
arguments = 1.5707963267949
newArgs = 1.5707963267949,[type Function],Derived class arg passed
------------
------------
function: plusFunc
arguments = 1.5707963267949,[type Function],Derived class arg passed
newArgs = 1.5707963267949,[type Function],Derived class arg passed
------------
function: twoExprFunc
arguments = 1.5707963267949,[type Function],Derived class arg passed
Original object: = particularFunc
:::::::::::::::::::: particularFunc(Math.PI/2) = 3 ::::::::::::::::::::
------------
function: anotherFunc
arguments = -2
newArgs = -2,[type Function],Derived class arg passed
------------
------------
function: divideFunc
arguments = -2,[type Function],Derived class arg passed
newArgs = -2,[type Function],Derived class arg passed
------------
function: twoExprFunc
arguments = -2,[type Function],Derived class arg passed
Original object: = anotherFunc
:::::::::::::::::::: anotherFunc(-2) = -0.25 ::::::::::::::::::::
       

То есть все сработало правильно; видно также, что методы объектов-функций (такие, как setResFunction и setExpressionsFuncs) не копировались при помощи newFunc и в результате не выводят про своем вызове отладочную информацию.

 

Полностью ли мы реализовали наследование функций?

Конечно, осталось еще довольно много работы. Нужно организовать хранение конструктора и его вызов при вызове newFunc (в нашем примере мы не пользовались конструкторами). Хорошо бы переписать наш пример с тем, чтобы функции-объекты (которые подвержены изменениям) создавать в конструкторе при помощи newFunc, тогда можно будет и разветвленные выражения с изменяемыми частями создавать при помощи rebel-наследования. Неплохо бы также реализовать механизм, аналогичный использованию ключевого слова super. Однако все подобные вещи мы уже проделывали в предыдущих параграфах этой лекции. Так что наведение глянца предоставляется читателю в качестве упражнения. А мы переходим к дальнейшим "фокусам".

 

Головоломки для продвинутых

И этот параграф при первом чтении тоже вполне можно пропустить. Нельзя сказать, что он полностью оправдывает свой заголовок. Во-первых, мы долго размышляли над уместностью в названии буквы "р". Во-вторых, содержимое сего параграфа никак не сложнее, скажем, предыдущего; однако, в отличие от него, не несет в себе уж совсем никакой практической пользы. Все, что вы можете от него получить, - это острые программистские ощущения и (ложную) уверенность, что уж теперь-то вы понимаете все детали работы Флэш МХ. Но уж острые ощущения будут что надо, поверьте!

 

Содержимое контекста вызова

Мы много раз использовали словосочетание "контекст вызова функции" (по-английски его также называют activation object, то есть "объект активации"). Мы даже убедились, что это действительно единый объект, доступный во вложенных функциях через this. Давайте же исследуем его - как исследовали мы другие системные объекты! А заодно - и все остальное, что попадется по дороге. Для этого мы применим вот такой код:

 
_global.dumpObj = function(obj){
      // Снимаем "защиту" со скрытых полей
   ASSetPropFlags(obj,null,0,1);
   for(name in obj){
      trace(name + ": " + obj[name]);
   }
}
o1 = {k:10, l:20};
a1 = [5, 6, 7];
trace("******* a1 ********");
dumpObj(a1);
trace("*******************");
trace("");
var f = function(){
   trace("---- arguments -----");
   dumpObj(arguments);
   trace("--------------");
   var x = 5;
   var y = "Текстовая локальная переменная";
   var o2 = {k:15, l:25};
   var getActivation = function(){
      return this;
   }
   return getActivation();
}
trace("Снаружи this = " + this);
trace("");
trace("==== Контекст вызова f ====");
dumpObj(f(1000, [1, 2, 3]));
trace("===========================");
trace("");
trace(":::::::: _level0 ::::::::");
dumpObj(this);
trace("::::::::::::::::::::::::::");
       

Посмотрим, как этот код справляется с нашей основной задачей: разобраться, что скрывается внутри контекста вызова функции. Контекст вызова f доступен при вызове getActivation через this. (Здесь важно, что ссылку getActivation мы пометили как var).

 

Обратите внимание, что на функции, сгенерированные внутри кадра какого-то клипа (в данном случае, _level0), не распространяется это правило. То есть в них this также указывает на _level0, а не на какой-то "контекст вызова кадра". Чтобы это подчеркнуть, мы перед f нарочно поставили var.

 

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

 
******* a1 ********
2: 7
1: 6
0: 5
__proto__:
constructor: [type Function]
length: 3
*******************
Снаружи this = _level0
==== Контекст вызова f ====
---- arguments -----
1: 1,2,3
0: 1000
caller: null
callee: [type Function]
__proto__:
constructor: [type Function]
length: 2
--------------
getActivation: [type Function]
o2: [object Object]
y: Текстовая локальная переменная
x: 5
arguments: 1000,1,2,3
this: _level0
===========================
:::::::: _level0 ::::::::
f: [type Function]
name: name
__proto__: [object Object]
constructor: [type Function]
a1: 5,6,7
o1: [object Object]
$appPath: file:///K|/Program%20Files/Macromedia/Flash%20MX/
$version: WIN 6,0,21,0
::::::::::::::::::::::::::
       

Мы видим, что в контексте вызова функции хранятся все локальные переменные, а также ссылки на arguments и this. Также видно, что объект arguments отличается от обычного массива (для примера и проверки работы функции dumpObj выведены "внутренности" массива a1) только полями caller и callee. Интересен также тот факт, что у объекта "контекст вызова" отсутствуют поля constructor и __proto__. Это наводит на мысль о некоторых трюках, которые мы испробуем в следующем подпараграфе. Напоследок давайте посмотрим, какие из увиденных нами полей являются открытыми, а какие мы видим только благодаря применению недокументированной функции ASSetPropFlags. Закомментируем ее вызов в функции dumpObj, и тогда на выходе получим следующее:

 
******* a1 ********
2: 7
1: 6
0: 5
*******************
Снаружи this = _level0
==== Контекст вызова f ====
---- arguments -----
1: 1,2,3
0: 1000
--------------
getActivation: [type Function]
o2: [object Object]
y: Текстовая локальная переменная
x: 5
===========================
:::::::: _level0 ::::::::
f: [type Function]
name: name
a1: 5,6,7
o1: [object Object]
$version: WIN 6,0,21,0
::::::::::::::::::::::::::
       

Примечательно, что в объекте arguments мы не видим теперь ничего, кроме самих аргументов, а в контексте вызова видим только локальные переменные (но не видим arguments и this).

 

Эмуляция with в функциях

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

 
_global.dumpObj = function(obj){
      // Снимаем "защиту" со скрытых полей
   ASSetPropFlags(obj,null,0,1);
   for(name in obj){
      trace(name + ": " + obj[name]);
   }
}
o1 = {k:10, l:20};
var f = function(){
   var x = 5;
   var y = "Текстовая локальная переменная";
   var o2 = {k:15, l:25};
  
   var getActivation = function(){
      return this;
   }
  
   var act = getActivation();
   act.__proto__ = o1; // Или var.__proto__ = o1;
  
   trace("===================");
   trace("k = " + k);
   trace("l = " + l);
   trace("===================");
     
   k = 145;
   var l = 1111;
     
   trace("===================");
   trace("k = " + k);
   trace("l = " + l);
   trace("===================");
   return getActivation();
}

trace("==== Контекст вызова f ====");
dumpObj(f(1000, [1, 2, 3]));
trace("===========================");
trace("");
trace("******* o1 ********");
dumpObj(o1);
trace("*******************");
trace("");
trace(":::::::: _level0 ::::::::");
dumpObj(this);
trace("::::::::::::::::::::::::::");

На выходе получаем:

==== Контекст вызова f ====
===================
k = 10
l = 20
===================
===================
k = 10
l = 1111
===================
k: 10
l: 1111
__proto__: [object Object]
act: [object Object]
getActivation: [type Function]
o2: [object Object]
y: Текстовая локальная переменная
x: 5
arguments: 1000,1,2,3
this: _level0
===========================
******* o1 ********
k: 10
l: 20
__proto__: [object Object]
constructor: [type Function]
*******************
:::::::: _level0 ::::::::
name: name
__proto__: [object Object]
constructor: [type Function]
k: 145
f: [type Function]
o1: [object Object]
$appPath: file:///K|/Program%20Files/Macromedia/Flash%20MX/
$version: WIN 6,0,21,0
::::::::::::::::::::::::::
       

Мы видим удивительные (хотя и ожидавшиеся нами) вещи! Действительно, переменные k и l воспринимаются внутри функции (после того, как контексту указали __proto__) как локальные! Но более того, они еще и являются чем-то вроде константных локальных переменных! Наша попытка изменить переменную k привела к тому, что была заведена соответствующая переменная в _level0. (Поскольку содержимое объекта, на который указывает __proto__, не меняется, то должна быть заведена новая переменная; но поскольку ключевое слово var не было применено, то переменная заводится в объекте, из которого была вызвана функция f.) Поскольку приоритет в функции имеет локальная переменная, то значение k как бы и не изменилось. В случае же переменной l, когда мы использовали var, значение l в функции, конечно, было заменено. Но в объекте o1 оно, разумеется, осталось п режним (так же как и значение k). Конечно, польза такого необычного применения __proto__ далеко не очевидна. Однако сама возможность подобных фокусов вызывает теплые ностальгические чувства и воспоминания о Бэйсике и ассемблере конца 80-х годов прошлого века. Не будем вдаваться в подробности. У каждого своя ностальгия.

 

Приватные поля и наследование

Перегенерация функций для доступа к частным полям

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

 

Включение в цепочку __proto__ контекста вызова конструктора

Перегенерация функций - это интересная идея. Но как быть, если вы хотите сделать приватными всего одно-два особенно важных поля, и обращаются к этим полям опять-таки один-два метода? Перегенерация всех методов класса будет в таком случае явно избыточной. С другой стороны, как обеспечить этим методам (в процессе перегенерации) доступ к остальному содержимому класса? Ведь вызывать мы будем эти методы при помощи apply, передавая им контекст вызова конструктора. Но на этот раз мы не собираемся в контекст конструктора копировать все нужные нам ссылки. Выход из этого положения как раз может подсказать найденный нами недавно фокус с заданием ссылки __proto__ для контекста вызова! Установим __proto__ контекста вызова ссылающимся на наш объект. Тогда все значения переменных из объекта будут доступны для рассматриваемой функции. Правда - и здесь нужно быть аккуратным - эта функция не сможет их изменить (точнее, измененные значения останутся в контексте вызова конструкт ора, а не в самом объекте). Так что если менять значение публичного поля все-таки надо, следует или воспользоваться функцией-setter'ом этого поля (если нужно, то создать его), или же сделать это поле приватным и перегенерировать еще и те функции, которые к нему обращаются. Первый способ, конечно, предпочтительнее.

 

Сейчас мы приведем пример использования предлагаемого метода. Воспользуемся уже знакомым нам замечательным лифтом. Предположим, мы добавили возможность внешнему контроллеру управлять этим лифтом. Контроллер сможет открывать и закрывать двери, а также останавливать лифт, если надо. Условимся, что изменение соответствующей переменной будет означать изменение состояния лифта. То есть, если мы установили переменную doorsAreOpen в true, то это означает, что двери открылись. Посмотрите на новый вариант кода лифта:

 
// ================ Базовый класс лифта - модифицирован =================
   // Базовый конструктор - добавлено поле moving
_global.lift = function (minFloor, maxFloor){
   // Проверяем, не перепутал ли пользователь максимальный и
   // минимальный этажи
   if (minFloor < maxFloor){
      this.minFloor = Math.round(minFloor);
      this.maxFloor = Math.round(maxFloor);
   }
   else{  // Если перепутал, корректируем
      this.minFloor = Math.round(maxFloor);
      this.maxFloor = Math.round(minFloor);
   }
      // Устанавливаем лифт на 1 этаж
       // (но если лифт туда не должен ездить, корректируем
       // начальный этаж в нужную сторону).
   this.currentFloor = Math.min(Math.max(1, this.minFloor),
      this.maxFloor);
   this.doorsAreOpen = true;
      // Новое поле
   this.moving = false;
}
   // Функция goto - без изменений
_global.lift.prototype.goto = function(where){
   trace("-----------");
      // Oкругляем и ограничиваем этаж назначения
   this.floorToGo = Math.min(Math.max(Math.round(where), this.minFloor), this.maxFloor);
   if (this.floorToGo != this.currentFloor) this.go(); 
// Поехали!
   else trace("Ничего не делаем." );
   trace("-----------");
}
   // Уделяем повышенное внимание безопасности
_global.lift.prototype.setDoorsOpen = function(open){
      // Если мы едем, двери трогать нельзя
   if (this.moving) return;
   if (this.doorsAreOpen != open){
      trace("Осторожно!");
      trace(open ? "Открываем двери." : "Закрываем двери.");
   }
   this.doorsAreOpen = open;
}
   // Добавлена регистрация состояний движения/неподвижности,
   // а также добавлено взаимодействие с внешним контроллером
_global.lift.prototype.go = function(){
      // Осторожно, двери закрываются
   this.setDoorsOpen(false);
   this.moving = true;
  
      // Поехали!
   var distance = this.floorToGo - this.currentFloor;
   var signOfDistance = (distance >= 0 ? 1 : -1);
   for(  // Это работает независимо от того, едем вверх или вниз
      var i = this.currentFloor;
      signOfDistance*i <= signOfDistance*this.floorToGo;
      i += signOfDistance
   ){
         // Едем и выводим об этом сообщение
      trace("Этаж " + i);
      this.currentFloor = i;
         // Проехали один этаж, сообщаем об этом контроллеру
      externalController.work(this);
         // А вдруг контроллер остановил лифт?
      if (!this.moving) break;
   }
      // Приехали
   this.moving = false;
   this.setDoorsOpen(true);
}
       

Пример .

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

 
//================ Контроллер лифта ===========================

externalController = {};
externalController.work = function(liftToControl){
   this.liftToControl = liftToControl;
   if (liftToControl.currentFloor == 3) this.doEvilThing();
}
externalController.openLiftDoors1 = function(){
   this.liftToControl.doorsAreOpen = true;
}
externalController.openLiftDoors2 = function(){
   this.liftToControl.setDoorsOpen(true);
}
externalController.openLiftDoors3 = function(){
   this.liftToControl.moving = false;
   this.liftToControl.setDoorsOpen(true);
}
externalController.doEvilThing = externalController.openLiftDoors1;
       

Давайте теперь напишем класс safeLift, который будет наследником класса lift, избавленным от этих проблем.

 
//================== Безопасный лифт =================================

_global.safeLift = function(minFloor, maxFloor){
   super(minFloor, maxFloor);

   var doorsAreOpen = true;
   var getActivationObject = function(){
      return this;
   }
   var activationObject = getActivationObject();
   activationObject.__proto__ = this;
   // Или var __proto__ = this;
   var setDoorsOpen = function(){
      return this.__proto__.setDoorsOpen.apply(activationObject,
         arguments);
   }
      // Переопределяем старую функцию
   this.setDoorsOpen = setDoorsOpen;
}
_global.safeLift.prototype = new _global.lift(0, 1);
       

Тестировочный код будет у нас выглядеть следующим образом:

 
//=========================== Тестовая часть =============================

l1 = new lift(1, 5);
l2 = new safeLift(1, 5);
 
  // Тестируем
trace("l1:");
l1.goto(10);
trace("l2:")
l2.goto(10);

externalController.doEvilThing = externalController.openLiftDoors2;

trace("l1:");
l1.goto(0);
trace("l2:")
l2.goto(0);

externalController.doEvilThing = externalController.openLiftDoors3;

trace("l1:");
l1.goto(10);
trace("l2:")
l2.goto(10);
       

Поместив в кадр _level0 все вышеприведенные кусочки кода и запустив эту программку, мы получим:

 
l1:
-----------
Осторожно!
Закрываем двери.
Этаж 1
Этаж 2
Этаж 3
Этаж 4
Этаж 5
-----------
l2:
-----------
Осторожно!
Закрываем двери.
Этаж 1
Этаж 2
Этаж 3
Этаж 4
Этаж 5
Осторожно!
Открываем двери.
-----------
l1:
-----------
Осторожно!
Закрываем двери.
Этаж 5
Этаж 4
Этаж 3
Этаж 2
Этаж 1
Осторожно!
Открываем двери.
-----------
l2:
-----------
Осторожно!
Закрываем двери.
Этаж 5
Этаж 4
Этаж 3
Этаж 2
Этаж 1
Осторожно!
Открываем двери.
-----------
l1:
-----------
Осторожно!
Закрываем двери.
Этаж 1
Этаж 2
Этаж 3
Осторожно!
Открываем двери.
-----------
l2:
-----------
Осторожно!
Закрываем двери.
Этаж 1
Этаж 2
Этаж 3
Осторожно!
Открываем двери.
-----------
       

Посмотрим, что у нас произошло. При первоначальном движении вверх у лифта номер 1 возникли проблемы. Контроллер открыл двери на 3-м этаже, при этом лифт не выдал никакого предупреждения и не остановился (о том, что двери открылись, можно судить по отсутствию сообщений об открывании дверей, когда лифт приехал на 5-й этаж). При движении вниз оба лифта отвергли попытку контроллера открыть двери с помощью setDoorsOpen, поскольку переменная moving была установлена в true. Наконец, при обратном движении наверх оба лифта остановились на 3-м этаже, поскольку контроллер установил moving в false. Итак, safeLift оказался полностью защищенным от некорректных действий контроллера.

 

Отметим, что строчка activationObject.__proto__ = this; в конструкторе безопасного лифта является обязательной. Попробуем закомментировать ее, и при движении лифта вниз (когда контроллер открывает двери при помощи setDoorsOpen) мы получим:

 
l2:
-----------
Осторожно!
Закрываем двери.
Этаж 5
Этаж 4
Этаж 3
Осторожно!
Открываем двери.
Этаж 2
Этаж 1
-----------
       

То есть в этом случае функция setDoorsOpen проигнорировала переменную moving, что неудивительно - она просто не смогла ее найти в контексте вызова конструктора, который мы передали ей в качестве this. И только запись activationObject.__proto__ = this; спасает положение.

 

Заметим, что вместо activationObject.__proto__ = this, мы могли также написать var __proto__ = this. Действительно, var как раз позволяет добавлять нам ссылки в контекст вызова, так что и ссылку __proto__ можно добавить эдаким образом. Хотя рассудок протестует против идеи составлять выражение из трех ключевых слов разного вида и оператора присваивания. Но, тем не менее, такая запись тоже работает.

 

Является ли приведенный здесь код образцом хорошего стиля? Вряд ли. Но в особых случаях (когда при наследовании вам очень хочется сделать некоторые поля приватными) можно применять подобные приемы. Главное, чтобы вы точно знали, что делаете и зачем. А если вы часто испытываете нужду в более продвинутых объектных средствах разработки - переходите на Flash MX 2004.

 
© INTUIT.ru, 2003-2008. Все права защищены.



 

13 центов(0,13$) за клик, выплаты через WebMoney каждый вторник +10% с рефералов

Мы выкупаем 100% трафа! $12 за 1000 хостов (РФ), и до $4 за 1000 хостов (зарубежный траф) + 10% с дохода Ваших рефералов!
 Выплаты через
WebMoney

~80-100$ за1000 хостов 2.5$ за 1 смс.
реф. процент - 10 %Выплаты происходят раз в неделю, в четверг на
WebMoney
 
 

 

____________________________

Посмотреть порно видео в онлайне »

_______________________________

 

   
   
Сайт управляется системой uCoz