учебники, программирование, основы, введение в,

 

Классы

Классы в ActionScript (в отличие от ActionScript 2) реализованы довольно нестандартным способом. Причина этого в том, что объектная модель первых трех версий ECMAScript ведет свою родословную не от С++, а от языка Self, в котором она, в свою очередь, является развитием конструкций из некоторых вариантов Smalltalk. Это так называемая прототипная модель, которая, как мы увидим дальше, хорошо подходит для интерпретируемых языков и использует те их преимущества, о которых мы уже вкратце говорили. Хорошая расширяемость такой объектной модели позволит нам эмулировать ряд аспектов множественного наследования, которое изначально в ActionScript не было предусмотрено.
Понятие прототипа
Чтобы наиболее наглядно проиллюстрировать, что такое прототип, мы попробуем эмулировать использование прототипа при помощи уже известных нам функций Флэш МХ. Как мы увидим далее, эмулировать можно не все, но достаточно много.
Прототип - обычный объект
Итак, начнем мы с того, что создадим пока не класс, а самый обыкновенный объект, выполняющий какую-нибудь полезную деятельность. Или делающий вид, что он ее выполняет. Вот для примера объект, который думает, будто он лифт в девятиэтажном здании.
lift = {};
lift.currentFloor = 1;      // Текущий этаж
lift.floorToGo = 1;       // Этаж назначения
lift.minFloor = 1;        // Минимальный этаж
lift.maxFloor = 9;        // Максимальный этаж
lift.doorsAreOpen = true;  // Двери открыты
lift.goto = function(where){
trace("-----------");
// Округляем и ограничиваем этаж назначения
this.floorToGo = Math.min(Math.max(Math.round(where),
this.minFloor), this.maxFloor);
if (this.floorToGo != this.currentFloor) this.go();//Поехали!
else trace("Ничего не делаем." );
trace("-----------");
}
lift.setDoorsOpen = function(open){
if (this.doorsAreOpen != open){
trace(open ? "Открываем двери." : "Закрываем двери.")
}
this.doorsAreOpen = open;
}
lift.go = function(){
this.setDoorsOpen(false);
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;
}
this.setDoorsOpen(true);
}
// Тестируем
lift.goto(5);
lift.goto(3);
lift.goto(10);
lift.goto(9);

На выходе получаем:
-----------
Закрываем двери.
Этаж 1
Этаж 2
Этаж 3
Этаж 4
Этаж 5
Открываем двери.
-----------
-----------
Закрываем двери.
Этаж 5
Этаж 4
Этаж 3
Открываем двери.
-----------
-----------
Закрываем двери.
Этаж 3
Этаж 4
Этаж 5
Этаж 6
Этаж 7
Этаж 8
Этаж 9
Открываем двери.
-----------
-----------
Ничего не делаем.
-----------

Лифт работает! Теперь посмотрим, сможем ли мы построить класс на основе этого объекта.
Упрощенное представление - копируем прототип
Наличие класса означает, что мы можем создавать множество независимых друг от друга объектов этого класса. Очевидно, что наивная запись lift2 = lift к добру не приведет - мы получим лишь еще одну ссылку на тот же самый объект. (Можно сказать, что эта запись делает два разных входа в здание с лифтом; однако, войдя через другой вход в дом мы все равно обнаружим лифт на том же самом этаже. Мы же хотим иметь два разных лифта в разных домах.) В самом деле, написав вместо старых операторов тестирования следующие строчки:
lift2 = lift;
// Проверяем
trace("lift:");
lift.goto(3);
trace("lift2:");
lift2.goto(5);

получим:
lift:
-----------
Закрываем двери.
Этаж 1
Этаж 2
Этаж 3
Открываем двери.
-----------
lift2:
-----------
Закрываем двери.
Этаж 3
Этаж 4
Этаж 5
Открываем двери.
-----------

Действительно, мы имеем дело только с одним лифтом - на пятый этаж lift2 поехал не с первого этажа (как было бы, если бы мы в самом деле создали новый лифт) а с третьего.
Таким образом, нам нужно как-то скопировать объект. Вот первый (несовершенный) вариант:
// Функция для копирования
_global.newObject = function(objectPrototype){
var tempObject = {};  // Создаем новый объект
for(fieldName in objectPrototype)  // Копируем (?) поля
tempObject[fieldName] = objectPrototype[fieldName];
return tempObject;
}
// Копируем лифт
lift2 = newObject(lift);
// Проверяем
trace("lift:");
lift.goto(3);
trace("lift2:");
lift2.goto(5);

Добавив этот код к коду из предыдущего подпараграфа (и убрав старые "пробные запуски" лифта), получаем на выходе:
lift:
-----------
Закрываем двери.
Этаж 1
Этаж 2
Этаж 3
Открываем двери.
-----------
lift2:
-----------
Закрываем двери.
Этаж 1
Этаж 2
Этаж 3
Этаж 4
Этаж 5
Открываем двери.
-----------

Действительно, лифты ездят независимо друг от друга.


Заменяем и дополняем поля и методы
Теперь, когда мы создали новый объект "класса" lift, мы сможем несколько изменить его. Например, заставить ездить на 16 этаж и в подвал. А также издавать звуковой сигнал при закрывании дверей. Вот та часть кода, которая эта делает:
// Копируем лифт
lift2 = newObject(lift);
// Meняем параметры
lift2.minFloor = 0;
lift2.maxFloor = 16;
// "Переопределение виртуальной функции"
lift2.setDoorsOpenImpl = lift2.setDoorsOpen;
lift2.setDoorsOpen = function(open){
if (!open && this.doorsAreOpen) trace("Динь!");
this.setDoorsOpenImpl(open);
}
// Проверяем
trace("lift:");
lift.goto(0); // Пробуем съездить в подвал на старом лифте
trace("lift2:");
lift2.goto(0); // И на новом

На выходе получаем:
lift:
-----------
Ничего не делаем.
-----------
lift2:
-----------
Динь!
Закрываем двери.
Этаж 1
Этаж 0
Открываем двери.
-----------

Полное впечатление, что lift2 наследован от lift и в нем переопределена функция управления дверьми. С этой точки зрения каждый объект во Флэше - это класс (поскольку этот объект можно скопировать и наследовать от него другие объекты).

Создание настоящего класса

Теперь, когда мы научились делать "почти классы", перейдем к созданию классов настоящих. Для этого сначала выясним, чем хороши и чем плохи "почти классы".

Анализ метода определения класса созданием объекта-прототипа

Что уже есть в созданной нами эмуляции

Итак, мы можем:

  1. создавать однотипные объекты
  2. менять их свойства и методы
Чего не хватает

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

Конструктор
Конструктор как обычная функция
Давайте обеспечим наш класс lift конструктором. Правило, согласно которому имя конструктора должно совпадать с именем класса, выполняется и во Флэше. В результате нам придется как-то избегать конфликта имен между объектом-прототипом и конструктором. Но начнем мы все же с конструктора, а проблемы с конфликтом имен, как мы увидим далее, решатся сами собой. Итак, при написании конструктора нам нужно знать, что это обычная функция, принадлежащая тому же объекту, которому должен принадлежать наш класс. Но при этом ключевое слово this будет обращением к объекту нашего класса, а не к объекту-хозяину. Это обеспечивается специальным механизмом вызова конструктора (в процессе выполнения оператора new, подробнее о котором далее). Приведенных только что сведений достаточно, чтобы написать следующий код:
_global.lift = function (minFloor, maxFloor){
this.minFloor = minFloor;
this.maxFloor = maxFloor;
}

Как мы уже говорили, конструктор будет вызван при создании объекта нашего класса с помощью оператора new. Но откуда оператор new возьмет прототип?
Конструктор как хранилище прототипа
Еще раз посмотрим, какая задача стоит перед нами. У нас имеется имя класса, оно же имя функции-конструктора. Так что оператор new, зная это имя, легко отыщет конструктор. Нет ли какого-то способа прицепить к конструктору прототип, чтобы не искать прототип отдельно?
Конечно же, есть! Объект функции-конструктора может содержать дополнительные поля, в одно из которых вполне можно положить ссылку на прототип. Именно так все и делается во Флэш МХ. У функции можно завести поле prototype и положить туда ссылку на объект-прототип. В тех случаях, когда функция используется в качестве конструктора (при вызове через оператор new), оператор new в качестве прототипа использует именно тот объект, ссылка на который лежит в поле prototype функции-конструктора. Так что к уже написанному коду, определяющему объект lift, нам надо добавить вот что:
// Конструктор
_global.lift = function (minFloor, maxFloor){
this.minFloor = minFloor;
this.maxFloor = maxFloor;
}
// Ссылка на прототип
_global.lift.prototype = lift;
// Создаем 2 объекта
l1 = new _global.lift(2, 5);
l2 = new _global.lift(0, 3);

// Тестируем
trace("l1:");
l1.goto(4);
trace("l2:")
l2.goto(4);

После запуска этого кода получим в консоли:
l1:
-----------
Закрываем двери.
Этаж 1
Этаж 2
Этаж 3
Этаж 4
Открываем двери.
-----------
l2:
-----------
Закрываем двери.
Этаж 1
Этаж 2
Этаж 3
Открываем двери.
-----------

Можно сделать следующие комментарии по поводу нашей работы. Во-первых, в конструкторе имеется ошибка: он не устанавливает начальный этаж, в результате лифт номер один поехал на четвертый с первого этажа, хотя минимальным является второй. Во-вторых, нам пришлось использовать явное указание на то, что конструктор берется из объекта _global. Иначе нам мешал бы уже имеющийся в _root (в котором был размещен наш код) объект по имени lift. Логично было бы не создавать конфликта имен, а сразу помещать все свойства и методы в prototype, тем более, что пустой объект, в который они будут добавлены, создается автоматически и надпись, аналогичная lift = {}; необязательна.

Методы и свойства в прототипе
В этом подпараграфе мы переработаем наш код с тем, чтобы все нужное для нашего класса было сразу помещено в прототип.
Добавляем методы в прототип
Итак, открываем новый Флэш-файл и пишем там вот что:
// Конструктор
_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;
}
// Функции - те же, что и раньше, но размещаем их в прототипе
_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.doorsAreOpen != open){
trace(open ? "Открываем двери." : "Закрываем двери.") 
}
this.doorsAreOpen = open;
}
_global.lift.prototype.go = function(){
this.setDoorsOpen(false);
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;
}
this.setDoorsOpen(true);
}
// Создаем 2 объекта
l1 = new lift(2, 5);
l2 = new lift(0, 3);
// Тестируем
trace("l1:");
l1.goto(4);
trace("l2:")
l2.goto(4);

На выходе получаем:
l1:
-----------
Закрываем двери.
Этаж 2
Этаж 3
Этаж 4
Открываем двери.
-----------
l2:
-----------
Закрываем двери.
Этаж 1
Этаж 2
Этаж 3
Открываем двери.
-----------

Теперь, как видно, мы можем писать не new _global.lift, а просто new lift, поскольку объект по имени lift мы не создавали, а сразу разместили все нужные методы в поле prototype функции-конструктора. Также видно, что переработка конструктора дала свои плоды: лифт номер 1 поехал на четвертый этаж со второго, а не с первого, поскольку ниже второго этажа он спускаться не может.
Нужно ли добавлять поля в прототип
Мы можем добавить в прототип не только методы, но и поля. Те значения, которые мы установим для этих полей в прототипе, будут автоматически скопированы во все экземпляры классов. (Точнее, так будет казаться на первый взгляд, до тех пор, пока мы не разберемся в механизме copy-on-write.) Таким способом имеет смысл устанавливать значения полей по умолчанию или же значения констант. Если же значение поля в любом случае устанавливается в конструкторе, то можно обойтись и без помещения поля в прототип. В случае нашего лифта мы могли бы поместить в прототип поле doorsAreOpen и присвоить ему там значение true. Это соответствовало бы тому факту, что лифты нашей конструкции после монтажа стоят с открытыми дверьми.

Вездесущий this
Фактически, наиболее важные вещи для создания собственных классов мы уже узнали. Теперь самое время еще раз упомянуть об основном источнике ошибок при работе с классами во Флэш-программе. Программист, пишущий на С++ или Java, привык, что внутри метода класса поля этого класса доступны просто по имени. Во Флэше это не так, перед именем поля нужно обязательно ставить this! Иначе Флэш решит, что вы обращаетесь к переменной того объекта, внутри которого написан код определения рассматриваемого метода (в простейшем случае этот объект - _root). Поэтому, если вы написали очевидный код и он не работает, проверьте, чтобы все слова this были на месте. Обычно при такой проверке обнаруживается, что вы забыли поставить вовсе не один, а штук пять this'ов.
Что делает оператор new - попытка эмуляции
Читая предыдущий параграф вы могли заметить, что никакой особенной поддержки языка для реализации всего, что там написано, не требуется. То есть, оператор new, конечно, делает полезные вещи, но с тем же успехом эти вещи можно сделать и вручную. Итак, попробуем эмулировать все, что мы знаем об операторе new, хотя наши знания пока и неполны (а кое в чем могут оказаться ошибочными - ведь мы неявно предполагаем, что оператор new полностью копирует объекты, а это не так).

Оператор new и копирование объектов

Вариант с нерекурсивным копированием

Для того, чтобы обеспечить функциональность, похожую на функциональность настоящего оператора new, нам понадобится вспомогательная функция копирования объектов; сама же функция, эмулирующая оператор new, должна помимо копирования прототипа еще и вызывать конструктор. Так что возьмем код из предыдущего параграфа, но после определения класса lift напишем следующее:

_global.newFunc = function (constr, args){
  var tmp = {}; // Или var tmp = new Object();
  
          // Копируем прототип:
  copyOneLevelObj(tmp, constr.prototype);
   // Выделяем подмассив с аргументами, которые надо передать в 
   // конструктор (все, кроме нулевого -  это сам конструктор) 
   // и вызываем его:
  constr.apply(tmp, arguments.slice(1));
  return tmp;
}
_global.copyOneLevelObj = function (toObj, fromObj){
   for (var fieldName in fromObj){ 
      toObj[fieldName] = fromObj[fieldName];  
   }
}
  // Создаем 2 объекта 
l1 = newFunc(lift, 2, 5);
l2 = newFunc(lift, 0, 3);
  // Тестируем  
trace("l1:");
l1.goto(4);
trace("l2:")
l2.goto(4);
        

Тестировочные операторы оставляем такими же, как раньше, и убеждаемся, что на выходе получается то же самое:

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

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

Недостатки нерекурсивного варианта. Как скопировать объект

То, что с нашей функцией копирования не все в порядке, обнаружится сразу, как только мы станем копировать объекты с полями, являющимися ссылками на другие объекты. И попытаемся эти самые объекты, на которые указывают поля-ссылки, модифицировать. Мы тут же увидим, что ссылки в копиях указывают на один и тот же объект - а ведь мы хотели бы модифицировать разные. Таким образом, увидев в объекте, предназначенном для копирования, поле, ссылающееся на другой (не примитивный) объект, мы должны этот другой объект также скопировать. То есть наша функция копирования должны быть рекурсивной.
Надо заметить, что, совершенствуя функцию newFunc в этом направлении, мы удаляемся от оригинала. Здесь уже пора приоткрыть краешек тайны и сказать о том, что настоящий оператор new вовсе не копирует объекты. Точнее, он делает возможным использование в дальнейшем так называемого механизма copy-on-write, то есть "копирование при записи". Что же касается размножения "вложенных" объектов, то эта задача возлагается на конструктор - и программист сам проследит за созданием новых объектов. Тем не менее, раз уж мы взялись за задачу размножения объектов, давайте доведем ее до конца. Хотя метод создания дочерних объектов в конструкторе наиболее универсален, он все же требует создания соответствующего кода в каждом конкретном конструкторе. В некоторых случаях может оказаться удобным использовать готовую функцию копирования.

Рекурсивный вариант функции копирования

Вот код рекурсивной функции copyObj. Мы проверим его работу в отдельном файле.

_global.copyObj = function(toObj, fromObj){
   for (var fieldName in fromObj){
      var type = typeof(fromObj[fieldName]);
         // Ссылки на объекты требуют копирования этих объектов                               
      if (type == "object"){ 
         // Параметризованные функции и клипы игнорируем
         toObj[fieldName] = {};
         copyObj(toObj[fieldName], fromObj[fieldName]);      
      }else{
         toObj[fieldName] = fromObj[fieldName];      
      }
   }
}
obj1 = {dataObj: {dataStr: "Some data"}, x: 10};
obj2 = {y: 5};
copyObj(obj2, obj1);
obj1.dataObj.dataStr = "Another data";
        

Этот пример ничего не выводит в консоль, но, нажав Ctrl+Alt+V (List Variables), мы получим:

Global Variables:
 Variable _global.copyObj = [function 'copyObj']
Level #0:
Variable _level0.$version = "WIN 6,0,21,0"
Variable _level0.obj1 = [object #2, class 'Object'] {
  x:10,
  dataObj:[object #3, class 'Object'] {
   dataStr:"Another data"
  }
 }
Variable _level0.obj2 = [object #4, class 'Object'] {
  y:5,
  dataObj:[object #5, class 'Object'] {
   dataStr:"Some data"
  },
  x:10
 }
        

Все поля объекта obj1 скопировались в obj2, включая поля вложенного подобъекта; более того, после изменения значения поля подобъекта, на который есть ссылка в obj1, значение поля аналогичного подобъекта в obj2 не изменилось. Отсюда мы можем сделать вывод, что рекурсивная функция копирования действительно работает. Так что теперь в нашей функции newFunc можно заменить вызов copyOneLevelObj на вызов copyObj (приводить результаты проверки нового варианта newFunc в примере с лифтами мы не будем, поскольку лифты не содержат пока ссылок на другие объекты и результат работы примера остается тем же самым).

Обсуждение результатов

Давайте посмотрим, что еще не сделано в нашей функции копирования. Во-первых, копируются не сами функции, а ссылки на них. Можно ли скопировать объект-функцию? Да, хотя и с некоторыми ограничениями. Ведь существует механизм генерации функций внутри других функций. Мы можем создать новую функцию и заставить ее вызывать старую. Как правило, делать такое незачем, если только наша функция-объект не имеет в себе поля, которые мы собираемся изменять. Пример оправданного копирования подобного рода мы можем найти в конце лекции о наследовании.
Во-вторых, не сделано копирование массивов. Копирования ссылки на массив недостаточно, если массив мы собираемся изменять. Хорошо бы определять заранее, что мы имеем дело с массивом, и копировать его при помощи метода slice (заставляя этот метод выдать фрагмент массива размером с целый массив). Копирование массивов также реализовано в примере, рассмотренном в лекции о наследовании (параграф "Наследование от необычных типов", подпараграф "Наследование от функции").
Копировать массив можно и другим способом. Скоро мы узнаем, как снимать "защиту" с системных полей и методов во встроенных объектах. После снятия такой защиты вполне можно будет скопировать массив, как любой другой объект. Правда, подобные действия надо выполнять с осторожностью, кроме того, они требуют использования недокументированных функций. Так что способ копирования массивов с помощью slice более предпочтителен.
Также отметим, что мы не в состоянии подобным образом копировать клипы. Даже упомянутое только что снятие защиты не поможет нам скопировать многие поля, а тем более - содержимое клипа (то, что в нем нарисовано).
Теперь обсудим, хорошо ли использовать подобную функцию копирования при создании новых объектов класса.
Во-первых, скопированный таким образом объект занимает много места. Мы ведь не смотрим, какие поля и методы будут изменяться, а какие - нет, размножаем все подряд. Особенно это касается рекурсивного копирования. Но и без рекурсии, как мы увидим далее, мы используем много лишней памяти, если наш объект занимает нижнее место в длинной иерархии классов. Наконец, если мы создаем много объектов за короткое время, то нам будет мешать тот факт, что для копирования всех полей времени уходит довольно много.
Есть изящное решение всех этих проблем. Нужно хранить в самом объекте только те поля и методы, которые изменились по сравнению с прототипом (или вовсе в прототипе отсутствуют). А за всеми прочими методами и полями обращаться прямо в прототип. Но для того, чтобы реализовать это, объект должен иметь возможность каким-то образом связаться с прототипом и "вытащить" из него все, что нужно. А есть ли в объекте ссылка на прототип? Если мы пройдемся по полям объекта в цикле for...in, мы не обнаружим ничего необычного. Тем не менее, в документации написано, что такая ссылка есть и называется она __proto__ (обратите внимание на два подчеркивания с каждой стороны). Почему же for...in ничего не сообщает нам о ней? Дело в том, что эта ссылка, как и ряд других, имеет специальный атрибут, скрывающий ее от итерирования в цикле for...in. Однако нам сейчас важно все подобные ссылки разыскать, и, оказывается, существуют средства для работы и изменения подобных атрибутов ссылок. Средства эти, правда, недокументированные, но воспользоваться ими в исследовательских целях сейчас самое время.

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

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

Снимаем и ставим защиту

Глобальная функция ASSetPropFlags имеет четыре параметра. Первый из них - это ссылка на объект, с которым мы будем работать. Второй - список полей, с которыми нужно совершать действия по изменению атрибутов. Этот список может быть представлен в виде массива строк или же в виде одной строки, внутри которой имена полей разделены запятыми. Последнее, кстати, означает, что мы можем передать в качестве второго аргумента строку с именем одного-единственного поля. Наконец, в качестве второго аргумента можно передать null, и тогда указанные нами действия будут производиться со всеми полями объекта. О том, какие же это будут действия, говорят третий и четвертый параметры. В третьем параметре передается информация о том, какие атрибуты нужно установить: атрибут hidden (скрытие от for...in), атрибут protect delete (защита от удаления) или атрибут protect overwrite (защита от перезаписи). Первому из атрибутов соответствует самый младший бит (соответствующий 20, то есть 1), второму - следующий (21 = 2), третьему - бит, соответствующий 22 = 4. Прочие биты этого параметра не используются, или, точнее, если и используются, то для внутренних секретных целей Флэш. Нам они не понадобятся. Что же касается четвертого параметра функции, то он полностью аналогичен третьему, но определяет те атрибуты, которые будут стерты у соответствующего поля. Это как раз то, что нам нужно: в дальнейшем мы будем использовать ASSetPropFlags для того, чтобы стирать у различных полей атрибут hidden. А пока что посмотрим на тестовых примерах, как работает установка различных атрибутов. Для того чтобы проверять, установлен или нет какой-либо атрибут, нам пр идется пойти на некоторые ухищрения: удачное удаление говорит, что не установлена защита от удаления, удачная замена значения сообщает, что нет защиты от перезаписи. Для проверки, установлен ли атрибут hidden мы могли бы попробовать отыскать данное поле при помощи for...in. Но, оказывается, можно обойтись и без этого: существует недокументированная функция isPropertyEnumerable, которая записана в Object.prototype и, таким образом, может быть вызвана в качестве метода любого объекта. Итак, вот обещанный пример:

   // Возвращает массив с данными о том, какие атрибуты
   // установлены у переменной 
_global.getAttributesReport = function(obj, prop){
   var overProt = false, delProt = false; 
      // hidden проверяем напрямую 
   var hidden = !obj.isPropertyEnumerable(prop);
   var tmp = obj[prop];
   if (tmp !== undefined){
         // проверяем защиту от перезаписи                   
      obj[prop] = !tmp;
      if (obj[prop] == tmp) overProt = true;
         // проверяем защиту от удаления
      delete obj[prop];
      if (obj[prop] !== undefined) delProt = true;
      obj[prop] = tmp;
   }
   return [hidden, delProt, overProt];
}
   // Выводит информацию об атрибутах в развернутом виде
_global.traceAttributesReport = function(obj, prop){
   var report = getAttributesReport(obj, prop);
   trace("======== property: " + prop + " ===========");
   trace("hidden = " + report[0]);
   trace("delete protection = " + report[1]);
   trace("overwrite protection = " + report[2]);
}
obj1 = {a: 10, b: 20, c: "str"};
trace(getAttributesReport(obj1, "a"));
trace(getAttributesReport(obj1, "b"));
trace(getAttributesReport(obj1, "c"));
trace("-----------------");
   // Устанавливаем полю b атрибуты hidden и delete protected
ASSetPropFlags(obj1, "b", 3); 
trace(getAttributesReport(obj1, "a"));
trace(getAttributesReport(obj1, "b"));
trace(getAttributesReport(obj1, "c"));
trace("-----------------");
   // Устанавливаем полю c атрибут overwrite protected
ASSetPropFlags(obj1, "c", 4);
trace(getAttributesReport(obj1, "a"));
trace(getAttributesReport(obj1, "b"));
trace(getAttributesReport(obj1, "c"));
trace("-----------------");
  // Устанавливаем всем полям сразу атрибуты hidden и 
   // overwrite protected и пытаемся одновременно снять hidden
ASSetPropFlags(obj1, null, 5, 1);
trace(getAttributesReport(obj1, "a"));
trace(getAttributesReport(obj1, "b"));
trace(getAttributesReport(obj1, "c"));
trace("-----------------");
  // Снимаем у всех полей атрибут hidden
ASSetPropFlags(obj1, null, 0, 1);
trace(getAttributesReport(obj1, "a"));
trace(getAttributesReport(obj1, "b"));
trace(getAttributesReport(obj1, "c"));
trace("-----------------");
   // А теперь выводим атрибуты у двух 
   // недокументированных функций.
traceAttributesReport(_global, "ASSetPropFlags");
traceAttributesReport(Object.prototype, "isPropertyEnumerable");
        

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

false,false,false
false,false,false
false,false,false
-----------------
false,false,false
true,true,false
false,false,false
-----------------
false,false,false
true,true,false
false,false,true
-----------------
true,false,true
true,true,true
true,false,true
-----------------
false,false,false
false,true,true
false,false,false
-----------------
======== property: ASSetPropFlags ===========
hidden = true
delete protection = false
overwrite protection = false
======== property: isPropertyEnumerable ===========
hidden = true
delete protection = true
overwrite protection = false
        

Из нашего примера видно, что при конфликте третьего и четвертого аргумента (то есть при попытке установить и сбросить один и тот же флаг) приоритетом обладает третий.


Что прячется в системных объектах
Теперь, конечно, трудно удержаться от того, чтобы снять флаги hidden со всех полей разнообразных системных объектов и вывести их при помощи for...in. Для начала проделаем это с объектом _global, а также посмотрим, какие методы наследуются во все объекты из Object.prototype. Вот код, который выводит в консоль все интересующие нас сведения.
trace("----------------------");
trace("|||||||||| Object.prototype |||||||||||");
trace("----------------------");
ASSetPropFlags(Object.prototype, null, 0, 1);
for (var name in Object.prototype) trace(name);
trace("----------------------");
trace("|||||||||| _global |||||||||||");
trace("----------------------");
ASSetPropFlags(_global, null, 0, 1);
for (var name in _global) trace(name);

В результате получаем вот что:
----------------------
|||||||||| Object.prototype |||||||||||
----------------------
toLocaleString
isPropertyEnumerable
isPrototypeOf
hasOwnProperty
toString
valueOf
addProperty
unwatch
watch
constructor
----------------------
|||||||||| _global |||||||||||
----------------------
CustomActions
MMSave
Cookie
System
Accessibility
Video
Stage
TextFormat
TextField
Button
Key
Mouse
Selection
XML
XMLNode
Sound
Math
Array
String
Date
Boolean
Number
o
clearInterval
setInterval
isFinite
isNaN
updateAfterEvent
trace
parseFloat
parseInt
unescape
escape
ASSetNative
ASSetPropFlags
LocalConnection
SharedObject
Microphone
Camera
NetStream
NetConnection
Color
AsBroadcaster
XMLSocket
LoadVars
MovieClip
Infinity
NaN
Function
Object
ASconstructor
ASnative

Мы видим, что те методы, которые мы можем найти в каждом объекте, действительно происходят из Object.prototype. Кроме того, мы видим там и несколько недокументированных функций. Огромное число как документированных, так и недокументированных объектов и функций мы находим в объекте _global. В частности, именно там мы видим ссылки на конструкторы самых важных классов, таких, как Number, String, Array, MovieClip и т.д. Даже функция ASSetPropFlags, с помощью которой мы обнаружили все это хозяйство, находится там. Мы не станем здесь подробно описывать поведение каждой их недокументированных функций, ведь эта книга ни в коей мере не является справочником. Тем, кто интересуется подробностями, порекомендуем ресурс http://web.archive.org/web/20040603171453/chattyfig.figleaf.com/flashcoderswiki/index.php?Undocumented+Features (это архивная копия документа с ныне закрытой wiki-конференции flashcoders-wiki). Также можно зайти на сайт FlashGuru http://www.flashguru.co.uk/tutorials.php. По этому адресу расположен список статей, в котором можно найти многие из обнаруженных нами недокументированных вещей. Также можно воспользоваться поиском на сайте Macromedia - многие классы и функции из недокументированных возможностей Flash MX превратились во вполне стандартные свойства системы Flash MX 2004, в описании которой вы сможете с ними познакомиться.

Могущество, но не всемогущество
Казалось бы, мы можем теперь снять защиту с любого метода и переопределить его. На самом деле это не так. Например, мы не сможем переопределить метод trace. Также мы не можем вмешаться в работу некоторых других механизмов, например, в работу механизма ссылок на _parent у MovieClip (правда, можно завести переменную с именем _parent и она скроет, хотя и не переопределит, системную ссылку _parent; удалив эту переменную, мы увидим информацию в системной ссылке _parent в целости и сохранности).
Тем не менее, большинство полей и методов в объектах Flash MX может быть переопределено (в том числе рисовательные методы MovieClip, с которыми мы познакомимся в лекции 10). Причем эти методы можно не только переопределить в производных классах, но и вовсе подменить в прототипе, скажем, MovieClip. Аналогичным образом можно заменять метод toString во всех объектах (меняя метод прямо в прототипе Object). Думаем, не стоит говорить, что пользоваться этими возможностями следует с особой осторожностью. Особенно если фрагментами системы (библиотекой, программными компонентами), написанными вами, будут п ользоваться другие программисты. Ведь они, как правило, будут рассчитывать на то поведение объектов и функций, которое описано в документации по Flash MX, а не на то, которое устроите вы.
Используем полученные знания
Теперь мы можем, наконец, приступить к исследованиям скрытых механизмов реализации наследования и порождения объектов класса. То есть посмотрим, нет ли каких-нибудь скрытых полей, которые отсутствуют в прототипе Object, но имеются у вновь создаваемых объектов. Итак, запустим следующий простенький код.
a = new Object();
ASSetPropFlags(a, null, 0, 1);
for (name in a) trace(name);
На выходе получаем:
__constructor__
constructor
__proto__

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

Что означают скрытые поля

Поле __proto__

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

Поле constructor

Впрочем, на самом-то деле, имея объект, мы можем добраться и до его конструктора. Не зря же мы обнаружили поле constructor в каждом объекте. Разумеется, именно в нем и записана ссылка на конструктор класса, к которому принадлежит наш объект. Хотя пользоваться этим полем надо с осторожностью - оно недокументированное.
Итак, мы видим, что, имея объект myObject, мы можем обратиться к его прототипу двумя способами: или myObject.__proto__, или myObject.constructor.prototype.

Поле __constructor__

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

myObject = new Object();
myObject1 = {};
trace(myObject.__constructor__);
trace(myObject1.__constructor__);
        

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

[type Function]
undefined
        

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

Механизм copy on write для содержимого прототипа

Мы разобрались, что означают и куда указывают ссылки, создаваемые в объектах оператором new. Но для чего они используются?

Недостатки прямого копирования

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

Сущность copy on write

Поможет решить нашу проблему механизм, известный под названием copy on write, то есть копирование при записи. В данном случае мы можем трактовать его так: пока мы не пытаемся изменить объект, все значения его полей берутся из прототипа. То есть в объекте не заводится ни одной реальной ссылки. И только если мы захотим изменить значение какого-либо из полей - только тогда это поле действительно заводится в самом объекте. Конечно, это требует поддержки со стороны языка. Алгоритм работы извлечения значения поля мы можем представить себе таким образом: сначала ищем поле в самом объекте, если не находим - обращаемся к прототипу (а если и там нет - к его прототипу и т.д.). Все это мы можем проиллюстрировать следующим кодом:

a = {x: 10, y:20};
b = {x: 15, z:25};
c = {x: 21, t:31};
// Эмуляция настоящего __proto__
c.proto = b;  
b.proto = a;
_global.getFieldValByName = function(name){
   if (this[name] != undefined) return this[name];
   if (this.proto != undefined) 
      return getFieldValByName.call(this.proto, name);
      
   return undefined;
}
trace("b.x = " + getFieldValByName.call(b, "x"));
trace("c.y = " + getFieldValByName.call(c, "y"));
trace("----- На самом деле -----");
trace("b.x = " + b.x);
trace("c.y = " + c.y);
c.__proto__ = b;
b.__proto__ = a;
trace("----- После установки __proto__ -----");
trace("b.x = " + b.x);
trace("c.y = " + c.y);
        

Запустив этот код, обнаружим в консоли следующее:

b.x = 15
c.y = 20
----- На самом деле -----
b.x = 15
c.y =
----- После установки __proto__ -----
b.x = 15
c.y = 20
        

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

  1. Создает новый (пустой) объект.
  2. Устанавливает ссылку __proto__, чтобы она указывала на прототип (это и делает объект принадлежащим к данному классу).
  3. Устанавливает (недокументированную) ссылку constructor - она будет указывать на конструктор класса.
  4. Устанавливает (недокументированную) ссылку __constructor__ - она будет указывать на конструктор базового класса.
  5. Вызывает для вновь созданного объекта конструктор и передает ему указанные аргументы.

Исправляем эмуляцию new
А теперь, когда мы знаем детали работы new, мы можем исправить его эмуляцию. Давайте перепишем работу newFunc из последнего примера с лифтами и посмотрим, что получится.
_global.newFunc = function (constr, args){
var tmp = {}; // Или var tmp = new Object();

// Устанавливаем ссылки принадлежности к классу:
tmp.__proto__ = constr.prototype;
tmp.constructor = constr;
// Детали работы этой строчки мы разберем
// в лекции о наследовании.
tmp.__constructor__ = constr.prototype.constructor;
// Выделяем субмассив с аргументами, которые надо
// передать в конструктор (все, кроме нулевого -
// это сам конструктор) и вызываем его:
constr.apply(tmp, arguments.slice(1));
return tmp;
}
// Мы не переписываем сюда весь код лифта,
// но напоминаем содержание тестового примера
// Создаем 2 объекта
l1 = newFunc(lift, 2, 5);
l2 = newFunc(lift, 0, 3);
// Тестируем
trace("l1:");
l1.goto(4);
trace("l2:")
l2.goto(4);

Запустив пример с этой исправленной функцией, получим:
l1:
-----------
Закрываем двери.
Этаж 2
Этаж 3
Этаж 4
Открываем двери.
-----------
l2:
-----------
Закрываем двери.
Этаж 1
Этаж 2
Этаж 3
Открываем двери.
-----------

Таким образом, видим, что лифты ездят как надо, хотя мы не скопировали в них ни единого поля из прототипа.
Можно ли создать статическое поле в прототипе
Кое-что похожее на статическое поле создать можно без особых усилий. Во всяком случае, это будет некое хранилище данных, доступных для всех экземпляров класса. Выглядеть это будет так:
_global.myClass = function(){}
_global.myClass.prototype.staticField = 5;
a = new myClass();
b = new myClass();
// Значение статического свойства читаем как обычно
trace("a.staticField = " + a.staticField);
// А изменяем только напрямую через __proto__
a.__proto__.staticField = 10;
trace("b.staticField = " + b.staticField);

Этот код выводит вот что:
a.staticField = 5
b.staticField = 10

Как и следовало ожидать. Однако нужно соблюдать осторожность: если мы попробуем изменить staticField напрямую через объект (например, a.staticField = 10), мы, разумеется, внесем изменения лишь в сам объект, а не в прототип. И другим объектам этого класса изменения будут недоступны. Можно исправить ситуацию, заведя не просто статическое поле, а свойство с функциями доступа. Получается вот что:
_global.myClass = function(){}
// setter и getter размещаем в прототипе
_global.myClass.prototype.setStField =
function(val){this.__proto__.stFieldData = val;};
_global.myClass.prototype.getStField =
function(){return this.__proto__.stFieldData;};

// И прямо в прототипе создаем свойство
_global.myClass.prototype.addProperty(
"staticField",
_global.myClass.prototype.getStField,
_global.myClass.prototype.setStField
);
// Устанавливаем начальное значение
_global.myClass.prototype.staticField = 5;
a = new myClass();
b = new myClass();
// Значение статического свойства читаем как обычно
trace("a.staticField = " + a.staticField);
// И записывать теперь можно тоже как обычно
a.staticField = 10;
trace("b.staticField = " + b.staticField);

На выходе получаем, как и в прошлый раз
a.staticField = 5
b.staticField = 10

К сожалению, недостатки есть и у этого варианта. Вспомним: в С++ и в Java к статической переменной можно обращаться не только через объект, но и через класс. Мы, конечно, можем написать что-то вроде MyClass.prototype.staticField = 10, но... нельзя ли, чтоб было совсем похоже на С++ или Java (особенно на Java)? Без всякого слова prototype? Можно! Добро пожаловать за этим в следующий параграф!
Способы эмуляции статических и приватных полей и методов
Этот параграф может быть пропущен при первом чтении. Заинтересует же он прежде всего тех, кто хочет непременно эмулировать на Флэше любимые элементы семантики Java и C++. А также любителей головоломок.
Статическое поле (свойство)
Усовершенствовать статические поля из предыдущего параграфа можно по двум направлениям:

  • Устроить так, чтобы все-таки можно было обращаться к статическому полю только через имя класса.
  • Спрятать получше переменную в прототипе, чтобы к ней нельзя было обратиться напрямую. (Это нужно, если ваши getter и setter делают какую-то дополнительную работу, кроме простой выдачи и установки значения; и вы хотите, чтобы весь доступ к свойству шел только через них.)

Вот три варианта кода. Один решает только первую из указанных проблем, второй решает обе (хотя делает он тривиальные getter и setter). Наконец, третий позволяет getter и setter задавать. Во всех вариантах дополнительная функция добавляется в Function.prototype и становится доступной как метод любого объекта-функции (в том числе - конструктора класса). Отметим также, что использованные здесь идеи принадлежат Тимоти Гролео (Timothee Groleau), его прекрасные статьи, упомянутые в списке литературы мы всячески рекомендуем. Итак, сначала самый простой вариант:
Function.prototype.addStaticProperty = function(name, propVal){
// Устанавливаем начальное значение
this.prototype[name] = propVal;
// Сохраняем ссылку на нужный нам объект-функцию
// (конструктор) в переменной, к которой будет иметь
// доступ сгенерированная функция
var thisVar = this;
var getter = function() {
return thisVar.prototype[name];
}
var setter = function(newVal) {
thisVar.prototype[name] = newVal;
}
// Как сам конструктор, так и прототип должны получить
// это свойство (ссылающееся на одни и те же данные).
this.addProperty(name, getter, setter);
this.prototype.addProperty(name, getter, setter);
}
// Прячем метод от for...in, наподобие системных методов
// В принципе, этого можно и не делать.
ASSetPropFlags(Function.prototype, "addStaticProperty", 1);

   // Тестируем
_global.SomeClass = function(){}
SomeClass.addStaticProperty("testProp", "Тестовое значение");
a = new SomeClass();
b = new SomeClass();
trace(a.testProp);
SomeClass.testProp = "Второе тестовое значение";
trace(b.testProp)
b.testProp = "Третье тестовое значение";
trace(SomeClass.testProp);

На выходе получим:
Тестовое значение
Второе тестовое значение
Третье тестовое значение

Так что статическое свойство успешно работает. Теперь давайте скроем данные, к которым обращается свойство, - поместим их не в прототип, а в контекст вызова addStaticProperty. Вот так:
Function.prototype.addStaticProperty = function(name, propVal){
// Храним значение прямо в контексте вызова
// addStaticProperty, прямо в аргументее ее.
var getter = function() {
return propVal;
}
var setter = function(newVal) {
propVal = newVal;
}
// Как сам конструктор, так и прототип должны получить
// это свойство (ссылающееся на одни и те же данные).
this.addProperty(name, getter, setter);
this.prototype.addProperty(name, getter, setter);
}

Весь остальной текст программки оставляем таким же; тестовый запуск дает тот же самый результат. Наконец, если мы хотим сами установить getter и setter, нам придется поступить следующим образом:
// Суффикс GS в названии функции означает "Getter, Setter"
Function.prototype.addStaticProperty_GS = function(name, propVal, argGetter, argSetter){
// Храним значение прямо в контексте вызова
// addStaticProperty, прямо в аргументее ее.
var getter = function() {
return argGetter(propVal);
}
var setter = function(newVal) {
propVal = argSetter(newVal);
}
// Как сам конструктор, так и прототип должны получить
// это свойство (ссылающееся на одни и те же данные).
this.addProperty(name, getter, setter);
this.prototype.addProperty(name, getter, setter);
}
// Прячем метод от for...in, наподобие системных методов
// В принципе, этого можно и не делать.
ASSetPropFlags(Function.prototype, "addStaticProperty", 1);
// Ссылка будет локальной, только если мы поместим
// весь этот код в функцию. Но мы хотим подчеркнуть,
// что testGetter по смыслу - временная переменная.
var testGetter = function(intrinsicVar){
return "Возвращаем: " + intrinsicVar;
}
// Комментарий, который мы написали к testGetter,
// относится и сюда тоже.
var testSetter = function(newValue){
return newValue.toUpperCase();
}
// Тестируем
_global.SomeClass = function(){}
SomeClass.addStaticProperty_GS("testProp", "Тестовое значение", testGetter, testSetter);
a = new SomeClass();
b = new SomeClass();
trace(a.testProp);
SomeClass.testProp = "Второе тестовое значение";
trace(b.testProp)
b.testProp = "Третье тестовое значение";
trace(SomeClass.testProp);

Обратите внимание, что getter в данном случае принимает аргумент: значение внутренней переменной, в которой хранятся данные; setter, в отличие от обычного поведения, возвращает значение, которое будет этой внутренней переменной присвоено.
Запускаем код на выполнение и получаем:
Возвращаем: Тестовое значение
Возвращаем: ВТОРОЕ ТЕСТОВОЕ ЗНАЧЕНИЕ
Возвращаем: ТРЕТЬЕ ТЕСТОВОЕ ЗНАЧЕНИЕ

Забавный эффект: начальное значение свойства не прошло через функцию-setter и поэтому не приведено к верхнему регистру. Если вы считаете, что подобное поведение провоцирует ошибки (а не "дает некоторую гибкость", например), вы легко можете это исправить, вставив в конце функции addStaticProperty_GS строчку propVal = argSetter(propVal).

Статический метод
Аппетит приходит во время еды; теперь мы можем вспомнить о том, что в C++ и Java бывают также и статические методы. Им разрешен доступ только к статическим полям и методам класса, но вызывать их можно как через класс, так и через объект. Раз уж поля мы сделали статическими, никто не мешает сделать статическими и методы. К статическим свойствам эти методы будут обращаться через класс. Более того, и вызваны эти методы должны быть как методы класса (то есть как если бы они были полями объекта-функции конструктора). Тогда this в этих методах будет указывать на класс, а не на конкретный объект. Иначе статические методы получат доступ и к обычным (не статическим) полям и методам, чего мы как раз хотим избежать. Таким образом, для заводимых в прототипе ссылок на нужные нам статические методы мы должны будем где-то сохранить ссылку на класс (то есть его конструктор). Далее мы будем следовать методике Тимоти Гролео и хранить все нужные вещи в контексте вызова функции addStaticMethod, которая приведена ниже.
// Для тестирования нам надо добавлять не только статические
// методы, но и свойства. Здесь используем более простой
// вариант кода, без пользовательских getter'ов setter'ов
Function.prototype.addStaticProperty = function(name, propVal){
// Храним значение прямо в контексте вызова
// addStaticProperty, прямо в аргументе ее.
var getter = function() {
return propVal;
}
var setter = function(newVal) {
propVal = newVal;
}
// Как сам конструктор, так и прототип должны получить
// это свойство (ссылающееся на одни и те же данные).
this.addProperty(name, getter, setter);
this.prototype.addProperty(name, getter, setter);
}
// Прячем метод от for...in, наподобие системных методов
// В принципе, этого можно и не делать.
ASSetPropFlags(Function.prototype, "addStaticProperty", 1);
// Простейший вариант добавления статического метода
Function.prototype.addStaticMethod = function(name, staticMethodEngene) {
var theClass = this;
var staticMethod = function() {
return staticMethodEngene.apply(theClass,
arguments);
};
this[name] = staticMethod;
this.prototype[name] = staticMethod;
}
// Прячем метод от for...in, наподобие системных методов
// В принципе, этого можно и не делать.
ASSetPropFlags(Function.prototype, "addStaticMethod", 1);
// Тестируем статическое свойство
_global.SomeClass = function(){}
SomeClass.addStaticProperty("testProp", "Тестовое значение");
a = new SomeClass();
b = new SomeClass();
trace(a.testProp);
SomeClass.testProp = "Второе тестовое значение";
trace(b.testProp)
b.testProp = "Третье тестовое значение";
trace(SomeClass.testProp);
// Тестируем статический метод
b.someStr = "Некоторая строка";
f = function(){
trace("this.testProp.toLowerCase = " + this.testProp.toLowerCase());
trace("this.someStr = " + this.someStr);
}
SomeClass.addStaticMethod("printTest", f);
trace("");
trace("--- b.printTest() ----");
b.printTest();
trace("--------------");
trace("");
trace("--- SomeClass.printTest() ----");
SomeClass.printTest();
trace("------------------");
trace("");
trace("b.someStr = " + b.someStr);

Выводит этот код, как и ожидалось,
Тестовое значение
Второе тестовое значение
Третье тестовое значение
--- b.printTest() ----
this.testProp.toLowerCase = третье тестовое значение
this.someStr =
--------------
--- SomeClass.printTest() ----
this.testProp.toLowerCase = третье тестовое значение
this.someStr =
------------------
b.someStr = Некоторая строка

Вы видите, что к полю someStr у нашего статического метода нет доступа. В то же время статическое свойство testProp ему замечательно видно.
Для более глубокого понимания происходящих здесь вещей попробуйте угадать, что выведет вызов b.printTest() в том случае, когда мы добавим f не с помощью addStaticMethod, а с помощью addStaticProperty. Для того чтобы побольше запутать вас, приведем сначала неправильный способ рассуждений. Он состоит в том, что getter возвращает объект-функцию, который хранится в контексте вызова addStaticProperty. Тогда this указывает на этот контекст, в котором отсутствуют как testProp, так и someStr. Таким образом, вместо их значений мы получим пустоту. Следующая мысль, которая приходит в голову, - это то, что getter, возвращая ссылку на объект-функцию, сохраняет ее в неком временном хранилище, к которому и будет относиться this. (Для сравнения загляните в лекцию 5, параграф "Функция как объект" и еще раз просмотрите пример, иллюстрирующий использование возвращаемых значений типа Function.) Вывод тот же самый - мы увидим пустое место вместо значений строк. Однако запустите наш код, заменив вызов addStaticMethod на addStaticProperty. И вы увидите, что будет выведен, в частности, следующий фрагмент:
--- b.printTest() ----
this.testProp.toLowerCase = третье тестовое значение
this.someStr = Некоторая строка
--------------

Правильное же решение, которое полностью избавляет от всех сомнений, состоит в том, что Flash MX делает поведение свойств максимально похожим на поведение обычных полей. Так что раз уж мы написали b.printTest(), то в функцию printTest в качестве this будет передана именно ссылка на b. А то, что ссылка на printTest хранится где-то в другом месте (не в b) и нам ее возвращает getter - это нас волновать уже не должно.

Изменяемые статические методы
Что делать, если мы хотим изменить поведение статического метода, созданного нами? Простейший способ - снова вызвать addStaticMethod и передать ему в качестве первого аргумента то же самое имя метода, а вот в качестве второго - уже другую функцию. Однако великий и ужасный Тимоти Гролео предлагает другое решение. Это решение хорошо тем, что мы можем просто присваивать свойству, через которое мы доступаемся к нашему статическому методу, любую функцию; главное, чтобы эта функция подразумевала, что this внутри нее - ссылка на класс (то есть конструктор). Такое поведение удобно, особенно если мы передаем объекты с уже созданными там статическими методами в код, уже давно созданный и предполагающий именно такое (стандартное) поведение. Или же этот код будет создаваться другими людьми и нам проще им сказать "все работает как обычно", чем объяснять, что надо всякий раз вызывать addStaticMethod. Итак, новая версия addStaticMethod от Гролео, позволяющая просто присваивать новые статические методы на место старых, вылядит так:
Function.prototype.addStaticMethod = function(name, staticMethodEngene) {
var theClass = this;
var getter = function() {
return staticMethodEngene;
};
// Setter должен сгенерировать новый метод - который вызовет
// newMethod,
// передав ему theClass в качестве this.
var setter = function(newMethod) {
staticMethodEngene = function() {
return newMethod.apply(theClass, arguments);
};
};
setter(staticMethodEngene);
this.addProperty(name, getter, setter);
this.prototype.addProperty(name, getter, setter);
}

Заменим на этот вариант addStaticMethod из предыдущего примера; и добавим в конце примера следующий проверочный код:
g = function(){
trace("Функция g вступает в работу.");
trace("this.testProp.toUpperCase = " + this.testProp.toUpperCase());
trace("this.someStr = " + this.someStr);
}
trace("");
SomeClass.printTest = g;
trace("Произведена замена функции: SomeClass.printTest = g;");
trace("");
trace("--- b.printTest() ----");
b.printTest();
trace("--------------");

Запускаем все это и получаем:
Тестовое значение
Второе тестовое значение
Третье тестовое значение
--- b.printTest() ----
this.testProp.toLowerCase = третье тестовое значение
this.someStr =
--------------
--- SomeClass.printTest() ----
this.testProp.toLowerCase = третье тестовое значение
this.someStr =
------------------
b.someStr = Некоторая строка
Произведена замена функции: SomeClass.printTest = g;
--- b.printTest() ----
Функция g вступает в работу.
this.testProp.toUpperCase = ТРЕТЬЕ ТЕСТОВОЕ ЗНАЧЕНИЕ
this.someStr =
--------------

Итак, setter сработал как надо: сгенерировал новую функцию, которая вызывает g, передавая ей с помощью apply ссылку на объект-функцию конструктора. И далее при работе g эта ссылка используется в качестве this. Таким образом, мы действительно получили доступ к статическому свойству testProp и не получили - к обычному полю объекта b (а именно, к полю someStr).

Создание приватных полей
Как вы, наверное, помните, даже с помощью недокументированных приемов нам не удается полностью скрыть поля (или методы) от воздействия внешних функций. В результате мы не можем быть уверены, что человек, использующий наши классы, не изменит там какое-нибудь поле (а потом все будут долго искать ошибку). Вывод: во многих случаях полезно защитить наши поля от неверного использования извне. Применение контекста вызова как места хранения данных поможет нам и в этом случае. Следуя по стопам Гролео, рассмотрим три различных варианта. Первый вариант: функции getter и setter должны быть вызваны явно. Второй вариант: используем addProperty, getter и setter вызываются неявно при обращении к свойству как к обычному полю. Наконец, третьим будет вариант с перекрестным использованием приватных свойств несколькими различными методами класса. Следует заметить, что для первых двух вариантов будет применен прием, с которым мы встречались в предыдущих параграфах: добавление нужных функций в прототип одного из системных типов (в данном случае - в Object), в результате чего все объекты этого типа смогут использовать добавленные нами методы. Как мы увидим в следующей лекции, эта возможность использования новых методов относится и к объектам производных типов.
Итак, сначала мы рассмотрим, как делаются первые два варианта приватных свойств. Правила для применяемых здесь getter'ов и setter'ов те же, что и при реализации статических свойств: getter принимает аргумент - значение внутреннего поля, в котором хранятся данные; setter возвращает значение, которое нужно этому внутреннему полю присвоить. Вот код, в котором для первого варианта применяется функция addExplicitPrivateProperty, а для второго - addImplicitPrivateProperty:
// Будем исследовать этой функцией объекты, чтобы
// убедиться, что приватные свойства там не спрятаны.
_global.dumpObj = function(obj){
// Снимаем "защиту" со скрытых полей
ASSetPropFlags(obj,null,0,1);
for(name in obj){
trace(name + ": " + obj[name]);     
}
}
// Приватное свойство с явными getter'ом и setter'ом
Object.prototype.addExplicitPrivateProperty = function(name, getter, setter) {
var propVal;
this["get" + name] = function() {
return getter.call(this, propVal);
};
this["set" + name] = function(newVal) {
propVal = setter.call(this, newVal);
};
}
// По традиции скрываем то, что мы добавили в системные объекты
ASSetPropFlags(Object.prototype, "addExplicitPrivateProperty", 1);
// Приватное свойство с неявными getter'ом и setter'ом
Object.prototype.addImplicitPrivateProperty = function(name, getter, setter) {
var propVal;
this.addProperty(name,
function() {
return getter.call(this, propVal);
},
function(newVal) {
propVal = setter.call(this, newVal);
}
);
}
// По традиции скрываем то, что мы добавили в системные объекты
ASSetPropFlags(Object.prototype, "addImplicitPrivateProperty", 1);
// Ключевое слово var стоит на случай переноса кода в функцию
var getter = function(propVal){
return "Значение: " + propVal;
}
// Ключевое слово var стоит на случай переноса кода в функцию
var setter = function(newVal){
return newVal.toString().toUpperCase();
}
// Тестируем
a = new Object();
b = new Object();
a.addExplicitPrivateProperty("testText", getter, setter);
b.addImplicitPrivateProperty("testText", getter, setter);
// Используем отсутствие чувствительности
// идентификаторов к регистру (более точные имена
// функций - settestText и gettestText).                                                                      
a.setTestText("Текст для объекта а");
trace(a.getTestText());
b.testText = "А такой текст будет в объекте b";
trace(b.testText);
trace("");
trace("----- Содержимое объекта а ------");
dumpObj(a);
trace("----------------------");
trace("");
trace("----- Содержимое объекта b ------");
dumpObj(b);
trace("----------------------");

На выходе этот код дает:
Значение: ТЕКСТ ДЛЯ ОБЪЕКТА А
Значение: А ТАКОЙ ТЕКСТ БУДЕТ В ОБЪЕКТЕ B
----- Содержимое объекта а ------
settestText: [type Function]
gettestText: [type Function]
__constructor__: [type Function]
constructor: [type Function]
__proto__: [object Object]
----------------------
----- Содержимое объекта b ------
testText: Значение: А ТАКОЙ ТЕКСТ БУДЕТ В ОБЪЕКТЕ B
__constructor__: [type Function]
constructor: [type Function]
__proto__: [object Object]
----------------------

Мы видим, что свойства замечательно работают, но до самих внутренних данных мы добраться не можем даже с помощью функции dumpObj (в случае объекта b свойство testText, которое мы видим - это созданное нами свойство. Это можно распознать по тому факту, что перед текстом выведен префикс "Значение:", то есть сработал getter).
Но как быть, если у нас есть несколько разных свойств, которые мы хотим сделать приватными, но при этом к ним (именно ко внутренним данным) должны иметь доступ несколько различных функций? В этом случае Гролео рекомендует заводить как сами свойства, так и функции, которые будут с ними работать, в контексте вызова конструктора. Сейчас мы приведем пример использования такой техники. Пример будет описывать "шифрованную" строчку. Шифрование применим простейшее - циклическим сдвигом. При этом внутренние данные будут храниться в нешифрованном виде, но наружу они будут выдаваться уже зашифрованными. Также мы сделаем функцию для проверки, правильно ли расшифрована строка. Вот код этого примера:
_global.CodedStringContainer = function(baseForCoding, stringToStore){
// Секретные данные
var uncoded_str = stringToStore;
// Эти данные несекретны, но просто не нужны снаружи
var spaceCode = 32;
var maxCode = 127;
var diff = maxCode - spaceCode;
// Для использования в сгенерированной внутри приватной функции
var thisVar = this;
// Эта функция в принципе могла бы быть и публичной, нарушить
// целостность данных она не позволяет. Но, с другой стороны,
// снаружи она явно не нужна
var updateCodedString = function(){
// не забудем, что в приватной функции (которая не сохранена
// в this) ключевое слово this указывает не туда, куда обычно.
thisVar.coded_str = "";
// Кодируем
for (var i=0; i<uncoded_str.length; i++){
var charCode = uncoded_str.charCodeAt(i);
charCode = (charCode - spaceCode + baseForCoding)%diff
+ spaceCode);
thisVar.coded_str += String.fromCharCode(charCode);
}
}
// В принципе, такой интерфейс делать необязательно,
// можно удовольствоваться установкой в конструкторе
this.setUncodedString = function(str){
uncoded_str = str;
updateCodedString();
}
// Проверка - знаем ли мы "пароль".
this.isStringEqualToUncoded = function(str){
return uncoded_str === str; 
}
// кодируем переданную строчку
updateCodedString();
}
_global.CodedStringContainer.prototype.getCodedString = function(){
return this.coded_str;              
}
// Проверяем, что получилось
container = new CodedStringContainer(20, "MyString");
trace(container.getCodedString());
trace(container.isStringEqualToUncoded("MyString"));
trace(container.isStringEqualToUncoded("YourString"));
trace("---------------");
// Меняем строку
container.setUncodedString("YourString");
trace(container.getCodedString());
trace(container.isStringEqualToUncoded("MyString"));
trace(container.isStringEqualToUncoded("YourString"));

В результате выполнения этого кода увидим вот что:
a.g)'}#{
true
false
---------------
m$*'g)'}#{
false
true

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

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика