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 для профессиональных программистов
2.Базовые понятия Action Script
 
Откуда взялся ActionScript

Ответ на вопрос, вынесенный в заголовок этого параграфа, прост и сложен одновременно. Разумеется, язык ActionScript был разработан фирмой Macromedia, выпускающей Флэш. Однако разработан не на пустом месте, а на основе стандарта ECMA-262.

 

Сначала скажем, как расшифровывается аббревиатура ECMA: это European Computer Manufacturers Association (Европейская ассоциация производителей компьютеров). Эта ассоциация (разумеется, не она одна) занимается стандартами в области вычислительной техники.

 

ActionScript, как и другой известный язык JavaScript, базируется на стандарте ECMA-262. (Фактически, стандарт был сделан на основе начальных версий JavaScript, а не наоборот, а вот ActionScript уже был сделан на основе стандарта. Конечно, само существование стандарта на развитие языка JavaScript также повлияло). Стандарт разработан специально для скриптовых языков, поэтому не специфицирует полностью все встроенные объекты. То есть для JavaScript встроенными объектами являются браузер и его окна, для ActionScript - клипы, кнопки, текстовые поля и компоненты Flash. Сам стандарт ECMA-262 имеет несколько версий, самая свежая, (к которой и имеет непосредственное отношение ActionScript 1.0) - это версия 3. (Вообще-то готовится версия 4, и на основе ее бета-версии уже сделан язык ActionScript 2.0, используемый во Флэш МХ 2004. Но его мы не будем рассматривать в этой книге). Надо отметить, что стандарт в ActionScript поддержан все-таки не полностью. Не реализована обработка исключений и регулярных выражений, ограничена функциональность оператора eval (об этом см. в лекции 3). Тем не менее, ActionScript соответствует стандарту в значительной степени.

 

Если вы хотите ознакомиться со стандартом ECMAScript, он же ECMA-262, вы можете это сделать на сайте ECMA (http://www.ecma-international.org, конкретный адрес http://www.ecma-international.org/publications/standards/Ecma-262.htm ), или на сайте Mozilla в разделе, посвященном языку JavaScript (http://www.mozilla.org/js/language/ , здесь вы сможете найти стандарт в формате Microsoft Word).

 

Еще раз скажем, что последней версией стандарта является третья; вторая версия в свое время была утверждена в качестве стандарта ISO/IEC 16262, однако в настоящее время ISO уже отозвала этот стандарт.

 

Также отметим, что большинство нововведений, которые были сделаны в третьей версии стандарта (такие, как регулярные выражения, обработка исключений и т.п.), не вошли в ActionScript.

 

Почему вам будет легко освоить ActionScript

Вашу работу на ActionScript значительно облегчит то, что большинство простых конструкций, которые сработают на С++ или Java, сработают и на ActionScript. Так что, если перед вами стоит задача написать кусочек кода в 50 строк - попробуйте написать код, который бы заработал и в C++ и в Java (учтите, что объявлять переменные заранее не нужно). С довольно большой вероятностью вам удастся заставить вашу программу вести себя так, как вы того хотите. Существенный минус такого способа (помимо того, что он не годится для программ более 100 строк) состоит в том, что остается ощущение некачественно сделанной работы. Профессионал-программист обычно не только хочет, чтобы его программа работала, он еще и хочет понимать, почему именно она работает. Так что давайте вникать в детали - начиная с самых базовых вещей. Но при этом мы все же будем считать, что вы понимаете общий смысл кода на ActionScript - даже если некоторые применяемые там вещи мы еще не обсуждали. Для профессионала С++ или Java такое предположение будет, без сомнения, верным. Если же вы, по каким-либо причинам, используете эту книгу, впервые изучая язык с С-подобным синтаксисом - запаситесь еще одним учебником, в котором этот синтаксис рассмотрен более подробно. (В любом случае вам не помешает какая-нибудь толстая книжка по Флэш-дизайну из тех, что указаны в списке литературы.)

 

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

 

Типы данных (примитивные и объектные)

Ну что ж, начнем наше путешествие вглубь ActionScript.

 

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

 

В С++ провести разделение между типами можно, задав вопрос, применим ли к объектам данного типа оператор "точка". Храниться же могут как объекты, так и данные примитивных типов, в одних и тех же областях памяти: в стеке, в статической памяти, в куче. В Java разделение по применимости оператора "точка" тоже присутствует, однако есть и еще одна важная вещь. Объекты хранятся в отдельном месте, переменная объектного типа не содержит объект, это лишь ссылка на него. Примитивные же типы хранятся прямо в недрах объектов, так что переменная примитивного типа представляет собой именно хранилище соответствующих данных, а не ссылку.

 

Во Флэше все обстоит по-другому. Не так, как в С++, но и не так, как в Java. То есть оператор "точка" применим также и к примитивным типам. А где же хранятся реальные данные примитивных типов? Можно заглянуть в стандарт и все узнать... но нас такая информация волновать не должна. Потому что есть весьма удобная точка зрения на примитивные типы, дающая ответ на все практические вопросы.

 

Примитивные типы

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

 

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

 

Какая же из точек зрения правильная? Согласно стандарту ECMAScript, первая ближе к истине. То есть имеются типы данных, не обладающие свойствами типа Object: дополнением к нему полей и методов. И вообще их содержимое не может быть изменено. С точки зрения С++ или Java это, тем не менее, объекты, поскольку собственными полями и методами они обладают. Вот мы и называем эти примитивные типы данных read-only-объектами. А какая же точка зрения удобнее? Как проще рассуждать: говоря о read-only-объектах, или об упаковке? На наш взгляд, опять-таки удобнее первая точка зрения. Но если вам нравится считать, будто число - это обычное число, и только при применении к нему оператора "." оно преобразовывается в некоторый временный read-only-объект, пожалуйста, рассуждайте так. Ибо средствами самого Флэш МХ никакой разницы между этими двумя точками зрения мы обнаружить не сможем.

 

Общеупотребительных примитивных типов не так много: это String, Number и Boolean. (Эти же типы могут быть не только примитивными, но и объектными - говоря иначе, объекты String, Number и Boolean не обязаны быть read-only). Как создать объект примитивного типа? (Да, не очень-то это хорошо звучит - "объект примитивного типа" - но в данном случае все так и есть. Примитивный во Флэш значит - read-only). Сделать это можно либо напрямую указав значение в тексте программы, либо вычислив какое-либо выражение, значение которого имеет тип String, Number или Boolean. Естественное исключение из указанного выше правила - вызов методов каких-либо объектов. Эти методы могут вернуть ссылку на объектный String, Number или Boolean. Однако, если вы вызываете методы встроенных объектов Флэш - можете не беспокоиться, во всех случаях, когда возвращаемый тип бывает примитивным, возвращается именно примитивное значение. Еще одно исключение - использование оператора eval, с которым мы познакомимся позднее. Вот примеры выражений, дающих в результате примитивные типы:

 
"строка", 1е-12, 5<4, false + "10", 1. + "1"
           

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

 
a = (5 < 4).toString(); // В переменную записывается слово false
trace("a.length = " + a.length);
trace("9+6..toString() = " + 9+6..toString());
trace("строка, набранная строчными буквами".toUpperCase());
           

Этот код выводит в консоль

 
a.length = 5
9+6..toString() = 96
СТРОКА, НАБРАННАЯ СТРОЧНЫМИ БУКВАМИ
           

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

 

Для порядка проверим, что все, что сказано в этом подпараграфе про скобки, также работает. В самом деле, запустив код

 
trace(4+(7).toString());
trace(4+(7));
           

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

 
47
11
           

так что синтаксис со скобками работает, как ожидалось.

 

Объектные типы

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

 
a = new Object();
           

Теперь в а хранится ссылка на этот объект. Если вы специалист в С++, но не знаете Java, то обратите внимание, что оператор new вернул не указатель (в Java нет указателей), а ссылку. И к полю по имени i объекта a можно обратиться как обычно, через точку:

 
a.i = 5;
           

Правда, мы не предпринимали никаких усилий для того, чтобы создать в объекте a поле i. Но во Флэше это необязательно! Как мы увидим далее, поле создастся в таком случае автоматически (что очень плохо для отладки, но хорошо для расширения возможностей Флэш МХ). Кстати, поле i в объекте а теперь является ссылкой на read-only объект, представляющий собой число 5. Кроме ссылок, в объектах Флэш не хранится ничего. Даже методы - и те являются ссылками на объекты-функции (подробнее об этом - далее). Сами же объекты хранятся отдельно и иначе как через ссылки нам недоступны. А объекты, на которые никто не ссылается, удаляет сборщик мусора. Детали работы этого сборщика остаются тайной. И подстегнуть его работу вручную, как это делается в Java, во Флэше, к сожалению, нельзя.

 

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

 

Вот взгляните на такой пример:

 
a = {f: 4, g: 10}; // Создает объект а с полями f = 4 и g = 10
trace(a.g);
delete a.g;
trace(a.g);
           

После запуска этот код выводит в консоль

 
10
undefined
           

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

 
a = {f: 4, g: 10, k: {e: 50, r: 60}};
           

Можно даже вызвать функцию у такого созданного "впопыхах" объекта:

 
a = {f: 4, g: 10, k: {e: 50, r: 60}.toString()};
trace("a.k = " + a.k);
           

Запускаем (Ctrl+Enter, как обычно) и получаем в консоли

 
a.k = [object Object]
           

Значение [object Object] - это то, что функция toString() выводит по умолчанию. Для вашего собственного объекта вы сможете ее переопределить (см. параграф "Функции и методы").

 

"Наращивание" объекта

Мы сказали, что объект Флэш МХ можно представлять себе как хэш-таблицу. Но хэш-таблица тем и ценна, что можно в нее записывать новые данные. А значит, мы можем создать в объекте новую переменную в любой момент. Есть разные способы создать в объекте новую переменную. Можно так: a.x = 5;. Даже если в объекте а не было переменной х, теперь она будет существовать. (Но если мы просто обратимся к несуществующей переменной, например вот так: trace(a.y), то получим результат undefined. Новая переменная при этом не создается.) Можно также создать новую переменную с именем, заданным в строке. Для этого есть два способа: set("a." + name, 5); и a[name] = 5;. (При создании переменной ей обязательно надо присвоить какое-нибудь значение. Мы в качестве этого значения берем число 5, а можно было бы взять, например, null. Впрочем, создавать поля заранее у объектов Флэш МХ, как вы видите, особого смысла нет. Так что создавайте переменные в тот момент, когда они понадобятся, и присваивайте им именно те значения, которые вам нужны.) Подробнее о способах создания переменных мы еще поговорим. А пока скажем только, что в большинстве случаев второй способ удобнее, хотя он и не является документированным. Как бы то ни было, использование этой экономной записи с квадратными скобками уже практически стало стандартом.

 

Если же вы в какой-то момент решите, что создали в объекте поля, которые больше там не нужны, - к вашим услугам оператор delete, удаляющий ненужное поле. Причем этому оператору можно передать как ссылку на само поле, так и его строковое имя. То есть в предыдущем примере мы могли написать и вот так: delete "a.g";. Такое поведение характерно для многих встроенных операторов и функций Флэш МХ. Но вот запись

 
name = "a.g";
delete name;
           

удалит, разумеется, только поле name того клипа, в кадре которого мы пишем код.

 

Все лежит в каком-то объекте

Запомните, что во Флэш МХ нет объектов, которые "висят в воздухе", то есть на которые нет ссылок в каком-то другом объекте. Они удаляются сборщиком мусора. Есть ряд системных объектов: это _global, в котором хранятся глобальные данные, и объекты с именами _level0, _level1 и т.д., которые представляют собой ссылки на текущие загруженные флэш-ролики (каждый из которых подгружен из отдельного *.swf-файла). Внутри кода каждого из роликов можно получить ссылку на корневой объект ролика с помощью записи _root. Еще есть системные объекты, соответствующие контекстам вызова функций (о них чуть позже). Если мы будем прослеживать цепочку "владения", то убедимся, что она непременно восходит к одному из вышеописанных системных объектов. То есть, если бы мы имели возможность удалить все эти объекты, то сборщик мусора удалил бы все остальные объекты текущей флэш-программы. Впрочем, объекты, начинающиеся на _level действительно можно выгружать, но это отдельный разговор.

 

Касается ли сказанное выше примитивных типов? Да, ведь они тоже объекты, только read-only. А локальных переменных? Снова ответ "да". Локальные переменные хранятся в так называемом объекте активации функции (activation object), или, как мы обычно будем про него говорить, контексте вызова функции. Далее мы научимся даже получать прямую ссылку на этот объект.

 

Возникает естественный вопрос: а где лежали переменные из примеров, приведенных выше? Ведь мы создавали их, определяя Action для какого-то кадра линейки времени? Так вот, линейка времени всегда принадлежит какому-либо клипу. Именно в соответствующем ему объекте (типа MovieClip) и лежат переменные, созданные в Actions. В нашем случае мы вовсе не создавали никакого нового клипа. Так что все переменные попали в корневой клип. Он называется _root. Из любого клипа мы могли бы обратиться к ним, например, так: _root.a.

 

Все есть ссылка

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

 

Проверка на примитивность

Как проверить, примитивными или объектными являются данные, на которые указывает ссылка? Есть два способа. Первый - проверить, сможем ли мы завести у тестируемого объекта новые поля. Если не сможем, то он read-only, то есть примитивный. Второй способ - это использовать оператор instanceof, который обычно применяется для проверки принадлежности к тому или иному классу.

 
a = new String("Объектная строка");
trace("a = " + a);
a.x = 4; // Объектные типы могут наращиваться
trace("a.x = " + (a.x == undefined ? "undefined" : a.x));
   // А это более простой способ проверки
trace("(a instanceof String) = " + (a instanceof String));
trace("");
b = "Примитивная строка";
trace("b = " + b);
b.x = 4; // Примитивные - наращиваться не могут,
      // они read-only. Так что это не сработает.
trace("b.x = " + (b.x == undefined ? "undefined" : b.x));
   // Используем простой способ проверки
trace("(b instanceof String) = " + (b instanceof String));
           

Выводит такой код вот что:

 
a = Объектная строка
a.x = 4
(a instanceof String) = true

b = Примитивная строка
b.x = undefined
(b instanceof String) = false
           

Итак, оба способа демонстрируют разницу между объектными и примитивными типами. Несмотря на эту разницу, методы объектной и примитивной строки полностью совпадают. Это неспроста: на самом деле примитивные строки - это действительно объекты типа String, только read-only, и то, что instanceof умудряется заметить разницу - само по себе неожиданно.

 

Заметим также, что в приведенном выше примере мы могли проверять с помощью instanceof на принадлежность к типу Object: писать trace("(b instanceof Object) = " + (b instanceof Object));. В этом случае мы могли бы сразу проверить на примитивность не только строку, а вообще какой угодно тип. Скажем, такая проверка сообщит нам, что функции являются объектами. Правда, мы еще ни слова не сказали о том, как во Флэше определять свои функции. К этому мы сейчас и перейдем.

 

Функции и методы

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

 

Пример глобальной функции

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

 
// Определяем глобальную функцию
_global.printSomeStringWithFrame = function(str){
   trace("***************************");
   trace(str);
   trace("***************************");
}
   // Проверяем, как она работает
printSomeStringWithFrame("Строка в рамке");
           

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

 
***************************
Строка в рамке
***************************
           

то есть функция сработала. Еще раз отметим, что функция, определенная таким образом (как принадлежащая объекту _global) будет видна в любом объекте или клипе без явного указания _global.

 

Пример метода

В качестве примера реализации метода мы переопределим метод toString() в созданном нами объекте (а заодно создадим и новый метод).

 
obj = {a: 10, b: "Str"};
   // Помещает поля объекта в строчку и возвращает ее
obj.placeFieldsToStr = function(){
   var str;
   str += "this.a = " + this.a + "\n";
   str += "this.b = " + this.b;
   return str;
}
   // Вызывает placeFieldsToStr и окружает звездочками
   // то, что она возвращает.
obj.toString = function(){
   var str = "***************************\n";
   str += this.placeFieldsToStr();
   return str + "\n***************************";
}
   // trace вызывает у объекта toString()
   // и выводит результат. Проверяем,
   // сработало ли переопределение.
trace(obj);
           

В результате выполнения этого кода мы увидим:

 
***************************
this.a = 10
this.b = Str
***************************
           

Итак, созданные нами методы работают. И становится очевидно, что создавать глобальные функции и методы объектов - это практически одно и то же. Теперь мы видим, что глобальная функция - это просто метод объекта _global. Кстати, кроме того синтаксиса создания функций, который мы показали сейчас, есть и другой, возможно, более привычный. Тем не менее, о нем мы сейчас говорить не будем, поскольку он менее универсален и результаты его менее очевидны. Этот синтаксис описан в самом начале пятой лекции.

 

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

 

Обязательно пишем this!

Вы, наверное, уже заметили, что в методах объекта obj, которые мы только что создали, встречается совершенно аномальное количество употреблений ключевого слово this. Точнее, аномальное для С++ или Java. А вот у Флэш МХ есть одна очень неприятная особенность: изнутри метода обратиться к полям и другим методам того же объекта можно, только употребив this явным образом. Если забыть поставить this - обращение будет к переменной того контекста, в котором создана функция. Что это за контекст - мы сейчас пояснять не будем (все пояснения вы найдете в пятой лекции), важно, что нужных нам полей и методов в этом контексте нет и в помине. Хотя Флэш и не подумает сообщать вам об ошибке, если вы забудете написать this. Если вы запрашиваете значение какой-то переменной, он вернет вам undefined. Еще хуже, если вы, напротив, присваиваете переменной какое-то значение: он заведет эту переменную там, где она совсем вам не нужна, а заметите вы это не сразу. Так что, если написанная вами функция не работает, в первую очередь проверяйте, не забыли ли вы написать this при обращении к полям и методам объекта. Не забывайте, что и к методам - тоже!

 

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

 

Встроенные функции

Большинство встроенных функций Флэш МХ являются методами объекта _global. Это означает, что вызывать их можно в любом месте (не указывая объект-хозяин). Важное исключение из этого правила - математические функции. Они представляют собой методы объекта Math, и этот объект нужно явным образом указывать при вызове функции. Такое не слишком удобное решение происходит из языка Java, где, однако, по-другому сделать было нельзя. А в JavaScript и, соответственно, во Флэш МХ, видимо, решили создать условия для удобного переноса туда Java-кода (по крайней мере, использующего стандартные математические функции). Все необходимые математические функции в этом объекте присутствуют, вплоть до удобнейшей функции atan2 (которая, как вы, наверное, помните, служит для облегчения перевода в полярные координаты). Заодно в объекте Math помещены и математические константы (пи, основание натуральных логарифмов, некоторые часто используемые корни и логарифмы). Ряд других важных функций Флэш МХ, которые по смыслу могли бы быть глобальными, тоже являются методами некоторых системных объектов. Но об этих функциях нам еще представится случай упомянуть в то время, когда про соответствующие объекты зайдет разговор.

 

Подробно о числовом типе Number

Без чего невозможна нормальная работа программиста - так это без четкого представления деталей работы числовых типов. Во Флэше числовой тип только один, это Number. Тем больше у нас оснований разобраться с ним как можно тщательнее.

 

Тип Number: точность

Итак, каково же внутреннее устройство типа Number во Флэше? В принципе, вы можете посмотреть в стандарт ECMAScript и сразу же обнаружить, что под типом Number скрывается. Однако мы поступим по-другому: мы сначала постараемся выяснить все что можно средствами самого Флэш МХ. А уж потом, когда все станет ясно, расставим последние точки. Почти наверняка те, на первый взгляд, хорошо знакомые вам вещи, которые мы обнаружим, засверкают новыми гранями.

 

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

 

Сейчас мы произведем ряд экспериментов, из которых выясним, что точность мантиссы - 53 бита. В 53 бита помещается 15 знаков мантиссы (14 знаков после запятой). Бывает, что число выводится с меньшим количеством знаков - но это лишь потому, что замыкающие нули не показываются для чисел в экспоненциальной записи. Так что на самом деле точность составляет 14 знаков после запятой всегда. Вообще-то мантисса с 14 знаками после запятой помещается (порой с хорошим запасом) не то что в 53, а даже и в 50 бит. И остается еще как минимум 3 дополнительных бита, которые позволяют увеличить точность расчетов и сделать последнюю цифру числа более надежной (при выводе числа мы все равно видим не более 14 цифр после запятой, так что "запасные" биты используются для округления). Итак, вот те примеры, которые демонстрируют сказанное выше.

 
 // Сколько знаков дают 50 бит: все целые числа вплоть до 10^15
 // (все 15-значные числа) помещаются в 50 бит
 // (числа вплоть до 2^50 - 1)
trace("2^50 - 1 = " + (Math.pow(2, 50) - 1));

 // Сколько знаков дают 53 бита:
 // НЕ ВСЕ целые числа вплоть до 10^16 помещаются в 53 бита
 // (числа вплоть до 2^53 - 1), так что 53 битам
 // все равно соответствует 15 знаков (14 после запятой).
trace("2^53 - 1 = " + (Math.pow(2, 53) - 1) + "\n");

   // Демонстрируем, что "лишние биты" присутствуют
   // и повышают точность вычислений
for (i=0; i<=10; i++){
   x = Math.pow(2, 52) - 2;
   y = Math.pow(2, 52) - 1 - i;
   trace("x = " + x + "; y = " + y + "; x - y = " + (x - y));
}

   // Просто печатает пустую строку для лучшего
   // форматирования выходных данных
trace("");

 // Демонстрируем, что внутреннее представление мантиссы
 // действительно записывается в 53 бита (не считая знак).
for (i=0; i<=10; i++){
      // Число 2^(52+i) помещается в 53+i бит,
      // так что последние i бит отрезаются
   a = Math.pow(2, 52+i);
      // Прибавляем двоичное число 10101010101
      // (единицы чередуются с нулями, чтобы избежать
      // округления при обрезании последних битов
   b = a + parseInt("10101010101", 2);
      // Выводим разницу полученной суммой и а
      // в двоичном виде (аргумент функции Number.toString -
      // это основание системы счисления).
   trace("Bits: " + (53 + i) + "; b - a = " + (b - a).toString(2));
}

   // Проверяем, во скольких разрядах происходит преобразование
   // в двоичную систему
trace(
   "\n(Math.pow(2, 31)).toString(2) = " +
   (Math.pow(2, 31)).toString(2)
);
trace(
   "(Math.pow(2, 31) + 1).toString(2) = " +
   (Math.pow(2, 31) + 1).toString(2)
);
   // И в 36-ричную
trace(
   "(Math.pow(2, 31)).toString(36) = " +
   (Math.pow(2, 31)).toString(36)
);
trace(
   "(Math.pow(2, 31) + 1).toString(36) = " +
   (Math.pow(2, 31) + 1).toString(36)
);
           

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

 
2^50 - 1 = 1.12589990684262e+015
2^53 - 1 = 9.00719925474099e+015

x = 4.50359962737049e+15; y = 4.5035996273705e+15; x - y = -1
x = 4.50359962737049e+15; y = 4.50359962737049e+15; x - y = 0
x = 4.50359962737049e+15; y = 4.50359962737049e+15; x - y = 1
x = 4.50359962737049e+15; y = 4.50359962737049e+15; x - y = 2
x = 4.50359962737049e+15; y = 4.50359962737049e+15; x - y = 3
x = 4.50359962737049e+15; y = 4.50359962737049e+15; x - y = 4
x = 4.50359962737049e+15; y = 4.50359962737049e+15; x - y = 5
x = 4.50359962737049e+15; y = 4.50359962737049e+15; x - y = 6
x = 4.50359962737049e+15; y = 4.50359962737049e+15; x - y = 7
x = 4.50359962737049e+15; y = 4.50359962737049e+15; x - y = 8
x = 4.50359962737049e+15; y = 4.50359962737048e+15; x - y = 9

Bits: 53; b - a = 10101010101
Bits: 54; b - a = 10101010100
Bits: 55; b - a = 10101010100
Bits: 56; b - a = 10101011000
Bits: 57; b - a = 10101010000
Bits: 58; b - a = 10101100000
Bits: 59; b - a = 10101000000
Bits: 60; b - a = 10110000000
Bits: 61; b - a = 10100000000
Bits: 62; b - a = 11000000000
Bits: 63; b - a = 10000000000

(Math.pow(2, 31)).toString(2) = -
(Math.pow(2, 31) + 1).toString(2) = -
111111111111111111111111111111
(Math.pow(2, 31)).toString(36) = -
(Math.pow(2, 31) + 1).toString(36) = -zik0zj
           

Давайте подробно разберем, что же означает весь этот вывод нашей программки. Две первых строки показывают, что 15 знаков помещаются в 50 бит, но 16 не поместятся и в 53. Затем идут строчки (вычисление значения выражения х - у для разных у), которые демонстрируют, что дополнительные биты действительно есть. Во всех строчках этой серии (кроме первой и последней) выводимые значения х и у одинаковы. Но разность их отнюдь не нулевая; нужные нам биты мы не потеряли. Заметьте, что здесь есть и некоторый подвох: не всегда два числа, которые выглядят одинаково при выводе на печать, являются равными (ненулевая разность означает, что x == y дает false). Впрочем, это обычное дело при сравнении чисел с плавающей точкой; достаточно лишь соблюдать осторожность и писать вместо х == у что-то вроде Math.abs(x - y) < (Math.abs(x)+Math.abs(y))*1e-13. Если же ваши данные являются целыми, можете смело их сравнивать с помощью оператора "==".

 

Далее следует около десятка строчек, которые иллюстрируют разрядность внутреннего представления мантиссы (без учета знака). Если в двоичном представлении числа имеется 53 знака, то младшие биты не обрезаются. Легко видеть, что для 54-битного числа обрезается самый младший бит, для 55-значного - два бита и т.д. Наконец, четыре последние строки возникли, в частности, из попытки получить информацию о содержимом "запасных" битов напрямую. Вдруг вывод числа в двоичной системе (или другой системе счисления) поможет и эти биты станут видны? Оказалось, однако, что при преобразовании в другую систему счисления используется 32-битный регистр, и не то что о 53, но и о 50 битах не может быть и речи. Причем это обыкновенный дополнительный код, то есть 32-й бит кодирует знак. (Именно поэтому такой странный результат получается при попытке вывести 231 - воспринимаемое как дополнительный код, такое число не имеет смысла). То же касается и 36-разрядной системы (как и любой другой, кроме десятичной) - 231 + 1 воспринимается как отрицательное число. Почему для примера мы взяли именно 36-ричную систему? Потому что это система счисления с наибольшим основанием - из тех, в которые функция toString способна переводить числа. Почему именно 36? А просто это 10 (число цифр) плюс 26 (число букв латинского алфавита). Для еще больших оснований знаков просто не хватает. И если вы попытаетесь ввести большее число в качестве аргумента toString, такой аргумент будет попросту проигнорирован (и вы получите десятичную запись). Наконец, укажем еще на то, что при переводе в другую систему счисления само число и основание системы приводятся к целым (причем не округлением, а отбрасыванием дробной части).

 

Граничные значения и размеры в памяти

Итак, разрядность мантиссы мы определили. Неплохо было бы прикинуть теперь разрядность порядка. Чтобы это сделать, нам необходимо знать максимальное и минимальное по модулю значения, которые можно записать в типе Number.

 

Эти значения легко получить, поскольку они хранятся в специальных константах Number.MAX_VALUE и Number.MIN_VALUE. Так вот, Number.MAX_VALUE равно 1.79769313486231e+308, а Number.MIN_VALUE равно 4.94065645841247e-324. Почему минимальное значение имеет порядок больший по абсолютной величине (минус 324, а не минус 308)? Дело в том, что мы можем, сохраняя порядок, уменьшать мантиссу до весьма малых значений (гораздо меньше 1). В обычных случаях это не практикуется, но очень малые по абсолютной величине числа позволяет записывать.

 

Давайте подсчитаем, сколько битов нужно для хранения порядка числа. Для этого надо в первую очередь вспомнить, что компьютеру, разумеется, удобнее оперировать числом с плавающей точкой в форме , а вовсе не (здесь m - мантисса, а n - порядок). 10308 - это примерно 21023 (ведь 308*log210 = 1023.15). Используя 11 битов, мы сможем записать положительные и отрицательные значения величиной от 0 до 1023. Итак, на хранение порядка надо 11 бит, а мантисса занимает 53 бита, как мы уже выяснили. Еще один бит нужен на знак числа. Всего получается 65; еще бы чуть-чуть сэкономить - и выйдет удобные 64 бита. И действительно, сэкономить есть на чем. Поскольку число у нас представляется в виде , то мантисса может быть не от 1.0 до 10.0 (точнее, строго меньше 10.0), как в обычном десятичном представлении, а от 1.0 до 2.0 (строго меньше 2.0). В результате первый бит мантиссы - всегда единица, так что его можно не хранить. Итого на внутреннее представление Number должно отводиться 64 бита (о чем вы, наверняка, догадались с самого начала; но надо ведь было поддержать видимость сюжетной интриги).

 

А теперь, когда интрига закончилась, пора срывать покровы. Конечно, внутри Number на самом деле скрывается тип double. Самый настоящий, соответствующий стандарту IEEE 754. Впрочем, было бы странно, если бы разработчики Флэш пренебрегли такой удобной и стандартной вещью. И, оказывается, хранить в double целые числа тоже весьма удобно. Особенно удобно то, что часто после ряда арифметических операций, имевших в качестве промежуточных результатов нецелые числа, в окончательном ответе мы можем получить целое число, причем абсолютно точно. Лучше всего срабатывает округление после операций умножения или деления. Вот примеры

 
a = 1/3;
trace("a = 1/3, то есть " + a);
trace("(a*3 == 1) = " + (a*3 == 1))
trace("(a*3 - 1)*Math.pow(2, 52) = " + (a*3 - 1)*Math.pow(2, 52));
trace("(a - 0.333333333333333)*Math.pow(2, 52) = "
   + (a - 0.333333333333333)*Math.pow(2, 52));
trace("(a - 0.3333333333333333)*Math.pow(2, 52) = "
   + (a - 0.3333333333333333)*Math.pow(2, 52));
trace("");
   // Пробуем разделить 1 на другое число
b = 1/Math.pow(7, 45);
trace("b = 1/Math.pow(7, 45), то есть " + b);
trace("(b*Math.pow(7, 45) == 1) = " + (b*Math.pow(7, 45) == 1))
           

На выходе имеем:

 
a = 1/3, то есть 0.333333333333333
(a*3 == 1) = true
(a*3 - 1)*Math.pow(2, 52) = 0
(a - 0.333333333333333)*Math.pow(2, 52) = 1.5
(a - 0.3333333333333333)*Math.pow(2, 52) = 0
b = 1/Math.pow(7, 45), то есть 9.34519137233795e-39
(b*Math.pow(7, 45) == 1) = true
           

А вот после других операций может не так повезти. Попробуем, скажем, вычислить (101/3)3. Должно получиться опять 10. А на деле получаем 9.99999999999999. А может быть еще хуже: (101/5)5 на печати выдает 10, но при попытке сравнить полученное число с десяткой, мы получаем false. Посмотрите сами: код

 
trace(Math.pow(Math.pow(10, 1/3), 3));
trace(Math.pow(Math.pow(10, 1/5), 5));
trace(Math.pow(Math.pow(10, 1/5), 5) == 10);
           

дает на выходе

 
9.99999999999999
10
false
           

Так что при сложных вычислениях с плавающей точкой никто не избавит нас от необходимости сравнивать числа приблизительно так: Math.abs(x - y) < (Math.abs(x)+ Math.abs(y))*1e-13. Мы уже говорили об этом чуть раньше, но привычка использовать такой прием при сравнении чисел с плавающей точкой настолько важна, что повториться не помешает.

 

Нечисловые значения типа Number

Мы посвятили довольно много времени разбору представления чисел в типе Number. Однако в этом типе могут храниться не только числовые, а точнее - не совсем числовые значения. Всего этих специальных значений имеется три. Два из них служат для обозначения бесконечности. Причем бесконечность бывает разная: положительная и отрицательная. В этом есть некоторый смысл, поскольку положительная бесконечность всего лишь обозначает любое число, большее, чем Number.MAX_VALUE. А отрицательная - число, меньшее чем (-Number.MAX_VALUE). Хотя при делении на ноль есть некоторый произвол - какую именно из бесконечностей употребить. А вот при попытке вычислить логарифм нуля мы всегда получим именно минус бесконечность, что естественно. Так вот, положительной бесконечности соответствует константа Number.POSITIVE_INFINITY. А отрицательной, соответственно - константа Number.NEGATIVE_INFINITY. При преобразовании в строку эти константы обращаются в Infinity и -Infinity. Вот парочка примеров с использованием этих значений:

 
trace("5/0 = " + 5/0);
trace("-5/0 = " + -5/0);
trace("-5/-0 = " + -5/-0);
trace("-5/Number.MIN_VALUE = " + -5/Number.MIN_VALUE);
trace("-5/-Number.MIN_VALUE = " + -5/-Number.MIN_VALUE);
trace("Math.log(0) = " + Math.log(0));
trace("Math.sqrt(-1) = " + Math.sqrt(-1));
           

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

 
5/0 = Infinity
-5/0 = -Infinity
-5/-0 = -Infinity
-5/Number.MIN_VALUE = -Infinity
-5/-Number.MIN_VALUE = Infinity
Math.log(0) = -Infinity
Math.sqrt(-1) = NaN
           

Видим, что попытка обнаружить разницу между 0 и (-0) провалилась - выражение (-0) тут же вычисляется и получает значение, равное 0. А вот если вместо 0 использовать Number.MIN_VALUE (наименьшее по абсолютной величине число, которое отличается от нуля) - тут уже разница между Number.MIN_VALUE и (- Number.MIN_VALUE) отчетливо видна.

 

Но вернемся к нечисловым значениям Number. Последнее из них (с которым, тем не менее, довольно часто приходится иметь дело) - это Number.NaN (при преобразовании в строку переводится в "NaN"). Название это является сокращением от "Not a Number" - "не число". Употребляется NaN для значений выражений, которые невозможно представить в виде действительного (хотя бы и бесконечного) числа. Пример мы только что видели - значением выражения Math.sqrt(-1) является Number.NaN. Кроме того, выражения c арифметическими операторами, подразумевающими преобразование к Number, в которые попали типы данных, не работающие с подобными операторами, также приобретают значение Number.NaN. Примером типов, приобретающих значение Number.NaN при преобразовании к Number, являются, например, String или Function. Заметьте, что в одном из предыдущих примеров в выражениях, наподобие trace("x = " + x + "; y = " + y + "; x - y = " + (x - y)) арифметические вычисления взяты в скобки. Если скобки убрать, значением выражения станет Number.NaN, поскольку вся начальная часть выражения преобразуется в строку, а затем эта строка будет приведена к типу Number при попытке вычесть из нее число y.

 

Кстати, писать именно Number.NaN необязательно. Можно употреблять и просто NaN. Также существует и предопределенная переменная Infinity (хотя редактор ActionScript среды Флэш МХ ее не узнает и не подсвечивает, но пользоваться ей можно и означает она то же самое, что и Number.POSITIVE_INFINITY). Можем ли мы проверить, что Number.NaN и NaN - это действительно одно и то же? Ведь оператор == не работает со значением NaN (всегда выдает false). Для подобной проверки есть функция isNaN. Сейчас мы продемонстрируем ее работу (и работу в чем-то родственной функции isFinite). Вот такой код

 
trace("NaN на печати выглядит как " + NaN);
trace("isNaN(NaN) = " + isNaN(NaN));
trace("Infinity на печати выглядит как " + Infinity);
trace("-Infinity на печати выглядит как " + (-Infinity));
trace("Number.NEGATIVE_INFINITY на печати дает "
    + Number.NEGATIVE_INFINITY);
trace("-Number.NEGATIVE_INFINITY на печати дает "
    + -Number.NEGATIVE_INFINITY);
trace("isFinite(Infinity) = " + isFinite(Infinity));
trace("1/Infinity = " + 1/Infinity);
           

выводит

 
NaN на печати выглядит как NaN
isNaN(NaN) = true
Infinity на печати выглядит как Infinity
-Infinity на печати выглядит как -Infinity
Number.NEGATIVE_INFINITY на печати дает -Infinity
-Number.NEGATIVE_INFINITY на печати дает Infinity
isFinite(Infinity) = false
1/Infinity = 0
           

Конвертация типов

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

 

При сложении строчка старше всех

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

 
trace((5 > 7) + 10 + " Строка ");
trace(10 + " Строка " + (5 > 7));
trace(10 - " Строка " - (5 > 7));
           

Запустив его на выполнение, мы увидим в консоли вот что:

 
10 Строка
10 Строка false
NaN
           

В первых двух операторах trace операнды, к которым прибавлялась строка, преобразовались в строки. Только в первом случае значение выражения (5 > 7) + 10 было вычислено сначала. При этом тоже было произведено преобразование типов: если в выражении встречаются Boolean и Number, и семантика выражения при этом подразумевает арифметические операции, то осуществляется преобразование к Number. При этом true преобразовывается в 1, а false - в 0. (Если же выполняются логические операции, то, напротив, Number преобразуется к Boolean: любое ненулевое число означает true, а ноль - false).

 

Таким образом, в первом вызове trace в нашем примере сначала было вычислено значение логического выражения, получилось false, что преобразовалось в ноль и в сумме с десяткой дало опять-таки десять. А уж потом эта десятка была состыкована со строкой. Во втором же вызове trace логическое выражение прибавлялось к строке, поэтому мы и получили там false в явном виде.

 

Наконец, в третьем вызове trace выражение содержало операцию "минус", что не подразумевает преобразования к строке. Поэтому пришлось преобразовывать строку к числу, что дало в результате NaN.

 

Явное преобразование типов

Иногда бывает нужно произвести явное преобразование типа. Для этого существуют четыре глобальных функции: Number(), String(), Boolean() и Object(). (Между прочим, это не просто функции, это также и конструкторы соответствующих типов. Но пока что мы их будем употреблять в качестве обыкновенных функций.) В качестве аргумента в эти функции подставляется преобразуемый объект, возвращается (примитивный) объект нужного типа. Ни к каким более типам (в том числе пользовательским) преобразование невозможно. Впрочем, поскольку проверка типа во Флэше не производится, преобразование к пользовательским типам совершенно не нужно. Все необходимые вещи могут быть сделаны без преобразования. Наконец, семантика Флэш МХ такова, что говорить о пользовательских типах вообще не очень корректно (хотя классы пользователь может создать какие угодно).

 

Но не будем отвлекаться и рассмотрим подробнее явные преобразования. Преобразование к типу String совсем простое - оно сводится к вызову метода toString у нужного объекта. Преобразование к Boolean также вполне тривиально для всех типов, не являющихся строчкой. А именно, все, что не есть 0, Number.NaN, null или undefined - это true. А вот строчки преобразуются к Boolean транзитом через Number, то есть Boolean(str) - это то же самое, что Boolean(Number(str)). И теперь мы естественным образом переходим к самому интересному - к тому, как осуществляются преобразования из String в Number. (С преобразованиями в Number остальных типов все понятно: Boolean преобразуется в 0 или 1 соответственно, а прочие типы в Number.NaN). Итак, чтобы описать преобразование из String в Number, удобно будет сначала рассмотреть более подробно работу глобальных функций parseInt и parseFloat. Несмотря на свои названия, обе эти функции способны извлекать из строки как целые числа, так и дробные. Но при этом числа в экспоненциальной записи правильно прочесть можно только при использовании parseFloat. Зато parseInt корректно обрабатывает 16-ричную запись с префиксом , восьмеричную с префиксом 0, а также запись в любой системе счисления от 2 до 36, если основание системы счисления явно указано во втором аргументе этой функции. Надо заметить, что каждая из этих функций пытается воспринять максимальное количество символов строки (начиная слева) как число. Это число она выдает в качестве результата своей работы, а все "лишние" символы в правой части строки игнорируются. И только когда в качестве числа не удается воспринять ни одного символа в строке (начиная слева), выдается Number.NaN. Так вот, функция Number при работе с аргументом строкового типа представляет собой своего рода гибрид этих двух функций - с той, однако, существенной разницей, что Number.NaN выдается, если в преобразуемой строке остался хотя бы один лишний символ (даже если это пробел). То есть функция Number пробует интерпретировать строку как число всеми возможными способами - и как целое в десятичной, восьмеричной или шестнадцатеричной системе счисления, и как число в записи с фиксированной или плавающей точкой - и лишь потерпев неудачу во всех попытках, выдает Number.NaN.

 
   // Создаем примитивную строку и проверяем ее на примитивность
a = "Примитивная строка";
   // Выводим значение строки
trace("a = " + a);
   // Пытаемся создать новое поле
a.r = 5;
   // Создать новое поле не удается - строка read-only
trace("a.r = " + a.r);
   // Проверяем на примитивность еще одним способом
trace("(a instanceof String) = " + (a instanceof String) + "\n");
   // Теперь конвертируем этот объект и проделываем
   // все те же операции еще раз
a = Object(a);
trace("a = " + a);
a.r = 5;
trace("a.r = " + a.r);
trace("(a instanceof String) = " + (a instanceof String));
           

На выходе имеем:

 
a = Примитивная строка
a.r =
(a instanceof String) = false
a = Примитивная строка
a.r = 5
(a instanceof String) = true
           

Эти результаты наилучшим образом подкрепляют наше утверждение о том, что функция Object() просто делает из read-only-объекта объект обыкновенный. (Конечно, с исходным read-only-объектом ничего не происходит. Создается новый объект, уже не примитивный. Фактически, вместо Object(a) мы в нашем примере могли бы писать и new Object(a) с абсолютно тем же результатом.)

 

Специальные типы

Во Флэш МХ есть два специальных типа, для которых значение объекта однозначно задается его типом.

 

Тип null

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

 

Тип undefined

undefined - это еще один специальный тип. Такой тип имеет выражение, обращающееся к переменной (объекту, функции), значение которой не было заранее задано. Впрочем, если undefined встречается в выражении, в котором оно должно быть преобразовано в строку - оно превращается в пустую строку. При преобразовании в число получается 0, в булевский тип - false. Если переменная была заведена, а потом была удалена с помощью delete, то справившись о значении этой переменной мы снова получим undefined. Запомните, что при сравнении null и undefined операторами == или != мы не сможем обнаружить разницы между двумя этими значениями. Но то, что эти значения разных типов, помогает нам решить проблему при помощи использования операторов "строгого равенства - неравенства" === и !==, специфичных для Флэш МХ. Об этих операторах мы еще будем говорить далее в параграфе, посвященном выражениям.

 

Список встроенных типов

Применение оператора typeof

В некотором смысле понятие "встроенные типы Флэш МХ" не является хорошо определенным. Скажем, встроенный или не встроенный тип - компонент "выпадающий список" из библиотеки компонентов? Мы все же ограничимся наиболее четким из возможных определений, а именно: встроенными типами будем называть только те, для которых специальный оператор typeof выдает различные значения. Синтаксис использования оператора typeof следующий:

 
typeof <выражение>
           

Если вам больше нравится рассматривать typeof как функцию, то можно ставить скобки и писать

 
typeof (<выражение>)
           

Оператор этот выдает строчку, которая содержит название типа значения, возвращаемого переданным typeof выражением. Строчка при этом приводится к нижнему регистру, как показано в таблице 2.1. В таблице также указано, как воспринимаются объекты, которые пользователь может создавать "руками" - то есть клипы, кнопки и текстовые поля.

 

Видно, что из создаваемых визуально объектов только клипы оператор typeof "отличает особо". Что же касается собственно языка ActionScript, то для него разным (не сводимым к уникальному набору методов) поведением обладают лишь объекты типов Number, Boolean, String, Object, Function, undefined и null. Да еще Array имеет специальные операторы для конструирования. И только типы Number, Boolean, String, undefined и null бывают примитивными ("только для чтения").

 

Значения, возвращаемые typeof

Таблица 2.1. Приведение типа значения typeof
Тип typeof возвращает
Number number
Boolean boolean
String string
Object object
Function function
Array array
MovieClip movieclip
Button object
Text field object
undefined undefined
null null
 

Давайте теперь пронаблюдаем работу этого оператора на примерах. Пишем следующий код:

 
trace('typeof(5 + "") = ' + typeof(5 + ""));
trace('typeof(a) = ' + typeof(a));
a = new Object();
trace('typeof(a) = ' + typeof(a));
a = null;
trace('typeof(a) = ' + typeof(a));
a = {x: 20, y: "строчка"};
trace('typeof(a) = ' + typeof(a));
trace('typeof(a.x) = ' + typeof(a.x));
trace("typeof(String) = " + typeof(String));
trace("typeof(Function) = " + typeof(Function));
// Запись typeof(function) выдаст синтаксическую ошибку
           

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

 
typeof(5 + "") = string
typeof(a) = undefined
typeof(a) = object
typeof(a) = null
typeof(a) = object
typeof(a.x) = number
typeof(String) = function
typeof(Function) = function
           

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

 

Function - редкий случай case-sensitivity для идентификатора

И, раз уж мы заговорили о классе Function, самое время обратить внимание на интересный феномен. Вообще-то во Флэше всякое имя функции - это обычный идентификатор. Более того, мы вполне можем считать имя функции именем переменной, ссылающейся на объект-функцию (подробнее об этом будет рассказано в пятой лекции). А идентификаторы Флэш (в отличие от ключевых слов) являются нечувствительными к регистру (case-insensitive). То есть, хотя ключевые слова, как в C++ или Java, к регистру чувствительны (case-sensitive), одни и те же идентификаторы можно писать как заглавными буквами, так и строчными (или же вперемешку). Мы, конечно, надеемся, что в своих программах вы не допустите беспорядка и не будете пользоваться этим свойством Флэш. И тем более не будете заводить идентификаторы вроде NEW (хотя это и разрешается). Забавно, что сами создатели Флэш небольшой беспорядок все-таки допустили: существует идентификатор Function (имя конструктора объектов-функций), и в то же время существует ключевое слово function, которое как раз и позволяет новые функции создавать. Использовать это ключевое слово в том месте, где по смыслу требуется идентификатор Function, вы, разумеется, не сможете (об этом и говорит последняя строчка-комментарий в вышеприведенном коде).

 

Подсказки по объектам встроенных типов

Отсутствие строгой типизации во Флэш МХ порождает одну специфическую трудность. Современные программные среды очень дружественно относятся к пользователю, в частности, предоставляя ему возможность автопродолжения набранного текста. Самый распространенный (и, наверное, самый полезный) вариант этой возможности выглядит так: вы набираете имя переменной, среда опознает тип этой переменной, и когда вы вслед за именем ставите точку, вам тут же выдается список полей и методов этого типа. Остается лишь выбрать нужный. Это избавляет от необходимости помнить детали написания множества методов, и вообще очень удобно. Но как организовать аналогичную подсказку в том случае, когда строго определенного типа переменная не имеет и может хранить ссылку на какой угодно объект? В этом случае программист должен сам помочь среде Флэш МХ обеспечить ему необходимые удобства. А именно, если программист заранее знает, что в некоторой переменной будет храниться ссылка на объект какого-то определенного типа (так и бывает в большинстве случаев), он может дать этой переменной имя с одним из заранее определенных суффиксов. Такие переменные среда Флэш МХ будет узнавать, и выдаст вам подсказку. Все такие суффиксы начинаются с символа подчеркивания, так что и вам и среде будет легко их узнавать. Например, имя переменной a_str сообщает среде Флэш МХ о том, что в этой переменной, скорее всего, содержится ссылка на строку и подсказку по ней следует выдавать, соответствующую типу String. Вот список предопределенных суффиксов и соответствующих им типов приведен в таблице 2.2.

 
Таблица 2.2. Предопределенные суффиксы и соответствующие им типы
Тип Суффикс Примечание
Обыкновенные объекты
Array _array  
Button _btn Стандартная кнопка (не компонент)
Color _color  
Date _date  
Sound _sound Именно объект типа Sound, а не просто звук, импортированный в клип
String _str  
TextField _txt  
TextFormat _fmt  
XML _xml  
XMLSocket _xmlsocket  
Компоненты
FCheckBox _ch  
FComboBox _cb Компонент "Выпадающий список"
FListBox _lb  
FPushButton _pb Кнопка-компонент
FRadioButton _rb  
FScrollBar _sb "Область с прокруткой" и (многострочные) текстовые поля.
FScrollPane _sp Компонент "Область с прокруткой"
 

Кроме этих суффиксов, Флэш МХ реагирует еще на несколько предопределенных имен, про которые ему заранее известно, что это объекты типа MovieClip. А именно, на _root (корневой клип данного флэш-ролика), _parent (в объектах типа MovieClip так называется поле, ссылающееся на родительский клип) и, наконец, на _level0, _level1, _level2 и т.д. (корневые клипы различных флэш-роликов, загруженных в плеер в настоящий момент). В лекции 12 мы расскажем вам о том, как самим делать подобные шаблоны, на которые реагирует среда разработки Флэш МХ.

 

Выражения

Флэш МХ имеет набор операторов, слегка расширенный по сравнению с Java. Сначала мы опишем (а точнее, кратко перечислим) ту часть операторов, которая является общей для Флэш МХ, Java и С++. Затем подробнее рассмотрим операторы, которых в С++ нет но есть в Java (для тех читателей, которые не работали на Java). И, наконец, подробно расскажем про операторы, которые вы кроме ActionScriptJavaScript) нигде не найдете.

 

Стандартные операторы

Во Флэше имеются практически все операторы из С++ и все операторы из Java. (Из С++ не вошли только операторы для работы с указателями - за неимением в языке самих указателей). По поводу работы некоторых из этих операторов нужно сделать ряд комментариев.

 

В первую очередь рассмотрим ситуацию с логическими операторами && и ||, а также с операторами & и |. В языке Java последние два оператора применяются не только как побитовые операторы, но и (при работе с булевскими операндами) как операторы с обязательным вычислением обоих операндов. Напомним, что для стандартных логических операторов && и || характерно следующее поведение. Если первый операнд позволяет определить значение выражения без использования второго (то есть, если это false в случае && и true в случае ||), то значение второго операнда не вычисляется вовсе. Как правило, такое поведение весьма удобно (и позволяет, например, в первом операнде сделать проверку на null, а во втором - обратиться к методу только что проверенного объекта). Если же в процессе проверки вы производите какие-то дополнительные действия и хотите, чтобы они производились каждый раз, независимо от значения первого операнда, вы можете воспользоваться операторами & или |. Так вот, несмотря на то, что подобное использование не является официально рекомендованным во Флэш МХ, вы и во Флэше можете применять эти операторы с такой целью. Главное - привести выражения к типу Boolean перед вычислением. Вычисления, конечно, все равно происходят побитовые, но после преобразования к Boolean возможные значения операндов - это только 0 и 1, так что разницы никакой нет. Тот же самый прием - приведения к boolean или bool соответственно - с успехом может применяться в Java и в C++. Только во Флэше (и в С++) перед вычислением значения выражения происходит неявное преобразование из булевского типа обратно в целый. И результат тоже получается целочисленный. Итак, посмотрите на примеры применения всех упомянутых операторов:

 
a = function(){
   trace("function a called");
   return true;
};
b = function(){
   trace("function b called");
   return false;
};
trace("a() && b() = " + (a() && b()));
trace("----------");
trace("b() && a() = " + (b() && a()));
trace("----------");
trace("a() || b() = " + (a() || b()));
trace("----------");
trace("b() || a() = " + (b() || a()));
trace("\n========================\n");

trace("a() & b() = " + (a() & b()));
trace("----------");
trace("b() & a() = " + (b() & a()));
trace("----------");
trace("a() | b() = " + (a() | b()));
trace("----------");
trace("b() | a() = " + (b() | a()));
           

Результат выполнения сего кода таков:

 
function a called
function b called
a() && b() = false
----------
function b called
b() && a() = false
----------
function a called
a() || b() = true
----------
function b called
function a called
b() || a() = true
========================
function a called
function b called
a() & b() = 0
----------
function b called
function a called
b() & a() = 0
----------
function a called
function b called
a() | b() = 1
----------
function b called
function a called
b() | a() = 1
           

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

 

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

 
a = function(){
   trace("function a called");
   return 1;
};
b = function(){
   trace("function b called");
   return -2;
};
           

мы получим:

 
function a called
function b called
a() && b() = -2
----------
function b called
function a called
b() && a() = 1
----------
function a called
a() || b() = 1
----------
function b called
b() || a() = -2
========================
function a called
function b called
a() & b() = 0
----------
function b called
function a called
b() & a() = 0
----------
function a called
function b called
a() | b() = -1
----------
function b called
function a called
b() | a() = -1
           

Обратите внимание, что в серии тестов с операторами & мы получили неправильные ответы - это из-за того, что мы пренебрегли приведением к Boolean. А вот операнды операторов | очень редко нуждаются в таком приведении: если один или оба - ненулевые, то и результат будет ненулевым. Неожиданности будут подстерегать нас лишь когда мы столкнемся с объектом, который не приводится к числу "естественным" образом и не является при этом строкой. То есть с объектом, имеющим тип Object, Function или другой подобный. При неявном приведении к типу Number для осуществления побитовых операций, из такого объекта получится Number.NaN. А это значение как для булевских, так и для побитовых операций равносильно 0. При непосредственном же приведении к булевскому типу - в случае использования оператора ||, а не | - объекты типа Object, Function, Array и т.д. преобразуются в true.

 

Еще одно интересное наблюдение мы можем сделать, если посмотрим на результаты работы логических операторов. В отличие от прошлого примера, когда мы получали true или false (потому что булевские значения возвращали функции a и b), в этот раз мы имеем целые числа. То есть "на выходе" логического оператора никакого преобразования к Boolean не происходит. Таким образом, можно представить себе работу оператора && в выражении a() && b() как (temp = a()) ? b() : temp. В самом деле, в случае, когда a() дает истину, результат будет истинным, лишь если истинно b() - его и возвращаем. Если же а() есть ложь, то результат заведомо ложен, так что в качестве него можно а() и вернуть. Аналогичным образом работу оператора || в выражении a() || b() можно эмулировать вот так: (temp = a()) ? temp : b() . То есть если а() - истина, то и результат всего выражения - истина, так что смело возвращаем а(). А если же а() - ложь, тогда возвращаем b(), поскольку в этом случае истину мы получим только если истинно b(). Еще раз отметим, что хотя сейчас мы рассуждения проводили в булевских терминах, поведение реальных операторов и эмуляции совпадает во всех случаях. Что мы сейчас продемонстрируем на следующем примере.

 
a = function(){
   trace("function a called");
   return "Some string";
};
b = function(){
   trace("function b called");
   return -2;
};
trace("((temp = a()) ? b() : temp) =
   " + ((temp = a()) ? b() : temp));
trace("----------");
trace("((temp = b()) ? a() : temp) =
   " + ((temp = b()) ? a() : temp));
trace("----------");
trace("((temp = a()) ? temp : b()) =
   " + ((temp = a()) ? temp : b()));
trace("----------");
trace("( (temp = b()) ? temp : a() ) = " + ((temp = b()) ?
temp : a()));
trace("\n========================\n");
trace("a() && b() = " + (a() && b()));
trace("----------");
trace("b() && a() = " + (b() && a()));
trace("----------");
trace("a() || b() = " + (a() || b()));
trace("----------");
trace("b() || a() = " + (b() || a()));
           

И получаем:

 
function a called
( (temp = a()) ? b() : temp ) = Some string
----------
function b called
function a called
( (temp = b()) ? a() : temp ) = Some string
----------
function a called
function b called
( (temp = a()) ? temp : b() ) = -2
----------
function b called
( (temp = b()) ? temp : a() ) = -2
========================
function a called
a() && b() = Some string
----------
function b called
function a called
b() && a() = Some string
----------
function a called
function b called
a() || b() = -2
----------
function b called
b() || a() = -2
           

То есть наше представление операторов && и || с помощью оператора ?: оказалось правильным. А это значит, что в некоторых случаях удобнее применять операторы && и || вместо ?: - сокращается запись и не нужно сохранять результат вычисления выражения, используемого дважды, во временную переменную. Например, мы запрашиваем нужный нам набор параметров функцией getParamArray(), но если этот массив не задан (undefined или null), используем набор параметров по умолчанию. Стандартный код выглядит так:

 
getParamArray() ? getParamArray() : getDefaultParamArray()
           

что длинно и заставляет вызывать getParamArray() дважды (или надо будет использовать временную переменную). Альтернативный вариант такой: getParamArray() || getDefaultParamArray(). Согласитесь, что писать так гораздо удобнее. Только, если вы действительно соберетесь использовать этот прием, не забудьте снабдить такой код комментариями. Все-таки подобная запись совершенно не является общепринятой.

 

Скажем еще несколько слов по поводу побитовых операторов. Они фактически работают с двоичным представлением числа, а мы уже знаем, что работа с недесятичными системами счисления проводится в 32 битах. Если вы захотите проделать побитовую операцию с числом, которое больше или равно 232, то будут взяты только 32 младших бита этого числа. Последовательность действий Флэш МХ в этом случае можно описать примерно так. Сначала операнд представляется в форме действительного числа с фиксированной точкой. Затем отбрасывается дробная часть. Затем вычисляется остаток от деления этого числа на 232. С этим остатком и производятся все побитовые операции. Вот пример, иллюстрирующий все это:

 
trace((1e12 + 0.6) + " = 1e12 + 0.6");
trace((1e12 + 0.6).toString(2) + " = (1e12 + 0.6).toString(2)");
trace(1e12.toString(2) + " = 1e12.toString(2)" + "\n");
trace(1e12.toString(2) + " в десятичном виде = " +
   parseInt(1e12.toString(2), 2));
trace("\n" + ((1e12 + 0.6) | 7) + " = (1e12 + 0.6) | 7");
trace(((1e12 % Math.pow(2,32)) | 7) + "
   = (1e12 % Math.pow(2,32)) | 7");
trace("\nВ двоичном виде: ");
trace(((1e12 + 0.6) | 7).toString(2) + "
   = ((1e12 + 0.6) | 7).toString(2)");
trace(((1e12 % Math.pow(2,32)) | 7).toString(2) + "
   = ((1e12 % Math.pow(2,32)) | 7).toString(2)");
           

что дает в результате

 
1000000000000.6 = 1e12 + 0.6
-101011010110101111000000000000 = (1e12 + 0.6).toString(2)
-101011010110101111000000000000 = 1e12.toString(2)

-101011010110101111000000000000 в десятичном виде = -727379968

-727379961 = (1e12 + 0.6) | 7
-727379961 = (1e12 % Math.pow(2,32)) | 7

В двоичном виде:
-101011010110101110111111111001 = ((1e12 + 0.6) | 7).toString(2)
-101011010110101110111111111001 = ((1e12 % Math.pow(2,32)) |
   7).toString(2)
           

Мы нарочно напечатали результаты вычислений слева, чтобы числа оказались одно под другим и равенство полученных разным способом чисел было очевидно. Легко заметить, что результаты побитовой операции существенно меньше, чем 1012, так что отбрасывание старших битов (равносильное делению по модулю на 232) действительно свершилось. А описанный нами алгоритм дал тот же результат, что и прямое выполнение операции "побитовое или". (Для наглядности мы представили результаты как в десятичном, так и в двоичном виде.)

 

Необычные операторы (специфика Java и Флэш МХ)

Сначала мы поговорим об одном операторе, который отсутствует в С++, но присутствует в Java и Флэш МХ. Это оператор беззнакового сдвига вправо >>>. Он позволяет сдвигать вправо все биты, включая знаковый; таким образом, использование этого оператора избавляет нас от побочных эффектов, связанных с переходом старших битов при сдвиге влево в знаковый разряд. Если использовать затем обычный сдвиг вправо, то знак сохраняется навечно (или, если разобраться, что происходит в дополнительном коде, то там знаковый бит сдвигается, как и положено, но на его старом месте каждый раз снова появляется единица). А вот в результате беззнакового сдвига ничего такого не происходит: единица из знакового бита спокойно "выезжает", оставляя там ноль. Вот пример, иллюстрирующий это.

 
trace((3 << 29).toString(2) + " = (3 << 29).toString(2)")
trace((3 << 30).toString(2) + " = (3 << 30).toString(2)")
trace((Math.pow(2, 31) + Math.pow(2, 30)).toString(2) +
   " = 2^31 + 2^30 в двоичном виде");
trace(((Math.pow(2, 31) + Math.pow(2, 30)) >> 5).toString(2) +
   " - результат >> 5");
trace(((Math.pow(2, 31) + Math.pow(2, 30)) >>> 5).toString(2) +
   " - результат >>> 5");
           

Выполнение этого кода дает следующий результат:

 
1100000000000000000000000000000 = (3 << 29).toString(2)
-1000000000000000000000000000000 = (3 << 30).toString(2)
-1000000000000000000000000000000 = 2^31 + 2^30 в двоичном виде
-10000000000000000000000000 - результат >> 5
110000000000000000000000000 - результат >>> 5
           

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

 

Теперь перейдем к необычным операторам, которых нет и в Java. Это операторы === (три последовательных знака "равно") и !== (восклицательный знак и за ним два знака равенства). Это так называемые операторы проверки на "строгое равенство". Дело в том, что обычные операторы == и != сначала конвертируют оба операнда к одному и тому же типу и лишь затем производят сравнение. Таким образом, значением выражения 5.5 == "5.5" является true. А вот при сравнении с помощью операторов === и !== учитывается и тип сравниваемых операндов. Так что 5.5 === "5.5" выдает false. Заметим, что именно на "строгое равенство" проверяются выражения в операторе switch. Также только === и !== позволяют отличить друг от друга null и undefined.

 

Далее следует сказать о дополнительных унарных операторах (приоритет их такой же, как и у прочих унарных): typeof и void. Про typeof мы уже говорили, он определяет принадлежность объекта к одному из встроенных типов и выдает строковое название этого типа. Что же касается оператора void, то он может быть применен к любому выражению и возвращает всегда undefined. Придумать этому оператору естественное применение довольно сложно. Разве что при отладке можно с его помощью временно отменять передачу в функцию какого-то сложного аргумента (быстрее написать void, чем поставить знаки комментария с двух сторон).

 

К операторам сравнения во Флэш МХ добавляется оператор instanceof (он, правда, есть и в Java). Этот оператор проверяет, является ли левый операнд объектом подкласса правого операнда. Подробнее об этом операторе мы еще поговорим в лекции, посвященной наследованию.

 

Устаревшие операторы

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

 
  • lt - меньше (less than)
  • gt - больше (greater than)
  • le - меньше или равно (less or equal)
  • ge - больше или равно (greater or equal)
  • eq - равно (equal)
  • ne - не равно (not equal)
 

Кроме того, из Флэш 4 перешел во Флэш 5 и Флэш МХ набор логических операторов, полностью аналогичных операторам ! (отрицание), && (логическое "И") и || (логическое "ИЛИ"). Вот они:

 
  • and - логическое "И"
  • or - логическое "ИЛИ"
  • not - логическое "НЕ"
 

Также имеется устаревшая форма оператора сравнения != (возвращающего true, если сравниваемые выражения не равны). Этот оператор имеет такой вид: <> (его можно встретить, скажем, в языке Бейсик). Итак, вносим в нашу таблицу устаревших операторов

 
  • <> - оператор "НЕ РАВНО"
 

Наконец, последним в списке устаревших операторов является оператор, предназначенный для склеивания строк:

 
  • add - конкатенация строк
 

Этот оператор преобразует свои операнды в строки, а затем работает точно так же, как со строками работает оператор +.

 

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

 

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

Во Флэш МХ многие стандартные операторы способны работать со строками. В первую очередь, это оператор "+", используемый для сцепления строк. Также все операторы сравнения могут использоваться для сравнения строк согласно их лексикографическому порядку. Например: выражение "five" >= "Five" имеет значение true, а значением выражения "Seven" < "Five" является false. Следует только учитывать, что если один из операндов является числом, то, в отличие от оператора "+", здесь произойдет преобразование к числовому типу. Таким образом, значением выражения "0x5" < 4 будет false, хотя "0x5" < "4" дает true. Наконец, имейте в виду, что если преобразование строкового оператора в число пройдет неудачно (получится Number.NaN), то результатом выполнения сравнения будет undefined.

 

Таблица приоритетов

Приоритеты операторов во Флэш МХ совпадают с таковыми в языке Java. Мы приведем здесь таблицу 2.3 приоритетов, добавив отсутствующие в Java операторы.

 

Все операции, кроме тех, про которые это упомянуто особо, вычисляются слева направо.

 

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

 
Таблица 2.3. Приоритеты операторов
Приоритет Операторы Тип операторов Комментарии
1 () Группировка  
2 [] . Доступ к полю или элементу  
3 - ~ ! ++ - typeof void Унарные Вычисляются справа налево
4 * / % Мультипликативные  
5 + - Аддитивные  
6 << >> >>> Бинарный сдвиг  
7 < > <= >= instanceof Отношение  
8 == != === !== Проверка равенства  
9 & Поразрядное И (AND)  
10 ^ Поразрядное исключающее ИЛИ (XOR)  
11 | Поразрядное ИЛИ (OR)  
12 && Логическое И (AND)  
13 || Логическое ИЛИ (OR)  
14 ?: Условный оператор Вычисляется справа налево
15 = *= /= %= += -= <<= >>= >>>= &= ^= |= Присваивание (простое и составное) Вычисляются справа налево
16 , Последовательное вычисление  
 

Особенности работы управляющих конструкций

Блоки

Блоки во Флэше делаются так же, как и в С++ и Java, - с помощью фигурных скобок. В отличие от С++ переменных, локальных внутри блока во Флэше нет. Как мы узнаем дальше, локальность переменных поддерживается только внутри функций. Переменные же, созданные внутри кода в кадре, попадают в тот объект, в кадре которого мы находимся. Ключевое слово var, означающее создание локальной переменной, в данном случае игнорируется. Таким образом, хотя создание вложенных блоков без каких-либо управляющих конструкций, относящихся к этим блокам, разрешено, особого смысла это не имеет.

 

Стандартные управляющие конструкции

Точно так же, как в С++ и Java, работают конструкции if-else, for, while, do-while, switch; в циклах можно пользоваться операторами break и continue (правда, средств для выхода из многократно вложенных циклов вроде goto или break c меткой не предусмотрено). Вот небольшие примечания по работе стандартных управляющих конструкций во Флэш.

 
  • При наличии многократно вложенных проверок можно пользоваться оператором else if (из двух слов). Это избавляет от необходимости писать блоки большой вложенности (особенно неприятно форматировать такие блоки). Таким образом, у вас может образоваться большая цепочка из операторов if - else if - else if - else if ... Завершаться такая цепочка может либо оператором else if, либо оператором else. Как вы знаете по C++ или Java, после каждого из этих операторов может размещаться как одна инструкция, так и блок из любого числа инструкций. В некоторых случаях даже одинокую инструкцию придется заключить в блок (например, если она управляющая и в ней самой есть блок, а рассматриваемый оператор if или else if не является последним в цепочке). Впрочем, во всех потенциально неясных случаях лучше ставить фигурные скобки хотя бы из соображений читаемости кода.
  • При пользовании оператором for помните, что запись for (int i=0; i<5; i++) во Флэше неверна. Вместо int надо писать var (или не писать ничего; но последний вариант мы не рекомендуем - будут проблемы с перенесением такого кода в функцию. В параграфе о локальных переменных из лекции 5 мы поговорим об этом подробнее).
  • При пользовании оператором switch учтите, что объекты в нем сравниваются при помощи оператора строгого сравнения ===, учитывающего тип объекта. То есть число и строчка с записью этого числа, для которых оператор == выдаст true, будут сочтены разными. Также не забывайте, что данные объектных типов (даже типа Number или String) сравниваются по ссылке. То есть переменные считаются равными, только если указывают на один и тот же объект. Так что в подобных случаях не брезгуйте явным преобразованием типа (особенно если вы получили, скажем, числовой ввод пользователя из текстового поля, а в клаузах case у вас стоят обыкновенные числа. В этом случае приведение к Number внутри switch является необходимым).
  • Как мы уже сказали, с тех пор, как анафеме был предан оператор goto, появилась специфическая проблема выхода из многократно вложенных циклов. В языке Java она решается при помощи оператора break с меткой. К сожалению, ничего подобного нет в языке ActionScript 1.0. Нельзя даже (хотя в стандарте ECMAScript это и описано) бросить и поймать исключение. Так что ничего кроме использования флагов выхода из цикла (или такого экзотического способа: создать специальную функцию, в которой будет помещен внутренний цикл, и из которой можно будет выйти с помощью оператора return) мы для решения этой проблемы предложить не можем. К счастью, возникает она достаточно редко.
 

Специфичные управляющие конструкции

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

 
a_array = [4,6,8, 1212];

b_obj = {x: 15, y: "some text"}
b_obj.a = 1;
b_obj.b = 2;

for (var name in a_array){
   trace("a_array[" + name + "] = " + a_array[name]);
}
for (var name in b_obj){
   trace("b_obj[" + name + "] = " + b_obj[name]);
}
           

Этот код выводит в консоль следующее:

 
a_array[3] = 1212
a_array[2] = 8
a_array[1] = 6
a_array[0] = 4
b_obj[b] = 2
b_obj[a] = 1
b_obj[x] = 15
b_obj[y] = some text
           

Мы видим, что цикл for...in похож на обычный цикл for - тоже надо задать переменную (и лучше всего объявить ее локальной, чтобы такой код можно было использовать внутри функции; напомним, что не внутри функций var игнорируется). Эта переменная при каждом следующем выполнении тела цикла принимает значение, равное имени следующей переменной (указывать переменная может на объекты, функции, примитивные типы). Выход из цикла обычно заканчивается, когда перебраны все поля; однако вы можете применять операторы break и continue, которые и в этом цикле работают как обычно. В массивах именами полей являются номера ячеек. Поэтому логично обращаться к ячейкам, указывая name в квадратных скобках. Оказывается, такой же способ работает и для обычного объекта! (Имя при этом, разумеется, с цифры уже не начинается.) Хотя это свойство и не документировано для Флэш МХ, но используется оно повсеместно. В следующей лекции мы подробнее обсудим всевозможные способы обращения к полям объекта в том случае, когда имя поля записано в строковой переменной. А пока обратим внимание еще и на порядок, в котором в объекте записаны поля (по крайней мере, как мы их видим в операторе for...in). Мы видим, что ячейки массива записаны в обратном порядке; поля, которые мы завели при создании объекта - в прямом; но поля, заведенные позже, попали в самое начало. (И чем позже поле заведено, тем раньше оно появляется при переборе в for...in). Однако не стоит полагаться на этот порядок расположения полей. После сортировки массива (или других его преобразований) этот порядок может измениться (и изменяется). Еще раз подчеркнем: меняется не только расположение содержимого в ячейках массива, но и расположение самих ячеек, когда мы их рассматриваем как поля объекта "массив". Так что по массиву лучше итерироваться с помощью обычного оператора for, а не for...in.

 

Директива #include

Как и в С++, во Флэш МХ есть директива #include. И хотя возможности ее здесь не столь широки (в первую очередь из-за отсутствия препроцессора), польза ее несомненна.

 

Как включить файл

В первую очередь давайте создадим файл по имени inc1.as (расширение as есть сокращение от слов Action Script). Затем создадим новый флэш-документ (*.fla) и сохраним его в ту же директорию, где лежит inc1.as. В inc1.as напишите, например,

 
trace("inc1.as included");
           

Во флэш-документе в первом кадр поместите код

 
#include "inc1.as"
           

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

 

После запуска этого кода вы увидите, что в консоль выдана строчка inc1.as included - включение файла сработало!

 

При работе с #include допускаются включения файлов по цепочке. Также можно указывать путь к файлу, считая от директории, куда сохранен *.fla-файл. Допускаются также полные пути с указанием диска. В пути можно использовать прямые и обратные слэши, причем в любых количествах (то есть обратные слэши могут быть одинарные, двойные или даже четверные - все равно это работает. Даже прямые слэши можно удваивать и утраивать, если есть на то желание). Не забудьте, что если вы включаете файлы по цепочке и указываете не абсолютные пути (с именем диска), а относительные, - в любом из файлов пути будут отсчитываться от директории, в которую сохранен исходный флэш-документ.

 

Наконец, упомянем, что включаемые файлы можно сохранять не только в кодировке ANSI, но и в UTF-8 (это может быть полезно, например, если вы используете строковые константы на различных языках). Однако если вы сохраняете включаемый файл в UTF-8, его первыми символами обязательно должны быть //!-- UTF8 (никаких пробелов перед знаком комментария и строго один пробел перед надписью UTF8). В противном случае компилятор не сможет подключить такой файл и выдаст ошибку.

 

Когда происходит включение

Включение кода из указанного файла происходит в момент компиляции. Более того, никаких средств условной компиляции (наподобие директив препроцессора в С) во Флэше нет. Таким образом, повлиять на то, будет ли включен определенный код в ваш флэш-ролик, или же не будет, нет никакой возможности. Будет! А если нужный файл не будет найден по указанному вами пути, при компиляции будет выдана ошибка. Так что если вам очень нужно подменять включаемый код, вам придется для этого пользоваться средствами операционной системы (сетевыми дисками, командой subst, soft- и hard-линками). О том, как это удобнее всего устроить, читайте в лекции про средства коллективной работы.

 

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

 

Стражи включения

Управлять тем, включается ли заданный код в ваш ролик в зависимости от определенных условий вы (средствами Флэш) не можете, однако вы можете воздействовать на то, выполняется ли этот код. То есть вы вполне можете вставить директиву #include внутри блока, предваренного инструкцией if. Или даже - внутри определения какой-либо функции. Дальнейшая ваша задача - добиться того, чтобы код и в этом обрамлении работал так, как надо. Здесь есть ряд тонких моментов, связанных, например, с определением функций внутри блоков или внутри других функций. Эти моменты будут подробно разобраны в лекции 5, посвященной функциям Флэш МХ. В частности, там разобран вопрос о том, как организовать стражи включения для файлов, содержащих определения функций. Эта задача содержит в себе подвох, поскольку для ее решения подходит лишь один (причем менее употребительный) из двух существующих во Флэш МХ методов определения функций. Поэтому прочтите лекцию 5, прежде чем начнете организовывать стражи включения.

 

Отладка во Flash MX

Вы уже, наверное, успели понять, что роль отладки во Flash MX сложно переоценить. Говоря нормальным языком - отлаживать вам придется, и немало. Ведь во Flash MX компилятор почти не помогает программисту. Единственное, что он поможет выловить на уровне компиляции - это простейшие синтаксические ошибки типа несоответствия фигурных скобок или отсутствия точки с запятой. Все остальное, к сожалению, остается за вами. В этой лекции мы рассмотрим использование встроенного отладчика, а также коротко пройдемся по другим методам отладки, детально рассматриваемым в лекции "Работа флэш-программ в Internet".

 

Использование встроенного отладчика

Во Flash MX имеется достаточно мощный встроенный отладчик, предоставляющий различные необходимые инструменты отладки: точки останова, пошаговое выполнение программы, просмотр значений переменных и вычисление выражений (watches).

 

Для того чтобы запустить флэш-ролик под отладчиком, выберите Control / Debug Movie (Ctrl+Shift+Enter). Кстати, обратите внимание, что при этом создается файл с отладочной информацией (.swd-файл), без которого большинство отладочных инструментов недоступно (этот файл должен лежать в том же каталоге, что и отлаживаемый .swf-файл).

 

Структура отладчика

По нажатию Ctrl+Shift+Enter перед вами открывается отладчик, окно которого состоит из четырех частей (дочерних окон).

 

 


 

 

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

Панель инструментов отладчика

Панель инструментов состоит из 7 кнопок. Рассмотрим их слева направо.

 

Continue - начать/продолжить выполнение ролика;

 

Stop Debugging - прекратить отладку (дальнейшее выполнение ролика продолжается без управления отладчика);

 

Toggle Breakpoint - поставить/убрать точку останова ("line" breakpoint) в данном месте;

 

Remove All Breakpoints - убрать все точки останова;

 

Step Over - сделать шаг вперед, не заходя в функцию/метод;

 

Step In - сделать шаг вперед с заходом в функцию/метод;

 

Step Out - выйти из функции/метода и остановиться на следующем операторе.

 

Итак, вы запустили отладчик, теперь нужно один раз нажать на кнопку Continue для запуска ролика (поставив перед этим все необходимые точки останова) - и можно отлаживать.

 

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

 

Просмотр значений свойств, переменных клипов и стека (locals)

Если выбрать закладку Properties или Variables в левой средней части окна отладчика, то под ней можно увидеть список свойств (или, соответственно, переменных) клипа, выбранного в левой верхней части окна отладчика, а также их значений.

 

Выбрав закладку Locals, вы увидите список локальных переменных с указанием их значений (стек) контекста исполнения, указанного в нижнем левом окне отладчика (Call Stack1)).

 

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

 

Вычисление выражений и корректировка их значений (watch, evaluate/modify)

Если нужно отредактировать значение свойства, переменной клипа или локальной переменной во время отладки, это можно сделать, щелкнув дважды на их текущем значении (в левом среднем окне отладчика) и вписав новое значение.

 

Во Flash MX есть специальная закладка - Watch, которая позволяет наблюдать за состоянием важных для вас переменных клипов и локальных переменных. Для добавления переменных клипов и локальных переменных в закладку нажмите на имя переменной (отображаемой в закладке Variables или Locals) правой кнопкой мыши и выберите в контекстном меню пункт Watch. Добавить переменную в окно Watch можно также, если воспользоваться пунктом Add контекстного меню этого окна. При этом в окне создается пустая строчка, в левой части которой нужно вписать имя переменной, тогда в правой части появится ее значение. Обратите внимание, что имена переменных должны быть записаны единственно правильным способом, с использованием абсолютного пути. Например, для переменной a, созданной в _root, правильным именем будет "_level0.a", но не "а" и не "_root.a". Очевидно, данное ограничение уместно только в окне Watch.

 

Значения переменных, отображаемые в закладке Watch, также можно изменять (просто вписывая новое значение).

 

Точки останова в отладчике и в программе

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

 

Выбор нужного контекста выполнения

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

 

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

 

Нужны ли другие методы отладки?

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

 

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

 

И, наконец, вы, конечно же, захотите протестировать ваш проект "в реальных условиях", то есть, на реальной для Интернета пропускной способности канала. Для этого можно использовать либо встроенный во Flash MX Bandwidth profiler, либо специальный веб-сервер.

 

Обо всех упомянутых только что методах отладки - удаленной отладке, отладочной консоли и о тестировании на канале малой пропускной способности - мы подробно расскажем в лекции "Работа флэш-программ в Internet".

 
  1)   Правда, в окно Call Stack почему-то не попадают функции, созданные так: funcName = function () {}. Если функцию определить таким образом: _root.funcName = function () {}, то она корректно отображается в окне Call Stack. Это похоже на ошибку в отладчике Флэш МХ, потому что в обоих случаях объекты функций создаются в одном и том же месте - в _root. Функции, созданные так: function funcName () {} или так: object.funcName = function () отображаются в окне Call Stack корректно. По поводу различных способов создания функций во Флэш МХ см. лекцию 5.
Авторы: М.А. Капустин, А.Г. Копылова, П.А. Капустин  источник: http://www.INTUIT.ru

 



 

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

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

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

 

____________________________

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

_______________________________

 

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