Ответ на вопрос, вынесенный в заголовок этого параграфа, прост и сложен одновременно. Разумеется, язык
Сначала скажем, как расшифровывается аббревиатура ECMA: это European Computer Manufacturers Association (Европейская ассоциация производителей компьютеров). Эта ассоциация (разумеется, не она одна) занимается стандартами в области вычислительной техники.
eval (об этом см. в лекции 3). Тем не менее,
Если вы хотите ознакомиться со стандартом ECMAScript, он же
Еще раз скажем, что последней версией стандарта является третья; вторая версия в свое время была утверждена в качестве стандарта ISO/IEC 16262, однако в настоящее время ISO уже отозвала этот стандарт.
Также отметим, что большинство нововведений, которые были сделаны в третьей версии стандарта (такие, как регулярные выражения, обработка исключений и т.п.), не вошли в
Вашу работу на
Но прежде чем мы погрузимся в детали, упомянем пару удивительных и непривычных вещей, с которыми нам придется столкнуться. Первая из них такова: точки с запятой после операторов во Флэше ставить необязательно! (Мы не будем пользоваться этой "свободой", она от лукавого.) Вторая удивительная вещь (впрочем, в настоящее время она уже не так удивительна) - это полная поддержка Unicode, в том числе - для идентификаторов языка. То есть, если вам хочется, то ваши классы, объекты, их поля и
Ну что ж, начнем наше путешествие вглубь
Изучая любой язык программирования, желательно в первую очередь разобраться, какими типами данных он оперирует. Несмотря на то что
В С++ провести разделение между типами можно, задав вопрос, применим ли к объектам данного типа оператор "точка". Храниться же могут как объекты, так и данные
Во Флэше все обстоит по-другому. Не так, как в С++, но и не так, как в Java. То есть оператор "точка" применим также и к
Другой способ говорить об этом - это так называемая "упаковка", когда при обращении к функциям
Какая же из точек зрения правильная? Согласно стандарту ECMAScript, первая ближе к истине. То есть имеются типы данных, не обладающие свойствами типа ." оно преобразовывается в некоторый временный read-only -объект, пожалуйста, рассуждайте так. Ибо средствами самого Флэш МХ никакой разницы между этими двумя точками зрения мы обнаружить не сможем.
Общеупотребительных String, Number и Boolean. (Эти же типы могут быть не только String, Number и Boolean не обязаны быть 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 СТРОКА, НАБРАННАЯ СТРОЧНЫМИ БУКВАМИ
Итак, мы видим, что и булевское, и числовое, и строчное выражение допускают вызов какого-либо
Для порядка проверим, что все, что сказано в этом подпараграфе про скобки, также работает. В самом деле, запустив код
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. Кроме ссылок, в объектах Флэш не хранится ничего. Даже
Есть специальное ключевое слово, позволяющее сделать ссылку, ведущую в пустоту. Это слово 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 действительно можно выгружать, но это отдельный разговор.
Касается ли сказанное выше
Возникает естественный вопрос: а где лежали переменные из примеров, приведенных выше? Ведь мы создавали их, определяя Action для какого-то кадра линейки времени? Так вот, линейка времени всегда принадлежит какому-либо клипу. Именно в соответствующем ему объекте (типа ) и лежат переменные, созданные в 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. То есть функция - это объект. Множество интересных следствий этого факта ожидает нас впереди.
Вы, наверное, уже заметили, что в obj, которые мы только что создали, встречается совершенно аномальное количество употреблений ключевого слова this. Точнее, аномальное для С++ или Java. А вот у Флэш МХ есть одна очень неприятная особенность: изнутри this явным образом. Если забыть поставить this - обращение будет к переменной того контекста, в котором создана функция. Что это за контекст - мы сейчас пояснять не будем (все пояснения вы найдете в пятой лекции), важно, что нужных нам полей и this. Если вы запрашиваете значение какой-то переменной, он вернет вам undefined. Еще хуже, если вы, напротив, присваиваете переменной какое-то значение: он заведет эту переменную там, где она совсем вам не нужна, а заметите вы это не сразу. Так что, если написанная вами функция не работает, в первую очередь проверяйте, не забыли ли вы написать this при обращении к полям и
По ходу дела может возникнуть вопрос: если все равно приходится писать this, может быть, проще явно указывать имя объекта? В некоторых случаях так действительно можно делать. Но на самом-то деле в реальных программах мы будем обычно заводить методы у классов, а не у отдельных объектов. А в этом случае мы, конечно, не сможем предугадать, какие имена будут у объектов классов. Да и семантика страдает: читая программу и встретив обращение через конкретное имя объекта, мы будем воспринимать это как указание на то, что данная функция по смыслу своему может работать только с этим конкретным объектом. А видя this, мы, напротив, поймем, что речь идет просто об обращении
Большинство встроенных функций Флэш МХ являются _global. Это означает, что вызывать их можно в любом месте (не указывая объект-хозяин). Важное исключение из этого правила - математические функции. Они представляют собой Math, и этот объект нужно явным образом указывать при вызове функции. Такое не слишком удобное решение происходит из языка Java, где, однако, по-другому сделать было нельзя. А в atan2 (которая, как вы, наверное, помните, служит для облегчения перевода в полярные координаты). Заодно в объекте Math помещены и математические константы (пи, основание натуральных логарифмов, некоторые часто используемые корни и логарифмы). Ряд других важных функций Флэш МХ, которые по смыслу могли бы быть глобальными, тоже являются
Без чего невозможна нормальная работа программиста - так это без четкого представления деталей работы 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). В обычных случаях это не практикуется, но очень малые по
Давайте подсчитаем, сколько битов нужно для хранения порядка числа. Используя 11 битов, мы сможем записать положительные и отрицательные значения величиной от 0 до 1023. Итак, на хранение порядка надо 11 бит, а мантисса занимает 53 бита, как мы уже выяснили. Еще один бит нужен на знак числа. Всего получается 65; еще бы чуть-чуть сэкономить - и выйдет удобные 64 бита. И действительно, сэкономить есть на чем. Поскольку число у нас представляется в виде , то мантисса может быть не от 1.0 до 10.0 (точнее, строго меньше 10.0), как в обычном десятичном представлении, а от 1.0 до 2.0 (строго меньше 2.0). В результате первый бит мантиссы - всегда единица, так что его можно не хранить. Итого на внутреннее представление Number должно отводиться 64 бита (о чем вы, наверняка, догадались с самого начала; но надо ведь было поддержать видимость сюжетной интриги).
А теперь, когда интрига закончилась, пора срывать покровы. Конечно, внутри Number на самом деле скрывается тип double. Самый настоящий, соответствующий стандарту 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.MAX_VALUE. А отрицательная - число, меньшее чем (-Number.MAX_VALUE). Хотя при делении на ноль есть некоторый произвол - какую именно из бесконечностей употребить. А вот при попытке вычислить логарифм нуля мы всегда получим именно минус бесконечность, что естественно. Так вот, положительной бесконечности соответствует константа Number.POSITIVE_INFINITY. А отрицательной, соответственно - константа Number.NEGATIVE_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. Также существует и предопределенная переменная (хотя редактор 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х, восьмеричную с префиксом 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 упрощает перенос на Флэш МХ фрагментов Java-кода.
undefined - это еще один специальный тип. Такой тип имеет выражение, обращающееся к переменной (объекту, функции), значение которой не было заранее задано. Впрочем, если undefined встречается в выражении, в котором оно должно быть преобразовано в строку - оно превращается в пустую строку. При преобразовании в число получается 0, в булевский тип - false. Если переменная была заведена, а потом была удалена с помощью delete, то справившись о значении этой переменной мы снова получим undefined. Запомните, что при сравнении null и undefined операторами == или != мы не сможем обнаружить разницы между двумя этими значениями. Но то, что эти значения разных типов, помогает нам решить проблему при помощи использования === и !==, специфичных для Флэш МХ. Об этих
В некотором смысле понятие "встроенные типы Флэш МХ" не является хорошо определенным. Скажем, встроенный или не встроенный тип - компонент "выпадающий список" из библиотеки компонентов? Мы все же ограничимся наиболее четким из возможных определений, а именно: встроенными типами будем называть только те, для которых специальный оператор typeof выдает различные значения. Синтаксис использования оператора typeof следующий:
typeof <выражение>
Если вам больше нравится рассматривать typeof как функцию, то можно ставить скобки и писать
typeof (<выражение>)
Оператор этот выдает строчку, которая содержит название типа значения, возвращаемого переданным typeof выражением. Строчка при этом приводится к нижнему регистру, как показано в таблице 2.1. В таблице также указано, как воспринимаются объекты, которые пользователь может создавать "руками" - то есть клипы, кнопки и текстовые поля.
Видно, что из создаваемых визуально объектов только клипы оператор typeof "отличает особо". Что же касается собственно языка Number, Boolean, String, Object, Function, undefined и null. Да еще Array имеет специальные операторы для конструирования. И только типы Number, Boolean, String, undefined и null бывают
| Тип | typeof возвращает |
|---|---|
Number |
number |
Boolean |
boolean |
String |
string |
Object |
object |
Function |
function |
Array |
array |
|
|
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-insensitive ). То есть, хотя ключевые слова, как в C++ или Java, к регистру чувствительны ( case-sensitive ), одни и те же идентификаторы можно писать как заглавными буквами, так и строчными (или же вперемешку). Мы, конечно, надеемся, что в своих программах вы не допустите беспорядка и не будете пользоваться этим свойством Флэш. И тем более не будете заводить идентификаторы вроде NEW (хотя это и разрешается). Забавно, что сами создатели Флэш небольшой беспорядок все-таки допустили: существует идентификатор Function (имя конструктора объектов-функций), и в то же время существует ключевое слово , которое как раз и позволяет новые функции создавать. Использовать это ключевое слово в том месте, где по смыслу требуется идентификатор Function, вы, разумеется, не сможете (об этом и говорит последняя строчка-комментарий в вышеприведенном коде).
Отсутствие строгой типизации во Флэш МХ порождает одну специфическую трудность. Современные программные среды очень дружественно относятся к пользователю, в частности, предоставляя ему возможность автопродолжения набранного текста. Самый распространенный (и, наверное, самый полезный) вариант этой возможности выглядит так: вы набираете имя переменной, среда опознает тип этой переменной, и когда вы вслед за именем ставите точку, вам тут же выдается список полей и методов этого типа. Остается лишь выбрать нужный. Это избавляет от необходимости помнить детали написания множества методов, и вообще очень удобно. Но как организовать аналогичную подсказку в том случае, когда строго определенного типа переменная не имеет и может хранить ссылку на какой угодно объект? В этом случае программист должен сам помочь среде Флэш МХ обеспечить ему необходимые удобства. А именно, если программист заранее знает, что в некоторой переменной будет храниться ссылка на объект какого-то определенного типа (так и бывает в большинстве случаев), он может дать этой переменной имя с одним из заранее определенных суффиксов. Такие переменные среда Флэш МХ будет узнавать, и выдаст вам подсказку. Все такие суффиксы начинаются с символа подчеркивания, так что и вам и среде будет легко их узнавать. Например, имя переменной a_str сообщает среде Флэш МХ о том, что в этой переменной, скорее всего, содержится ссылка на строку и подсказку по ней следует выдавать, соответствующую типу String. Вот список предопределенных суффиксов и соответствующих им типов приведен в таблице 2.2.
| Тип | Суффикс | Примечание |
|---|---|---|
| Обыкновенные объекты | ||
Array |
_array |
|
Button |
_btn |
Стандартная кнопка (не компонент) |
Color |
_color |
|
Date |
_date |
|
Sound |
_sound |
Именно объект типа Sound, а не просто звук, импортированный в клип |
String |
_str |
|
TextField |
_txt |
|
TextFormat |
_fmt |
|
XML |
_xml |
|
|
_xmlsocket |
|
| Компоненты | ||
FCheckBox |
_ch |
|
FComboBox |
_cb |
Компонент "Выпадающий список" |
FListBox |
_lb |
|
FPushButton |
_pb |
Кнопка-компонент |
FRadioButton |
_rb |
|
FScrollBar |
_sb |
"Область с прокруткой" и (многострочные) текстовые поля. |
FScrollPane |
_sp |
Компонент "Область с прокруткой" |
Кроме этих суффиксов, Флэш МХ реагирует еще на несколько предопределенных имен, про которые ему заранее известно, что это объекты типа . А именно, на _root (корневой клип данного флэш-ролика), _parent (в объектах типа так называется поле, ссылающееся на родительский клип) и, наконец, на _level0, _level1, _level2 и т.д. (корневые клипы различных флэш-роликов, загруженных в плеер в настоящий момент). В лекции 12 мы расскажем вам о том, как самим делать подобные шаблоны, на которые реагирует среда разработки Флэш МХ.
Флэш МХ имеет набор операторов, слегка расширенный по сравнению с Java. Сначала мы опишем (а точнее, кратко перечислим) ту часть операторов, которая является общей для Флэш МХ, Java и С++. Затем подробнее рассмотрим операторы, которых в С++ нет но есть в Java (для тех читателей, которые не работали на Java). И, наконец, подробно расскажем про операторы, которые вы кроме
Во Флэше имеются практически все операторы из С++ и все операторы из 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 и Флэш МХ. Это оператор беззнакового сдвига вправо >>>. Он позволяет сдвигать вправо все биты, включая знаковый; таким образом, использование этого оператора избавляет нас от побочных эффектов, связанных с переходом старших битов при сдвиге влево в знаковый разряд. Если использовать затем обычный сдвиг вправо, то знак сохраняется навечно (или, если разобраться, что происходит в дополнительном коде, то там
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.
Все операции, кроме тех, про которые это упомянуто особо, вычисляются слева направо.
Напоследок скажем только, что хорошее знание этой таблицы может сослужить вам и плохую службу: вы перестанете ставить скобки везде, где можно обойтись без них, а это ухудшает читаемость кода. Хотя излишнее количество скобок ее, разумеется, также ухудшает. Так что в пользовании скобками, как и в любом деле, надлежит соблюдать меру. (В этой книге мы предпочитаем расставлять побольше скобок - будем надеяться, что не в ущерб восприятию текста.)
| |
Операторы | Тип операторов | Комментарии |
|---|---|---|---|
| 1 | () |
Группировка | |
| 2 | []. |
Доступ к полю или элементу | |
| 3 | - ~ ! ++ - typeof void |
Унарные | Вычисляются справа налево |
| 4 | * / % |
Мультипликативные | |
| 5 | + - |
Аддитивные | |
| 6 | << >> >>> |
Бинарный сдвиг | |
| 7 | < > <= >= instanceof |
Отношение | |
| 8 | == != === !== |
Проверка равенства | |
| 9 | |
Поразрядное И (AND) | |
| 10 | ^ |
Поразрядное исключающее ИЛИ (XOR) | |
| 11 | | |
Поразрядное ИЛИ (OR) | |
| 12 | |
Логическое И (AND) | |
| 13 | || |
Логическое ИЛИ (OR) | |
| 14 | ?: |
Условный оператор | Вычисляется справа налево |
| 15 | = *= /= %= += -= <<= >>= >>>= = ^= |= |
Присваивание (простое и составное) | Вычисляются справа налево |
| 16 | , | Последовательное вычисление |
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 с меткой. К сожалению, ничего подобного нет в языке return ) мы для решения этой проблемы предложить не можем. К счастью, возникает она достаточно редко.На самом деле, специфичной для Флэш является всего одна управляющая конструкция: это оператор . Служит он для перебора всех полей некоторого объекта. Далее (в лекции 4, посвященной контейнерам Флэш МХ) мы увидим, что любой объект во Флэше фактически является хэш-таблицей: по ключу (имени переменной) выдается записанная информация. Перебор всех ключей, по которым записано хоть что-нибудь, - это и есть задача оператора . Под "чем-нибудь" - понимаются обычные переменные, функции, номера элементов массива, если мы применили оператор к массиву, и даже скрытые встроенные поля и методы - если только мы сможем "раскрыть" их. Последнее нужно редко и делается только при помощи недокументированных функций (но об этом мы все же будем говорить в лекции, посвященной наследованию). А вот как делается перебор полей объектов и массивов:
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 - тоже надо задать переменную (и лучше всего объявить ее локальной, чтобы такой код можно было использовать внутри функции; напомним, что не внутри функций var игнорируется). Эта переменная при каждом следующем выполнении тела цикла принимает значение, равное имени следующей переменной (указывать переменная может на объекты, функции, break и continue, которые и в этом цикле работают как обычно. В массивах именами полей являются номера ячеек. Поэтому логично обращаться к ячейкам, указывая name в квадратных скобках. Оказывается, такой же способ работает и для обычного объекта! (Имя при этом, разумеется, с цифры уже не начинается.) Хотя это свойство и не документировано для Флэш МХ, но используется оно повсеместно. В следующей лекции мы подробнее обсудим всевозможные способы обращения к полям объекта в том случае, когда имя поля записано в строковой переменной. А пока обратим внимание еще и на порядок, в котором в объекте записаны поля (по крайней мере, как мы их видим в операторе ). Мы видим, что ячейки массива записаны в обратном порядке; поля, которые мы завели при создании объекта - в прямом; но поля, заведенные позже, попали в самое начало. (И чем позже поле заведено, тем раньше оно появляется при переборе в ). Однако не стоит полагаться на этот порядок расположения полей. После сортировки массива (или других его преобразований) этот порядок может измениться (и изменяется). Еще раз подчеркнем: меняется не только расположение содержимого в ячейках массива, но и расположение самих ячеек, когда мы их рассматриваем как поля объекта "массив". Так что по массиву лучше итерироваться с помощью обычного оператора for, а не .
Как и в С++, во Флэш МХ есть директива . И хотя возможности ее здесь не столь широки (в первую очередь из-за отсутствия препроцессора), польза ее несомненна.
В первую очередь давайте создадим файл по имени inc1.as (расширение as есть сокращение от слов Action Script). Затем создадим новый флэш-документ (*.fla) и сохраним его в ту же директорию, где лежит inc1.as. В inc1.as напишите, например,
trace("inc1.as included");
Во флэш-документе в первом кадр поместите код
#include "inc1.as"
Обратите внимание, что точку с запятой ставить после этой директивы нельзя. Вообще ничего лишнего в строке после директивы писать нельзя (а вот до знака #, как это ни странно, иногда можно, но лучше не пишите там ничего; пользы от этого все равно никакой не будет). Кавычки должны быть обязательно двойными.
После запуска этого кода вы увидите, что в консоль выдана строчка inc1.as included - включение файла сработало!
При работе с допускаются включения файлов по цепочке. Также можно указывать путь к файлу, считая от директории, куда сохранен *.fla-файл. Допускаются также полные пути с указанием диска. В пути можно использовать прямые и обратные слэши, причем в любых количествах (то есть обратные слэши могут быть одинарные, двойные или даже четверные - все равно это работает. Даже прямые слэши можно удваивать и утраивать, если есть на то желание). Не забудьте, что если вы включаете файлы по цепочке и указываете не абсолютные пути (с именем диска), а относительные, - в любом из файлов пути будут отсчитываться от директории, в которую сохранен исходный флэш-документ.
Наконец, упомянем, что включаемые файлы можно сохранять не только в кодировке ANSI, но и в UTF-8 (это может быть полезно, например, если вы используете строковые константы на различных языках). Однако если вы сохраняете включаемый файл в UTF-8, его первыми символами обязательно должны быть //!-- UTF8 (никаких пробелов перед знаком комментария и строго один пробел перед надписью UTF8). В противном случае компилятор не сможет подключить такой файл и выдаст ошибку.
Включение кода из указанного файла происходит в момент компиляции. Более того, никаких средств условной компиляции (наподобие директив препроцессора в С) во Флэше нет. Таким образом, повлиять на то, будет ли включен определенный код в ваш флэш-ролик, или же не будет, нет никакой возможности. Будет! А если нужный файл не будет найден по указанному вами пути, при компиляции будет выдана ошибка. Так что если вам очень нужно подменять включаемый код, вам придется для этого пользоваться средствами операционной системы (сетевыми дисками, командой subst, soft - и hard -линками). О том, как это удобнее всего устроить, читайте в лекции про средства коллективной работы.
Зато вам не нужно заботиться о ваших включаемых файлах при запуске флэш-ролика. В отличие от файлов с переменными и настройками, которые вы можете подгружать явным образом при помощи объекта LoadVars (см. лекцию 14), включаемые файлы после компиляции больше не нужны.
Управлять тем, включается ли заданный код в ваш ролик в зависимости от определенных условий вы (средствами Флэш) не можете, однако вы можете воздействовать на то, выполняется ли этот код. То есть вы вполне можете вставить директиву внутри if. Или даже - внутри определения какой-либо функции. Дальнейшая ваша задача - добиться того, чтобы код и в этом обрамлении работал так, как надо. Здесь есть ряд тонких моментов, связанных, например, с определением функций внутри
Вы уже, наверное, успели понять, что роль отладки во Flash MX сложно переоценить. Говоря нормальным языком - отлаживать вам придется, и немало. Ведь во Flash MX компилятор почти не помогает программисту. Единственное, что он поможет выловить на уровне компиляции - это простейшие синтаксические ошибки типа несоответствия фигурных скобок или отсутствия точки с запятой. Все остальное, к сожалению, остается за вами. В этой лекции мы рассмотрим использование встроенного
Во Flash MX имеется достаточно мощный встроенный
Для того чтобы запустить флэш-ролик под
По нажатию Ctrl+Shift+Enter перед вами открывается

Панель инструментов состоит из 7 кнопок. Рассмотрим их слева направо.
Continue - начать/продолжить выполнение ролика;
Stop Debugging - прекратить отладку (дальнейшее выполнение ролика продолжается без управления
Toggle Breakpoint - поставить/убрать точку останова ( "line" breakpoint ) в данном месте;
Remove All Breakpoints - убрать все точки останова;
Step Over - сделать шаг вперед, не заходя в функцию/метод;
Step In - сделать шаг вперед с заходом в функцию/метод;
Step Out - выйти из функции/метода и остановиться на следующем операторе.
Итак, вы запустили
Базовые принципы отладки, такие, как пошаговое выполнение и точки останова, мы детально рассматривать не будем, ибо они совершенно аналогично действуют в любой другой среде разработки, остановимся подробнее на той части
Если выбрать закладку Properties или Variables в левой средней части окна
Выбрав закладку Locals, вы увидите список локальных переменных с указанием их значений (стек) контекста исполнения, указанного в нижнем левом окне funcName = function () {}. Если функцию определить таким образом: _root.funcName = function () {}, то она корректно отображается в окне Call Stack. Это похоже на ошибку в _root. Функции, созданные так: function funcName () {} или так: object.funcName = function () отображаются в окне Call Stack корректно. По поводу различных способов создания функций во Флэш МХ см. лекцию 5.
Полезный совет: не забывайте открывать (с помощью значка плюса) такие объекты, как this, чтобы увидеть значения их полей.
Если нужно отредактировать значение свойства, переменной клипа или локальной переменной во время отладки, это можно сделать, щелкнув дважды на их текущем значении (в левом среднем окне
Во Flash MX есть специальная закладка - Watch, которая позволяет наблюдать за состоянием важных для вас переменных клипов и локальных переменных. Для добавления переменных клипов и локальных переменных в закладку нажмите на имя переменной (отображаемой в закладке Variables или Locals ) правой кнопкой мыши и выберите в контекстном меню пункт Watch. Добавить переменную в окно Watch можно также, если воспользоваться пунктом Add контекстного меню этого окна. При этом в окне создается пустая строчка, в левой части которой нужно вписать имя переменной, тогда в правой части появится ее значение. Обратите внимание, что имена переменных должны быть записаны единственно правильным способом, с использованием абсолютного пути. Например, для переменной a, созданной в _root, правильным именем будет "_level0.a", но не "а" и не "_root.a". Очевидно, данное ограничение уместно только в окне Watch.
Значения переменных, отображаемые в закладке Watch, также можно изменять (просто вписывая новое значение).
Следует отметить один интересный факт, касающийся точек останова: если их расставлять уже в
Как мы уже ранее заметили, в правой части
Правда, этим линейным списком очень неудобно пользоваться, если ролик большой и элементов с кодом много. В таких случаях бывает удобнее заранее расставить все необходимые точки останова перед отладкой.
Это вопрос, над которым стоит задуматься. Если ваш проект небольшой, вам должно хватить возможностей встроенного
Однако в большом проекте могут окупиться и затраты по включению дополнительных отладочных возможностей прямо в библиотеку, которую вы для этого проекта делаете. Вы вполне можете положить поверх вашего ролика текстовую панель, которая все время скрыта, но может быть показана по нажатию какой-нибудь клавиши (например, F12 ). Получится своего рода аналог отладочной консоли. В эту консоль ваши методы (если установлена в true заведенная вами переменная "отладочный режим") могут выводить информацию о своей работе (не только об ошибках). Конечно, в вашей воле поддерживать несколько отладочных режимов с разной степенью детализации информации, выдаваемой в консоль. Опыт показывает, что в каждом кадре в консоль вполне может выдаваться не один десяток строчек, и это практически не влияет на производительность (если не хранить слишком большое количество строчек).
И, наконец, вы, конечно же, захотите протестировать ваш проект "в реальных условиях", то есть, на реальной для Интернета пропускной способности канала. Для этого можно использовать либо встроенный во Flash MX Bandwidth profiler, либо специальный веб-сервер.
Обо всех упомянутых только что методах отладки - удаленной отладке, отладочной консоли и о тестировании на канале малой пропускной способности - мы подробно расскажем в лекции "Работа флэш-программ в Internet".
Ответ на вопрос, вынесенный в заголовок этого параграфа, прост и сложен одновременно. Разумеется, язык
Сначала скажем, как расшифровывается аббревиатура ECMA: это European Computer Manufacturers Association (Европейская ассоциация производителей компьютеров). Эта ассоциация (разумеется, не она одна) занимается стандартами в области вычислительной техники.
eval (об этом см. в лекции 3). Тем не менее,
Если вы хотите ознакомиться со стандартом ECMAScript, он же
Еще раз скажем, что последней версией стандарта является третья; вторая версия в свое время была утверждена в качестве стандарта ISO/IEC 16262, однако в настоящее время ISO уже отозвала этот стандарт.
Также отметим, что большинство нововведений, которые были сделаны в третьей версии стандарта (такие, как регулярные выражения, обработка исключений и т.п.), не вошли в
Вашу работу на
Но прежде чем мы погрузимся в детали, упомянем пару удивительных и непривычных вещей, с которыми нам придется столкнуться. Первая из них такова: точки с запятой после операторов во Флэше ставить необязательно! (Мы не будем пользоваться этой "свободой", она от лукавого.) Вторая удивительная вещь (впрочем, в настоящее время она уже не так удивительна) - это полная поддержка Unicode, в том числе - для идентификаторов языка. То есть, если вам хочется, то ваши классы, объекты, их поля и
Ну что ж, начнем наше путешествие вглубь
Изучая любой язык программирования, желательно в первую очередь разобраться, какими типами данных он оперирует. Несмотря на то что
В С++ провести разделение между типами можно, задав вопрос, применим ли к объектам данного типа оператор "точка". Храниться же могут как объекты, так и данные
Во Флэше все обстоит по-другому. Не так, как в С++, но и не так, как в Java. То есть оператор "точка" применим также и к
Другой способ говорить об этом - это так называемая "упаковка", когда при обращении к функциям
Какая же из точек зрения правильная? Согласно стандарту ECMAScript, первая ближе к истине. То есть имеются типы данных, не обладающие свойствами типа ." оно преобразовывается в некоторый временный read-only -объект, пожалуйста, рассуждайте так. Ибо средствами самого Флэш МХ никакой разницы между этими двумя точками зрения мы обнаружить не сможем.
Общеупотребительных String, Number и Boolean. (Эти же типы могут быть не только String, Number и Boolean не обязаны быть 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 СТРОКА, НАБРАННАЯ СТРОЧНЫМИ БУКВАМИ
Итак, мы видим, что и булевское, и числовое, и строчное выражение допускают вызов какого-либо
Для порядка проверим, что все, что сказано в этом подпараграфе про скобки, также работает. В самом деле, запустив код
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. Кроме ссылок, в объектах Флэш не хранится ничего. Даже
Есть специальное ключевое слово, позволяющее сделать ссылку, ведущую в пустоту. Это слово 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 действительно можно выгружать, но это отдельный разговор.
Касается ли сказанное выше
Возникает естественный вопрос: а где лежали переменные из примеров, приведенных выше? Ведь мы создавали их, определяя Action для какого-то кадра линейки времени? Так вот, линейка времени всегда принадлежит какому-либо клипу. Именно в соответствующем ему объекте (типа ) и лежат переменные, созданные в 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. То есть функция - это объект. Множество интересных следствий этого факта ожидает нас впереди.
Вы, наверное, уже заметили, что в obj, которые мы только что создали, встречается совершенно аномальное количество употреблений ключевого слова this. Точнее, аномальное для С++ или Java. А вот у Флэш МХ есть одна очень неприятная особенность: изнутри this явным образом. Если забыть поставить this - обращение будет к переменной того контекста, в котором создана функция. Что это за контекст - мы сейчас пояснять не будем (все пояснения вы найдете в пятой лекции), важно, что нужных нам полей и this. Если вы запрашиваете значение какой-то переменной, он вернет вам undefined. Еще хуже, если вы, напротив, присваиваете переменной какое-то значение: он заведет эту переменную там, где она совсем вам не нужна, а заметите вы это не сразу. Так что, если написанная вами функция не работает, в первую очередь проверяйте, не забыли ли вы написать this при обращении к полям и
По ходу дела может возникнуть вопрос: если все равно приходится писать this, может быть, проще явно указывать имя объекта? В некоторых случаях так действительно можно делать. Но на самом-то деле в реальных программах мы будем обычно заводить методы у классов, а не у отдельных объектов. А в этом случае мы, конечно, не сможем предугадать, какие имена будут у объектов классов. Да и семантика страдает: читая программу и встретив обращение через конкретное имя объекта, мы будем воспринимать это как указание на то, что данная функция по смыслу своему может работать только с этим конкретным объектом. А видя this, мы, напротив, поймем, что речь идет просто об обращении
Большинство встроенных функций Флэш МХ являются _global. Это означает, что вызывать их можно в любом месте (не указывая объект-хозяин). Важное исключение из этого правила - математические функции. Они представляют собой Math, и этот объект нужно явным образом указывать при вызове функции. Такое не слишком удобное решение происходит из языка Java, где, однако, по-другому сделать было нельзя. А в atan2 (которая, как вы, наверное, помните, служит для облегчения перевода в полярные координаты). Заодно в объекте Math помещены и математические константы (пи, основание натуральных логарифмов, некоторые часто используемые корни и логарифмы). Ряд других важных функций Флэш МХ, которые по смыслу могли бы быть глобальными, тоже являются
Без чего невозможна нормальная работа программиста - так это без четкого представления деталей работы 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). В обычных случаях это не практикуется, но очень малые по
Давайте подсчитаем, сколько битов нужно для хранения порядка числа. Используя 11 битов, мы сможем записать положительные и отрицательные значения величиной от 0 до 1023. Итак, на хранение порядка надо 11 бит, а мантисса занимает 53 бита, как мы уже выяснили. Еще один бит нужен на знак числа. Всего получается 65; еще бы чуть-чуть сэкономить - и выйдет удобные 64 бита. И действительно, сэкономить есть на чем. Поскольку число у нас представляется в виде , то мантисса может быть не от 1.0 до 10.0 (точнее, строго меньше 10.0), как в обычном десятичном представлении, а от 1.0 до 2.0 (строго меньше 2.0). В результате первый бит мантиссы - всегда единица, так что его можно не хранить. Итого на внутреннее представление Number должно отводиться 64 бита (о чем вы, наверняка, догадались с самого начала; но надо ведь было поддержать видимость сюжетной интриги).
А теперь, когда интрига закончилась, пора срывать покровы. Конечно, внутри Number на самом деле скрывается тип double. Самый настоящий, соответствующий стандарту 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.MAX_VALUE. А отрицательная - число, меньшее чем (-Number.MAX_VALUE). Хотя при делении на ноль есть некоторый произвол - какую именно из бесконечностей употребить. А вот при попытке вычислить логарифм нуля мы всегда получим именно минус бесконечность, что естественно. Так вот, положительной бесконечности соответствует константа Number.POSITIVE_INFINITY. А отрицательной, соответственно - константа Number.NEGATIVE_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. Также существует и предопределенная переменная (хотя редактор 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х, восьмеричную с префиксом 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 упрощает перенос на Флэш МХ фрагментов Java-кода.
undefined - это еще один специальный тип. Такой тип имеет выражение, обращающееся к переменной (объекту, функции), значение которой не было заранее задано. Впрочем, если undefined встречается в выражении, в котором оно должно быть преобразовано в строку - оно превращается в пустую строку. При преобразовании в число получается 0, в булевский тип - false. Если переменная была заведена, а потом была удалена с помощью delete, то справившись о значении этой переменной мы снова получим undefined. Запомните, что при сравнении null и undefined операторами == или != мы не сможем обнаружить разницы между двумя этими значениями. Но то, что эти значения разных типов, помогает нам решить проблему при помощи использования === и !==, специфичных для Флэш МХ. Об этих
В некотором смысле понятие "встроенные типы Флэш МХ" не является хорошо определенным. Скажем, встроенный или не встроенный тип - компонент "выпадающий список" из библиотеки компонентов? Мы все же ограничимся наиболее четким из возможных определений, а именно: встроенными типами будем называть только те, для которых специальный оператор typeof выдает различные значения. Синтаксис использования оператора typeof следующий:
typeof <выражение>
Если вам больше нравится рассматривать typeof как функцию, то можно ставить скобки и писать
typeof (<выражение>)
Оператор этот выдает строчку, которая содержит название типа значения, возвращаемого переданным typeof выражением. Строчка при этом приводится к нижнему регистру, как показано в таблице 2.1. В таблице также указано, как воспринимаются объекты, которые пользователь может создавать "руками" - то есть клипы, кнопки и текстовые поля.
Видно, что из создаваемых визуально объектов только клипы оператор typeof "отличает особо". Что же касается собственно языка Number, Boolean, String, Object, Function, undefined и null. Да еще Array имеет специальные операторы для конструирования. И только типы Number, Boolean, String, undefined и null бывают
| Тип | typeof возвращает |
|---|---|
Number |
number |
Boolean |
boolean |
String |
string |
Object |
object |
Function |
function |
Array |
array |
|
|
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-insensitive ). То есть, хотя ключевые слова, как в C++ или Java, к регистру чувствительны ( case-sensitive ), одни и те же идентификаторы можно писать как заглавными буквами, так и строчными (или же вперемешку). Мы, конечно, надеемся, что в своих программах вы не допустите беспорядка и не будете пользоваться этим свойством Флэш. И тем более не будете заводить идентификаторы вроде NEW (хотя это и разрешается). Забавно, что сами создатели Флэш небольшой беспорядок все-таки допустили: существует идентификатор Function (имя конструктора объектов-функций), и в то же время существует ключевое слово , которое как раз и позволяет новые функции создавать. Использовать это ключевое слово в том месте, где по смыслу требуется идентификатор Function, вы, разумеется, не сможете (об этом и говорит последняя строчка-комментарий в вышеприведенном коде).
Отсутствие строгой типизации во Флэш МХ порождает одну специфическую трудность. Современные программные среды очень дружественно относятся к пользователю, в частности, предоставляя ему возможность автопродолжения набранного текста. Самый распространенный (и, наверное, самый полезный) вариант этой возможности выглядит так: вы набираете имя переменной, среда опознает тип этой переменной, и когда вы вслед за именем ставите точку, вам тут же выдается список полей и методов этого типа. Остается лишь выбрать нужный. Это избавляет от необходимости помнить детали написания множества методов, и вообще очень удобно. Но как организовать аналогичную подсказку в том случае, когда строго определенного типа переменная не имеет и может хранить ссылку на какой угодно объект? В этом случае программист должен сам помочь среде Флэш МХ обеспечить ему необходимые удобства. А именно, если программист заранее знает, что в некоторой переменной будет храниться ссылка на объект какого-то определенного типа (так и бывает в большинстве случаев), он может дать этой переменной имя с одним из заранее определенных суффиксов. Такие переменные среда Флэш МХ будет узнавать, и выдаст вам подсказку. Все такие суффиксы начинаются с символа подчеркивания, так что и вам и среде будет легко их узнавать. Например, имя переменной a_str сообщает среде Флэш МХ о том, что в этой переменной, скорее всего, содержится ссылка на строку и подсказку по ней следует выдавать, соответствующую типу String. Вот список предопределенных суффиксов и соответствующих им типов приведен в таблице 2.2.
| Тип | Суффикс | Примечание |
|---|---|---|
| Обыкновенные объекты | ||
Array |
_array |
|
Button |
_btn |
Стандартная кнопка (не компонент) |
Color |
_color |
|
Date |
_date |
|
Sound |
_sound |
Именно объект типа Sound, а не просто звук, импортированный в клип |
String |
_str |
|
TextField |
_txt |
|
TextFormat |
_fmt |
|
XML |
_xml |
|
|
_xmlsocket |
|
| Компоненты | ||
FCheckBox |
_ch |
|
FComboBox |
_cb |
Компонент "Выпадающий список" |
FListBox |
_lb |
|
FPushButton |
_pb |
Кнопка-компонент |
FRadioButton |
_rb |
|
FScrollBar |
_sb |
"Область с прокруткой" и (многострочные) текстовые поля. |
FScrollPane |
_sp |
Компонент "Область с прокруткой" |
Кроме этих суффиксов, Флэш МХ реагирует еще на несколько предопределенных имен, про которые ему заранее известно, что это объекты типа . А именно, на _root (корневой клип данного флэш-ролика), _parent (в объектах типа так называется поле, ссылающееся на родительский клип) и, наконец, на _level0, _level1, _level2 и т.д. (корневые клипы различных флэш-роликов, загруженных в плеер в настоящий момент). В лекции 12 мы расскажем вам о том, как самим делать подобные шаблоны, на которые реагирует среда разработки Флэш МХ.
Флэш МХ имеет набор операторов, слегка расширенный по сравнению с Java. Сначала мы опишем (а точнее, кратко перечислим) ту часть операторов, которая является общей для Флэш МХ, Java и С++. Затем подробнее рассмотрим операторы, которых в С++ нет но есть в Java (для тех читателей, которые не работали на Java). И, наконец, подробно расскажем про операторы, которые вы кроме
Во Флэше имеются практически все операторы из С++ и все операторы из 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 и Флэш МХ. Это оператор беззнакового сдвига вправо >>>. Он позволяет сдвигать вправо все биты, включая знаковый; таким образом, использование этого оператора избавляет нас от побочных эффектов, связанных с переходом старших битов при сдвиге влево в знаковый разряд. Если использовать затем обычный сдвиг вправо, то знак сохраняется навечно (или, если разобраться, что происходит в дополнительном коде, то там
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.
Все операции, кроме тех, про которые это упомянуто особо, вычисляются слева направо.
Напоследок скажем только, что хорошее знание этой таблицы может сослужить вам и плохую службу: вы перестанете ставить скобки везде, где можно обойтись без них, а это ухудшает читаемость кода. Хотя излишнее количество скобок ее, разумеется, также ухудшает. Так что в пользовании скобками, как и в любом деле, надлежит соблюдать меру. (В этой книге мы предпочитаем расставлять побольше скобок - будем надеяться, что не в ущерб восприятию текста.)
| |
Операторы | Тип операторов | Комментарии |
|---|---|---|---|
| 1 | () |
Группировка | |
| 2 | []. |
Доступ к полю или элементу | |
| 3 | - ~ ! ++ - typeof void |
Унарные | Вычисляются справа налево |
| 4 | * / % |
Мультипликативные | |
| 5 | + - |
Аддитивные | |
| 6 | << >> >>> |
Бинарный сдвиг | |
| 7 | < > <= >= instanceof |
Отношение | |
| 8 | == != === !== |
Проверка равенства | |
| 9 | |
Поразрядное И (AND) | |
| 10 | ^ |
Поразрядное исключающее ИЛИ (XOR) | |
| 11 | | |
Поразрядное ИЛИ (OR) | |
| 12 | |
Логическое И (AND) | |
| 13 | || |
Логическое ИЛИ (OR) | |
| 14 | ?: |
Условный оператор | Вычисляется справа налево |
| 15 | = *= /= %= += -= <<= >>= >>>= = ^= |= |
Присваивание (простое и составное) | Вычисляются справа налево |
| 16 | , | Последовательное вычисление |
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 с меткой. К сожалению, ничего подобного нет в языке return ) мы для решения этой проблемы предложить не можем. К счастью, возникает она достаточно редко.На самом деле, специфичной для Флэш является всего одна управляющая конструкция: это оператор . Служит он для перебора всех полей некоторого объекта. Далее (в лекции 4, посвященной контейнерам Флэш МХ) мы увидим, что любой объект во Флэше фактически является хэш-таблицей: по ключу (имени переменной) выдается записанная информация. Перебор всех ключей, по которым записано хоть что-нибудь, - это и есть задача оператора . Под "чем-нибудь" - понимаются обычные переменные, функции, номера элементов массива, если мы применили оператор к массиву, и даже скрытые встроенные поля и методы - если только мы сможем "раскрыть" их. Последнее нужно редко и делается только при помощи недокументированных функций (но об этом мы все же будем говорить в лекции, посвященной наследованию). А вот как делается перебор полей объектов и массивов:
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 - тоже надо задать переменную (и лучше всего объявить ее локальной, чтобы такой код можно было использовать внутри функции; напомним, что не внутри функций var игнорируется). Эта переменная при каждом следующем выполнении тела цикла принимает значение, равное имени следующей переменной (указывать переменная может на объекты, функции, break и continue, которые и в этом цикле работают как обычно. В массивах именами полей являются номера ячеек. Поэтому логично обращаться к ячейкам, указывая name в квадратных скобках. Оказывается, такой же способ работает и для обычного объекта! (Имя при этом, разумеется, с цифры уже не начинается.) Хотя это свойство и не документировано для Флэш МХ, но используется оно повсеместно. В следующей лекции мы подробнее обсудим всевозможные способы обращения к полям объекта в том случае, когда имя поля записано в строковой переменной. А пока обратим внимание еще и на порядок, в котором в объекте записаны поля (по крайней мере, как мы их видим в операторе ). Мы видим, что ячейки массива записаны в обратном порядке; поля, которые мы завели при создании объекта - в прямом; но поля, заведенные позже, попали в самое начало. (И чем позже поле заведено, тем раньше оно появляется при переборе в ). Однако не стоит полагаться на этот порядок расположения полей. После сортировки массива (или других его преобразований) этот порядок может измениться (и изменяется). Еще раз подчеркнем: меняется не только расположение содержимого в ячейках массива, но и расположение самих ячеек, когда мы их рассматриваем как поля объекта "массив". Так что по массиву лучше итерироваться с помощью обычного оператора for, а не .
Как и в С++, во Флэш МХ есть директива . И хотя возможности ее здесь не столь широки (в первую очередь из-за отсутствия препроцессора), польза ее несомненна.
В первую очередь давайте создадим файл по имени inc1.as (расширение as есть сокращение от слов Action Script). Затем создадим новый флэш-документ (*.fla) и сохраним его в ту же директорию, где лежит inc1.as. В inc1.as напишите, например,
trace("inc1.as included");
Во флэш-документе в первом кадр поместите код
#include "inc1.as"
Обратите внимание, что точку с запятой ставить после этой директивы нельзя. Вообще ничего лишнего в строке после директивы писать нельзя (а вот до знака #, как это ни странно, иногда можно, но лучше не пишите там ничего; пользы от этого все равно никакой не будет). Кавычки должны быть обязательно двойными.
После запуска этого кода вы увидите, что в консоль выдана строчка inc1.as included - включение файла сработало!
При работе с допускаются включения файлов по цепочке. Также можно указывать путь к файлу, считая от директории, куда сохранен *.fla-файл. Допускаются также полные пути с указанием диска. В пути можно использовать прямые и обратные слэши, причем в любых количествах (то есть обратные слэши могут быть одинарные, двойные или даже четверные - все равно это работает. Даже прямые слэши можно удваивать и утраивать, если есть на то желание). Не забудьте, что если вы включаете файлы по цепочке и указываете не абсолютные пути (с именем диска), а относительные, - в любом из файлов пути будут отсчитываться от директории, в которую сохранен исходный флэш-документ.
Наконец, упомянем, что включаемые файлы можно сохранять не только в кодировке ANSI, но и в UTF-8 (это может быть полезно, например, если вы используете строковые константы на различных языках). Однако если вы сохраняете включаемый файл в UTF-8, его первыми символами обязательно должны быть //!-- UTF8 (никаких пробелов перед знаком комментария и строго один пробел перед надписью UTF8). В противном случае компилятор не сможет подключить такой файл и выдаст ошибку.
Включение кода из указанного файла происходит в момент компиляции. Более того, никаких средств условной компиляции (наподобие директив препроцессора в С) во Флэше нет. Таким образом, повлиять на то, будет ли включен определенный код в ваш флэш-ролик, или же не будет, нет никакой возможности. Будет! А если нужный файл не будет найден по указанному вами пути, при компиляции будет выдана ошибка. Так что если вам очень нужно подменять включаемый код, вам придется для этого пользоваться средствами операционной системы (сетевыми дисками, командой subst, soft - и hard -линками). О том, как это удобнее всего устроить, читайте в лекции про средства коллективной работы.
Зато вам не нужно заботиться о ваших включаемых файлах при запуске флэш-ролика. В отличие от файлов с переменными и настройками, которые вы можете подгружать явным образом при помощи объекта LoadVars (см. лекцию 14), включаемые файлы после компиляции больше не нужны.
Управлять тем, включается ли заданный код в ваш ролик в зависимости от определенных условий вы (средствами Флэш) не можете, однако вы можете воздействовать на то, выполняется ли этот код. То есть вы вполне можете вставить директиву внутри if. Или даже - внутри определения какой-либо функции. Дальнейшая ваша задача - добиться того, чтобы код и в этом обрамлении работал так, как надо. Здесь есть ряд тонких моментов, связанных, например, с определением функций внутри
Вы уже, наверное, успели понять, что роль отладки во Flash MX сложно переоценить. Говоря нормальным языком - отлаживать вам придется, и немало. Ведь во Flash MX компилятор почти не помогает программисту. Единственное, что он поможет выловить на уровне компиляции - это простейшие синтаксические ошибки типа несоответствия фигурных скобок или отсутствия точки с запятой. Все остальное, к сожалению, остается за вами. В этой лекции мы рассмотрим использование встроенного
Во Flash MX имеется достаточно мощный встроенный
Для того чтобы запустить флэш-ролик под
По нажатию Ctrl+Shift+Enter перед вами открывается

Панель инструментов состоит из 7 кнопок. Рассмотрим их слева направо.
Continue - начать/продолжить выполнение ролика;
Stop Debugging - прекратить отладку (дальнейшее выполнение ролика продолжается без управления
Toggle Breakpoint - поставить/убрать точку останова ( "line" breakpoint ) в данном месте;
Remove All Breakpoints - убрать все точки останова;
Step Over - сделать шаг вперед, не заходя в функцию/метод;
Step In - сделать шаг вперед с заходом в функцию/метод;
Step Out - выйти из функции/метода и остановиться на следующем операторе.
Итак, вы запустили
Базовые принципы отладки, такие, как пошаговое выполнение и точки останова, мы детально рассматривать не будем, ибо они совершенно аналогично действуют в любой другой среде разработки, остановимся подробнее на той части
Если выбрать закладку Properties или Variables в левой средней части окна
Выбрав закладку Locals, вы увидите список локальных переменных с указанием их значений (стек) контекста исполнения, указанного в нижнем левом окне funcName = function () {}. Если функцию определить таким образом: _root.funcName = function () {}, то она корректно отображается в окне Call Stack. Это похоже на ошибку в _root. Функции, созданные так: function funcName () {} или так: object.funcName = function () отображаются в окне Call Stack корректно. По поводу различных способов создания функций во Флэш МХ см. лекцию 5.
Полезный совет: не забывайте открывать (с помощью значка плюса) такие объекты, как this, чтобы увидеть значения их полей.
Если нужно отредактировать значение свойства, переменной клипа или локальной переменной во время отладки, это можно сделать, щелкнув дважды на их текущем значении (в левом среднем окне
Во Flash MX есть специальная закладка - Watch, которая позволяет наблюдать за состоянием важных для вас переменных клипов и локальных переменных. Для добавления переменных клипов и локальных переменных в закладку нажмите на имя переменной (отображаемой в закладке Variables или Locals ) правой кнопкой мыши и выберите в контекстном меню пункт Watch. Добавить переменную в окно Watch можно также, если воспользоваться пунктом Add контекстного меню этого окна. При этом в окне создается пустая строчка, в левой части которой нужно вписать имя переменной, тогда в правой части появится ее значение. Обратите внимание, что имена переменных должны быть записаны единственно правильным способом, с использованием абсолютного пути. Например, для переменной a, созданной в _root, правильным именем будет "_level0.a", но не "а" и не "_root.a". Очевидно, данное ограничение уместно только в окне Watch.
Значения переменных, отображаемые в закладке Watch, также можно изменять (просто вписывая новое значение).
Следует отметить один интересный факт, касающийся точек останова: если их расставлять уже в
Как мы уже ранее заметили, в правой части
Правда, этим линейным списком очень неудобно пользоваться, если ролик большой и элементов с кодом много. В таких случаях бывает удобнее заранее расставить все необходимые точки останова перед отладкой.
Это вопрос, над которым стоит задуматься. Если ваш проект небольшой, вам должно хватить возможностей встроенного
Однако в большом проекте могут окупиться и затраты по включению дополнительных отладочных возможностей прямо в библиотеку, которую вы для этого проекта делаете. Вы вполне можете положить поверх вашего ролика текстовую панель, которая все время скрыта, но может быть показана по нажатию какой-нибудь клавиши (например, F12 ). Получится своего рода аналог отладочной консоли. В эту консоль ваши методы (если установлена в true заведенная вами переменная "отладочный режим") могут выводить информацию о своей работе (не только об ошибках). Конечно, в вашей воле поддерживать несколько отладочных режимов с разной степенью детализации информации, выдаваемой в консоль. Опыт показывает, что в каждом кадре в консоль вполне может выдаваться не один десяток строчек, и это практически не влияет на производительность (если не хранить слишком большое количество строчек).
И, наконец, вы, конечно же, захотите протестировать ваш проект "в реальных условиях", то есть, на реальной для Интернета пропускной способности канала. Для этого можно использовать либо встроенный во Flash MX Bandwidth profiler, либо специальный веб-сервер.
Обо всех упомянутых только что методах отладки - удаленной отладке, отладочной консоли и о тестировании на канале малой пропускной способности - мы подробно расскажем в лекции "Работа флэш-программ в Internet".
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.