То, как ActionScript
реализует функции - на
первый взгляд очевидно, на второй - непривычно,
последующие же взгляды открывают ряд неожиданных
возможностей (например, возможности параметризации). Но
сначала мы должны научиться просто определять и вызывать
функции.
Два способа определения
функций
Определяем функцию
заранее
Самый простой способ
определить функцию - это
написать
function func_name(arg1,
arg2){
// здесь определяем тело функции
return ret_value; // возвращаем значение
}
То есть почти как в С
- только вместо типа возвращаемого значения мы пишем
ключевое слово function.
Кроме того, не указываются типы аргументов. Чтобы
вернуть из функции значение,
используем ключевое слово return.
Если вслед за return сразу
идет точка с запятой - получим аналог
функции типа
void из С, то есть никакое
значение не возвращается. Наконец, можно вовсе не писать
return. Словом, все как
обычно.
Определяем функцию "на
лету"
А вот второй способ
определить функцию. Он хотя
и выглядит несколько непривычно, зато предоставляет нам
более широкие возможности.
func_name = function (arg1,
arg2){
// здесь определяем тело функции
return ret_value; // возвращаем значение
}
То, что здесь написано,
полностью эквивалентно первому
определению. Но из этой новой записи мы можем
сделать кое-какие выводы. Во-первых, похоже, что
func_name - это что-то
вроде ссылки на нашу функцию.
Причем func_name является
полем того клипа, в Actions которого мы
определяем нашу функцию (так
оно и есть - и для первого варианта
определения, и для второго).
Наконец, такое впечатление, что во втором случае
ключевое слово function
играет роль оператора генерации нужной нам
функции. То есть вполне
возможно, что этот "оператор" мы сможем использовать в
любой удобный момент.
Определение "на лету"
для реализации стражей включения
И действительно, в
отличие от первого способа, второй можно применять
внутри многих других конструкций языка, что
предоставляет массу дополнительных возможностей
использования. В частности, вы можете реализовать что-то
вроде "условной трансляции" и "стражей
включения", которые применяются в заголовочных
файлах на C++. На самом деле, поскольку инструкция
#include работает в
процессе компиляции *.swf-файла, то на любой код,
стоящий вокруг нее, она внимания не обращает. "Зацикливание"
включаемых файлов приводит к генерации синтаксической
ошибки и избавиться от этого зацикливания мы сможем
только путем перекомпоновки включаемых файлов, никакие "условные
включения" здесь не помогут.
Однако есть другие
задачи, для которых в С++ часто используются директивы
препроцессора. Первая из них - чтобы все
функции определялись ровно
по разу. Во Флэше повторное
определение к ошибкам никогда не приводит. Но нам
может понадобиться переопределить какие-то
функции "вручную". Нужно
убедиться, что последующее включение подключаемого файла
не заменит наше новое определение
старым. Вот код для решения этой задачи. Во включаемом
файле inc1.as (расширение as является сокращением от
ActionScript) пишем
inc1_as = true;
_global.func = function(arg){
//code
}
При подключении
обрамляем оператор #include
условным оператором. При этом подключение произойдет в
любой случае, но выполнен этот код будет только если
выполнено условие проверки.
if (!inc1_as){
#include "inc1.as"
}
Вторая задача: иногда
надо в зависимости от некоторого условия использовать
тот или иной набор функций.
Вот довольно очевидный код примера.
В файле inc1.as пишем:
_global.func = function(arg){
//code1
}
В файле inc2.as пишем,
в свою очередь:
_global.func = function(arg){
//code2 - отличие здесь
}
И внутри нашего
флэш-ролика поступаем так:
if (!advancedMode){
#include "inc1.as"
}
else{
#include "inc2.as"
}
В обоих случаях только
второй способ определения функций
нам подойдет. Кроме того, только этим способом мы можем
поместить определяемые функции
в объект _global. То есть
нам не придется заново подключать файл с
функциями в каждом клипе,
что привело бы к лишним заботам и лишнему расходу
памяти.
Пример необычного
использования
Однако, говоря об
использовании оператора генерации
функции "во многих местах" мы имели в виду не
только блоки внутри управляющих конструкций. Их
действительно можно использовать в любом месте, где по
смыслу может стоять оператор, возвращающий тип
Function.
Различным вариантам такого использования посвящена вся
эта лекция. Чтобы вы сразу увидели, что мы понимаем под
"необычным" (для С++ и Java) использованием, взгляните
на такой код.
iter2 = function(f){
return function(x){ return f(f(x)) }
}
trace(iter2(function(y){return y*y*y;})(2));
Этот код выводит число
512. Здесь есть два заслуживающих внимания момента:
во-первых, мы написали функцию-генератор
второй итерации переданного в нее аргумента. Во-вторых,
этот аргумент мы создали "на лету", прямо внутри
оператора вызова функции-генератора.
Когда вы закончите читать эту лекцию, вы будете
полностью понимать все детали подобных трюков.
Локальные переменные
Локальные переменные нужны не только в языках
программирования общего назначения, но и во Флэш МХ.
Чтобы убедиться в этом, достаточно рассмотреть следующий
пример:
function progressionSum(low,
hi, step){
for (i=low; i<=hi; i+=step) sum+=i;
// Здесь нарочно не пишем var как будто бы мы не
знаем, что это
return sum;
}
// Используем нашу функцию
trace("sum = " + progressionSum(4, 10, 3));
//Вроде бы делаем то же самое
trace("sum = " + progressionSum(4, 10, 3));
Этот код выводит
следующее:
sum = 21
sum = 42
То есть программист,
решивший положиться на то, что Флэш сам заводит
переменную, если в нее хотят что-то записать (и при этом
делает ее равной нулю), должен хорошо представлять себе,
где именно Флэш эту переменную заведет. В данном
случае было заведено поле в том клипе, в котором
определена функция
progressionSum (и,
конечно, записанное в нем значение сохранилось до
следующего вызова). Так что
локальные переменные нужны. И для того чтобы
такую переменную создать, необходимо использовать
ключевое слово var. В
результате заводится обычная флэш-переменная, только
теперь объект, в котором она лежит - это
контекст вызова
функции. (Далее мы увидим,
что до этого объекта можно хитрым образом добраться
"снаружи" - но это далеко не типичное использование
локальной переменной . А
типичное, хотя оно и очевидно, на всякий случай описано
далее.)
Типичный пример
использования
Вот исправленный
пример функции
progressionSum:
function progressionSum(low,
hi, step){
var sum;
for (i=low; i<=hi; i+=step) sum+=i;
return sum;
}
trace("sum = " + progressionSum(4, 10, 3));
trace("sum = " + progressionSum(4, 10, 3));
На сей раз мы получаем
sum = 21
sum = 21
как того и следовало
ожидать. (В этом варианте функции
все еще есть одна погрешность, которую мы исправим
позднее. Можете пока ее поискать.)
Локальные переменные
бывают только в функциях
Однако не следует
думать, что с локальными
переменными все так же хорошо, как в С++.
Во-первых, в С++ на локальность переменных влияют
вложенные блоки (и переменная, заведенная внутри блока,
видна только внутри него). Этого свойства нет даже в
Java! Что уж говорить о Флэше. Но даже если мы объявим
локальную переменную просто
в кадре какого-либо клипа, мы на деле заведем в этом
клипе обычное поле (то есть ключевое слово
var будет попросту
проигнорировано). Точнее, все будет именно так, если код
в кадре выполняется обычным образом - в тот момент,
когда до этого кадра дошла очередь в линейке времени.
Если же код из кадра был вызван при помощи команды
Call (не путайте с методом
Function.call, о котором
далее), то переменная, объявленная с помощью
var все-таки будет
локальной. Но для нас
подобные ухищрения не имеют особого смысла, поскольку
гораздо проще и понятнее создать отдельную
функцию, чем вызывать код
из какого-то кадра командой Call.
Мы ведь уже говорили, что, в отличие от метода
Function.call, команда
Call - это типичный
атавизм.
Итак, смысл
локальной переменной во
Флэше - это переменная, видимая только внутри данной
функции. С другой стороны,
поскольку переменные во Флэше могут быть заведены
автоматически в момент использования - постарайтесь при
определении функций ставить
var везде, где только
возможно (чтобы не засорять пространство имен
объемлющего объекта). Например, в
функции progressionSum
из предыдущего примера все же была допущена ошибка, в
чем мы убедимся, запустив такой код:
for (i=0; i<4; i++)
trace("i = " + i + "; sum = " + progressionSum(i, 10, 3));
В результате мы
получим
i = 0; sum = 18
- всего одну строчку,
хотя ожидали получить четыре. Ошибка, разумеется, в том,
что в цикле for внутри
функции мы использовали то
же имя переменной, что и снаружи, а вот о том, чтобы
сделать переменную локальной,
не позаботились.
Так что следовало
написать:
function progressionSum(low,
hi, step){
var sum;
for (var i=low; i<=hi; i+=step) sum+=i;
return sum;
}
for (i=0; i<4; i++)
trace("i = " + i + "; sum = " + progressionSum(i, 10, 3));
и тогда мы получим
i = 0; sum = 18
i = 1; sum = 22
i = 2; sum = 15
i = 3; sum = 18
Хотелось бы, конечно,
сделать и внешнюю переменную i
локальной внутри своего
цикла, но, к сожалению, это невозможно.
Локальной внутри цикла
(блока) переменная во Флэш МХ не бывает. Только внутри
функции.
"Иерархия" локальных
переменных
Итак, область
видимости локальных переменных
не зависит от вложенных блоков. Но... зависит от
вложенных функций! Вот
пример:
i = 3;
function a(){
trace("function a: variable i = " + i);
var i=5;
trace("function a: variable i = " + i);
function b(){
trace("function b: variable i = " + i);
var i=7;
trace("function b: variable i = " + i);
}
trace("function a: variable i = " + i);
var i=6;
b();
trace("function a: variable i = " + i);
}
a();
Этот код выводит
следующее:
function a: variable i = 3
function a: variable i = 5
function a: variable i = 5
function b: variable i = 6
function b: variable i = 7
function a: variable i = 6
Обратите внимание, что
до тех пор, пока в функции
b() не была объявлена
собственная переменная i,
использовалась переменная из
функции a(), то
есть из того контекста, где
функция
b() была определена. (Но
значение этой переменной бралось, разумеется, то,
которое было на момент вызова, а не на момент
определения.) Если бы мы
определили функцию
b() снаружи, то она, так
же, как и a(),
использовала бы "внешнюю" переменную
i, несмотря на то, что
вызывалась бы внутри a().
Действительно, код
i = 3;
function b(){
trace("function b: variable i = " + i);
var i=7;
trace("function b: variable i = " + i);
}
function a(){
trace("function a: variable i = " + i);
var i=5;
trace("function a: variable i = " + i);
// Раньше здесь было определение b()
trace("function a: variable i = " + i);
var i=6;
b();
trace("function a: variable i = " + i);
}
a();
выводит
function a: variable i = 3
function a: variable i = 5
function a: variable i = 5
function b:
variable i = 3
function b: variable i = 7
function a: variable i = 6
(Полужирным курсивом
выделена строчка, в которой видно отличие от предыдущего
примера.) По этой же причине попытка заменить большое
количество похожих вызовов trace
вызовом специальной функции
в данном случае не сработает.
В самом деле, код
function traceI(funcName){
trace("function " + funcName + ": variable i = " + i);
}
i = 3;
function a(){
traceI("a");
var i=5;
traceI("a");
function b(){
traceI("b");
var i=7;
traceI("b");
}
traceI("a");
var i=6;
b();
traceI("a");
}
a();
и вовсе выводит
function a: variable i = 3
function a: variable i = 3
function a: variable i = 3
function b: variable i = 3
function b: variable i = 3
function a: variable i = 3
- очевидно, совсем не
то, что мы хотели видеть. Но поделать ничего нельзя:
контексты, в которых
существуют локальные переменные
i в фукнции
a() и тем более
b(), для
функции
traceI(funcName),
определенной "снаружи", недоступны. Она видит только
поле i того объекта, в
котором помещен наш код.
Наконец, отметим, что
и сама функция
b() является своего рода "локальной".
И, разумеется, может быть аналогичная "иерархия"
локальных функций. В
следующем параграфе мы подробнее поясним, что здесь
имеется в виду.
"Иерархия" локальных
функций
Как обычно, начинаем с
примера:
a();
function a(){
trace("function a called");
function b(){
trace("function b: first incarnation");
}
b();
function c(){
trace("function c called")
b();
function b(){
trace("function b: second incarnation");
}
b();
}
c();
b();
trace("--------------");
}
a();
b(); //Ничего не делает
Вас может слегка
удивить, что первый раз функция
а() вызвана до своего
определения. Однако
посмотрим на результат. Этот код выводит:
function a called
function b: first incarnation
function c called
function b: second incarnation
function b: second incarnation
function b: first incarnation
--------------
function a called
function b: first incarnation
function c called
function b: second incarnation
function b: second incarnation
function b: first incarnation
--------------
Мы видим, что и первый
вызов функции
а() замечательно сработал!
Также мы видим, что внутри функции
с() всегда вызывается
вторая инкарнация функции
b(), а снаружи - первая.
(Снаружи функции
а() попытка вызвать
b() вовсе не дает никакого
результата). Таким образом, то, какая версия
функции
b() будет вызываться
внутри другой функции (в
данном случае - внутри функций
а() и
с()) решается на этапе
компиляции. Мы можем это представлять себе так:
компилятор находит в коде все вызовы
функций и затем ищет в
подходящем контексте
определения этих функций
(неважно, до или после); затем генерирует безусловные
переходы на тело функции и
обратно. Можно, напротив, представлять себе, что сначала
компилируются все функции
(для локальных функций
получается рекурсия),
генерируется нечто вроде таблицы адресов, а затем уже,
если компилятор находит вызовы этих
функций, он генерирует
безусловные переходы. Все это наводит на мысль, что идея
продемонстрировать на примере
функций то же поведение, что и для
локальных переменных (а
именно: пока не объявим переменную - используется
переменная из вышележащего
контекста; как только объявили - новая ее
перекрывает, но только в пределах внутренней
функции), обречена на
провал. Однако вспомним, что у нас ест ь еще и способ "определения
функций на лету". Посмотрим, что он дает: код
a();
function a(){
trace("function a called");
function b(){
trace("function b: first incarnation");
}
b();
function c(){
trace("function c called")
b();
b = function(){
trace("function b: second incarnation");
}
b();
}
c();
b();
trace("--------------");
}
a();
b(); //Ничего не делает по-прежнему
дает на выходе
function a called
function b: first incarnation
function c called
function b: first incarnation
function b: second incarnation
function b: second incarnation
--------------
function a called
function b: first incarnation
function c called
function b: first incarnation
function b: second incarnation
function b: second incarnation
--------------
Вот так раз! Внутри
функции
с() мы добились того, что
хотели - до определения второй инкарнации
b() была вызвана первая.
Но при этом эта первая инкарнация, определенная в
вышележащей функции
а(), была испорчена!
(Заметим также, что снаружи
функции а() вызвать
b() по-прежнему не
удается. Этот факт - обратная сторона того, что новая
реализация записалась поверх старой и ее испортила.
Старая-то была локальной! Если бы старой реализации не
было вовсе - вот тогда запись b =
function() означала бы создание новой переменной
текущего клипа, содержащей в себе
ссылку на функцию).
И только если мы
сделаем вторую инкарнацию b()
по-настоящему локальной с помощью ключевого слова
var (см. код, здесь это
слово выделено жирным шрифтом):
a();
function a(){
trace("function a called");
function b(){
trace("function b: first incarnation");
}
b();
function c(){
trace("function c called")
b();
var b = function(){
trace("function b: second incarnation");
}
b();
}
c();
b();
trace("--------------");
}
a();
b(); //Ничего не делает по-прежнему
то наконец-то получим
function a called
function b: first incarnation
function c called
function b: first incarnation
function b: second incarnation
function b: first incarnation
--------------
function a called
function b: first incarnation
function c called
function b: first incarnation
function b: second incarnation
function b: first incarnation
Итак, нам удалось
сделать полностью локальные
функции, да еще и определяемые в нужный момент.
Однако, чтобы объяснить, как же это на самом деле
у нас получилось, придется ввести понятие
функции как объекта. Этому
и посвящен следующий параграф.
Функция как объект
Вы наверняка помните,
что среди небольшого количества типов данных,
поддерживаемых во Флэш МХ, имеется тип
Function.
А это значит, что мы можем создавать объекты этого типа
и переменные, ссылающиеся на эти объекты. Наконец,
каждый тип данных во Флэш МХ предназначен для того,
чтобы с ним особым образом работали некоторые операторы
- для функционального типа это круглые скобки, оператор
вызова функции. Сейчас мы
упорядочим наши знания о способах
определения функций, используя этот взгляд на
вещи.
Разные ссылки на одну
функцию
Итак, в некоторой
переменной может лежать ссылка
на объект-функцию и круглые
скобки можно использовать для вызова этой
функции.
Подтвердить такую
точку зрения может использование нескольких
ссылок на одну функцию. Вот
как это делается:
var a = function(){
trace("function a called");
}
var b = a;
a();
b();
(Переменные, в которых
хранятся ссылки на функции,
мы сделали локальными на
всякий случай: когда есть вероятность, что какой-то
кусок кода будет потом помещен в
функцию, будет нехорошо, если эта
функция станет плодить
вокруг себя массу переменных, а то и портить уже
существующие. Так что хорошим стилем будет писать
var всегда, когда это не
противоречит вашим намерениям.)
Этот код, как и
следовало ожидать, выводит:
function a called
function a called
Здесь мы определяли
функцию "на лету". Сейчас
мы можем догадаться, что такой способ
определения всего лишь
приводит к генерации временной
ссылки на функцию, которую мы затем записываем в
именованную переменную (а можем, например, передать в
другую функцию в качестве
аргумента). Это наводит на мысль, что и на
функции, определенные
заранее, тоже можно делать альтернативные
ссылки.
var d = c;
d();
function c(){
trace("function c called");
}
Этот код, хотя и не
является интуитивно понятным, все же выводит именно
function с called
Мы еще раз убеждаемся
в том, что функции,
определенные заранее, действительно определяются перед
выполнением всего остального кода.
Передача функций в
качестве аргумента
Про эту красивую
возможность мы не раз уже говорили. Пора, наконец-то,
привести простой и наглядный пример.
var d = ["a", "b", "c", "d"];
iterate(d, doubleString);
trace(d.join(", "));
iterate(d, capitalize);
trace(d.join(", "));
function iterate(arr, func){
for (var i=0; i<arr.length; i++) arr[i] = func(arr[i]);
}
function doubleString(str){
return str+str;
}
function capitalize(str){
return str.toUpperCase();
}
Этот код выводит
aa, bb, cc, dd
AA, BB, CC, DD
В этом примере, кроме
передачи функции как
аргумента, мы также в очередной раз продемонстрировали
особенности определения функций
заранее. Этот прием имеет ряд преимуществ:
например, если сущность функций
очевидна, то код, делающий основную работу можно
поместить в начале экрана. Однако
определение "на лету" имеет более важные
преимущества при использовании во включаемых файлах и
при помещении функций в
объект _global (откуда
они, напомним, доступны для любого
_level, то есть всем
загруженным *.swf-роликам. Причем, если нет конфликта
имен, доступны без явного указания
_global). Поэтому мы, как
правило, будем использовать именно
определение "на лету".
Возвращаемое значение
типа Function. Куда указывает this
Мы уже видели, что
любая ссылка может указывать на
функцию. В частности - ссылка, возвращаемая
другой функцией в операторе
return. Получив эту
ссылку, мы можем немедленно вызвать ту
функцию, которую нам
вернули (получается забавный синтаксис со стоящими
подряд парами открывающих и закрывающих фигурных
скобок). А можем присвоить это значение какой-нибудь
переменной. Интересно также рассмотреть разницу между
тем, куда указывает в функциях,
сгенерированных внутри других
функций, ссылка
this. Вот пример
кода, который позволит ответить на наши вопросы.
t1 = "Снаружи функции";
function test(arg){
var t1 = "Внутри функции";
var func = function(){
trace("this.arg = " + this.arg);
trace("this.t1 = " + this.t1);
trace("t1 = " + t1);
}
trace("---- Вызываем func() ------");
func();
trace("------------------");
return func;
}
trace("======= Вызываем test()() ==============");
test()();
trace("");
trace("======= Вызываем а = test(); a() =======");
a = test("argString");
a();
На выходе получаем:
======= Вызываем test()()
==============
---- Вызываем func() ------
this.arg =
this.t1 = Внутри функции
t1 = Внутри функции
------------------
this.arg =
this.t1 =
t1 = Внутри функции
======= Вызываем а = test(); a() =======
---- Вызываем func() ------
this.arg = argString
this.t1 = Внутри функции
t1 = Внутри функции
------------------
this.arg =
this.t1 = Снаружи функции
t1 = Внутри функции
Выводы отсюда можно
сделать следующие.
- Возвращать из
функции другую
функцию можно.
-
this
внутри функции
действительно всегда указывает на тот объект, к
которому принадлежит ссылка,
через которую вызвана функция.
- Если ссылка
является локальной переменной
другой (генерирующей нашу)
функции - она принадлежит объекту,
описывающему контекст
вызова функции. В этом
объекте лежат все локальные
переменные и аргументы. (Но не путайте это с
объектом
arguments, о
котором речь пойдет дальше:
arguments, - это массив и аргументы лежат в
нем по номерам). На этот-то объект, описывающий
контекст вызова, и
указывает
this в
сгенерированной функции
(если ссылка на эту функцию
сохранена в локальной
переменной).
- Наконец, если
функция вызывается
немедленно по получении ссылки
на нее (в нашем примере это делает оператор
test()();), то
ссылка эта, видимо,
хранится в каком-то временном объекте. Во всяком
случае, это и не внешний объект и не
контекст вызова
test, поскольку и в
том и в другом мы заводили переменную
t1; а результат вызова
test()(); говорит об
отсутствии этой переменной в обсуждаемом объекте.
Конструирование
функции в момент передачи
Прием конструирования
при передаче особенно удобен при сортировке массивов (мы
его даже уже применяли, когда рассказывали о массивах):
var arr = ["aaaaaaaa",
"bbb", "cc", "dddd", "e", "gg"];
arr.sort(
function(str1, str2){
return (str1.length > str2.length) ?
1 : ((str1.length ==
str2.length) ? 0 : -1);
}
);
trace(arr.join(", "));
В результате получаем:
e, cc, gg, bbb, dddd, aaaaaaaa
то есть мы
отсортировали массив по возрастанию длины строк. В этом
примере привлекает то, что вся информация сосредоточена
в одном месте: прямо в вызове метода
sort мы и указываем, как
именно надо сортировать.
"Опасная" рекурсия
Сейчас мы покажем вам
пример, из которого следует, насколько нужно быть
осторожным при копировании функции.
В нем мы заводим у объекта а
метод, вычисляющий сумму прогрессии по рекурсивному
алгоритму, а потом пытаемся скопировать этот метод в
другой объект.
a = {};
a.recProgressionSum = function(n){
if (n < 1) return n;
return n + this.recProgressionSum(n - 1);
}
trace("from a: " + a.recProgressionSum(5));
trace("from a: " + a.recProgressionSum(5.5));
b = {};
b.recFunction = a.recProgressionSum;
trace("from b: " + b.recFunction(5));
trace("from b: " + b.recFunction(5.5));
В результате
выполнения этого кода мы получим следующее:
from a: 15
from a: 18
from b: 5
from b: 5.5
Причина такого конфуза
становится ясной после некоторых размышлений. Ведь в
объекте b нет
функции
recProgressionSum, к
которой имеется рекурсивное обращение. Идея явно указать
объект, в котором эта функция
есть, будет приносить нам плоды ровно до тех пор, пока
мы не уничтожим переменную а
(или просто не запишем туда что-нибудь другое):
a = {};
a.recProgressionSum = function(n){
if (n < 1) return n;
return n + a.recProgressionSum(n - 1);
}
trace("from a: " + a.recProgressionSum(5));
trace("from a: " + a.recProgressionSum(5.5));
b = {};
b.recFunction = a.recProgressionSum;
trace("from b: " + b.recFunction(5));
trace("from b: " + b.recFunction(5.5));
a = {dddd: 1};
trace("from b: " + b.recFunction(5));
trace("from b: " + b.recFunction(5.5));
Этот код выводит
from a: 15
from a: 18
from b: 15
from b: 18
from b: 5
from b: 5.5
О том, как справиться
с подобной бедой, мы узнаем в следующем параграфе.
Объект arguments
Программисты, пишущие
на С (а уж тем более - на ассемблере) хорошо знакомы с
так называемым стеком вызовов. Под этот стек отводится
специальная область памяти, в которой указываются адреса
вызывающих и вызываемых функций,
а также передаваемые в них аргументы. Если есть желание
работать на низком уровне, то покопавшись в стеке, можно
выяснить, кто вызвал ту функцию,
внутри которой мы в данный момент находимся, какие
аргументы ей передал, у этой вызвавшей
функции тоже определить
аргументы и вызывающую функцию
и т.д. Это позволяет делать разные трюки, самым
распространенным из которых является
функция с переменным числом
аргументов. Оказывается, Флэш-программисты в этом
отношении ничуть не обделены, и у них тоже есть
возможность получить всю подобную информацию. И для
этого существует специальный объект по имени
arguments,
который определен внутри каждой
функции.
Сущность объекта
arguments
Объект
arguments - это массив, содержащий в себе
все аргументы функции (не
формальные параметры, а именно те, что пользователь
передал в нее в данном вызове). Как любой массив,
arguments
имеет поле length, в
котором записана его длина. Кроме того,
arguments
имеет еще два поля: caller
и callee, через которые
можно получить доступ к объектам вызывающей и вызываемой
функций. Подробнее об этом
мы расскажем чуть ниже. (Этот рассказ будет тем более
полезен, что не вся информация в справочной документации
Флэш МХ соответствует действительности.) А теперь
попробуем воспользоваться полученными сведениями об
объекте
arguments для того, чтобы сделать
функцию с произвольным
числом аргументов.
Функции с произвольным
числом аргументов
Вот пример такой
функции, в которую в
качестве первого аргумента передается строка-"клей", а
все последующие аргументы объединены в одну строку, при
этом "клей" используется в качестве разделителя.
_global.joinBy =
function(glue){
var tmpString = ((arguments[1] != undefined) ? arguments[1] :
"");
for (var i = 2; i<arguments.length; i++){
tmpString += glue + arguments[i];
}
return tmpString;
}
trace("result = " + joinBy("___"));
trace("result = " + joinBy("___", "r"));
trace("result = " + joinBy("___", "E", "R", "T",
"G"));
Выводит этот код
следующие бессмертные строки:
result =
result = r
result = E___R___T___G
Впрочем, то же самое
можно сделать и чуть покороче (хотя, возможно, и менее
наглядно):
_global.joinBy1 =
function(glue){
if (arguments.length == 2) return arguments[1];
var tmpString = arguments.join(glue);
return tmpString.substr(tmpString.indexOf(glue) +
glue.length);
}
trace("result = " + joinBy1("___"));
trace("result = " + joinBy1("___", "r"));
trace("result = " + joinBy1("___", "E", "R", "T",
"G"));
Результат получается
тот же самый.
Обратите внимание, что
arguments
- это самый настоящий массив, у которого, в частности,
работает метод join. Хотя
по этому поводу и могут возникнуть небольшие сомнения,
когда Флэш МХ автоматически подсказывает нам поля и
методы, которые можно выбрать у объекта
arguments
- в этом списке только length,
caller и
callee. Но сомнения
напрасны, все методы, относящиеся к массивам, прекрасно
работают и с массивом
arguments. Его можно
даже отсортировать, хотя не так уж просто представить
себе задачу, в которой это было бы необходимо.
caller и callee
Как мы уже писали,
поля caller и
callee позволяют получить
доступ к объектам вызывающей и вызываемой
функций. В справочной
документации по Флэш МХ написано, правда, по-другому.
Там сказано, что, в то время как поле
callee действительно дает
доступ к объекту вызываемой
функции (то есть именно той, внутри которой мы и
получаем доступ к объекту
arguments), поле
caller дает доступ к
объекту
arguments вызывающей
функции. Если бы все было именно так, это было бы
хорошо. Мы имели бы возможность получить доступ к
полному списку аргументов вызывающей
функции, сам объект
вызывающей функции надо
было бы получать через
arguments.caller.callee, зато можно было бы п
олностью раскрутить стек вызовов (arguments.caller.caller.caller
и т.д.). К сожалению, дела обстоят вовсе не так радужно,
как утверждает документация. И
arguments.caller
указывает именно на объект вызывающей
функции, добраться же до
объекта
arguments вызывающей
функции мы не сможем никак (если только нам его
не передадут специально в качестве аргумента вызываемой
функции). Таким образом,
область применения поля caller
не очень широка. В следующих лекциях мы приведем пример
использовании caller в
одной из специфических форм наследования. А пока что -
вот пример использования caller
для отладочных целей. Если мы заранее позаботимся о
снабжении каждой функции
полем name, то отладочная
функция сможет выводить эту
информацию в консоль:
_global.traceDebugInfo =
function(){
trace("---------------");
trace("function: " + arguments.caller.name);
trace("a = " + a);
trace("---------------");
}
_global.doSomething = function(){
a++;
traceDebugInfo();
}
_global.doSomething.name = "doSomething";
_global.doAnything = function(){
a+=4;
traceDebugInfo();
}
_global.doAnything.name = "doAnything";
a = 3;
traceDebugInfo();
doAnything();
doSomething();
doAnything();
Приведенный здесь код
выводит
---------------
function:
a = 3
---------------
---------------
function: doAnything
a = 7
---------------
---------------
function: doSomething
a = 8
---------------
---------------
function: doAnything
a = 12
---------------
Обратите внимание, что
мы можем вызвать нашу функцию
traceDebugInfo() в любом
месте - не только в специально приспособленной
функции. (В нашей программе
она была вызвана "снаружи" всех
функций в самом начале). В этом случае мы просто
не получаем информации о вызывающей
функции, но все прочие
операции по выводу отладочных сообщений нормально
работают. Так что отладочные
функции подобного типа - это неплохая идея, даже
если вы не всегда снабжаете прочие ваши
функции отладочными полями
вроде name.
"Безопасная" рекурсия
Область применения
поля callee, в отличие от
caller, достаточно широка.
Сейчас мы продемонстрируем, как "опасная
рекурсия", описанная в
прошлом параграфе, с помощью
callee превращается в "безопасную". Для
разнообразия будет использована другая рекурсивная
функция, вычисляющая
специальный вид факториала, обозначаемый обычно двумя
восклицательными знаками. (Напомним, что
n!! - это произведение
всех натуральных чисел той же четности, что и
n, которые меньше или
равны n).
a = {};
a.factEvenOdd = function(n){
return n > 2 ? n*arguments.callee(n - 2) : n;
}
trace("7!! = " + a.factEvenOdd(7));
b = {};
b.specialFact = a.factEvenOdd;
a.factEvenOdd = function(){return "function is
obsolete"};
trace("8!! = " + a.factEvenOdd(8));
trace("8!! = " + b.specialFact(8));
Код, который здесь
написан, выводит в консоль следующее:
7!! = 105
8!! = function is obsolete
8!! = 384
Итак, использование
arguments.callee позволило нам копировать
рекурсивную функцию в
другой объект под новым именем и не заботиться о том,
что произойдет в дальнейшем с исходной
функцией.
Методы apply и call
Объект типа
function
имеет два интересных метода:
apply и
call.
Оба они служат одной цели: вызвать
функцию так, как будто она является методом
объекта, передаваемого в первом аргументе
apply
или call.
Аргументы самой функции
передаются в следующих аргументах
apply и
call
либо в массиве (для
apply), либо подряд
через запятую (для
call). Внутри же
самой функции при ее таком
вызове мы как бы "ничего не заметим" - за исключением
того, что ссылка
this будет указывать
на первый аргумент
apply или
call.
Привести примеры полезного использования этих методов мы
пока не можем; методы эти нужны в основном при работе с
наследованием, так что нужно подождать до
соответствующей лекции (а самые информативные примеры
будут приведены в лекции о множественном наследовании).
Тем не менее, упомянем об использовании объекта
arguments
совместно с этими методами.
Итак, иногда бывает
нужно сделать функцию,
которая (в числе других своих действий) вызывает
заданную функцию для
заданного объекта. Собственно говоря, для этого
существуют целых два механизма - один более общий, но
неудобный, и поэтому устаревший. И пришел он еще из Флэш
5. Во Flash 5 проблема с вызовом
функции, не являющейся методом класса в таком
контексте, где она им
должна быть, решалась так. В нужном объекте просто
заводили ссылку на нужную функцию.
После чего можно было считать, что у объекта нужный
метод появился. Затем ссылку
удаляли:
a = {};
func = function(text){trace(text + this.field);}
a.field = 5;
a.f1 = func;
a.f1("a = ");
delete a.f1;
Конечно, это
громоздкий способ сделать простую вещь. И во Флэш МХ
появился другой, гораздо более простой и элегантный. Он
основан как раз на использовании метода
call
объекта Function (не
путайте со встроенной устаревшей процедурой
call). С помощью этого
метода предыдущий пример можно переписать так:
a = {};
func = function(){trace(this.field);}
a.field = 5;
func.call(a, "a = "); // Применяем функцию к объекту
а
Как мы видим, на
первом месте стоит объект, к которому применяем
функцию, на втором -
аргументы, перечисленные по порядку. Иногда еще удобнее
применять метод
apply, в который,
кроме объекта, в качестве метода которого вызывается
функция, передается массив
аргументов. Очевидно, аргументы вызываемой
функции придется
"вытаскивать" из массива аргументов вызывающей (а ведь
мы частенько не будем знать заранее, сколько аргументов
будет у вызываемой функции).
Вот самый удобный способ решать такие проблемы:
obj1 = {dataStr: "Строка из
первого объекта"};
obj2 = {dataStr: "Строка из второго объекта"};
_global.applyToAnother = function(func, obj, args){
trace("Вызвана функция applyToAnother");
// Отрезаем конец массива аргументов - берем все,
// кроме первых двух
return func.apply(obj, arguments.slice(2));
}
_global.traceDataStr = function(begStr, endStr){
trace(begStr);
trace(this.dataStr);
trace(endStr);
}
traceDataStr("Начало: объект _global", "Конец:
объект _global");
trace("--------------");
applyToAnother(traceDataStr, obj1, "Начало: первый
объект",
"Конец: первый объект");
trace("--------------");
applyToAnother(traceDataStr, obj2, "Начало: второй
объект",
"Конец: второй объект");
Этот код выводит:
Начало: объект _global
undefined
Конец: объект _global
--------------
Вызвана функция applyToAnother
Начало: первый объект
Строка из первого объекта
Конец: первый объект
--------------
Вызвана функция applyToAnother
Начало: второй объект
Строка из второго объекта
Конец: второй объект
Примеры более
осмысленного использования методов
call
и apply
вы, как и было обещано, найдете в лекции о наследовании
(параграф про вызов функции
с явным указанием базового класса).
Параметризованные
функции
Чем параметр
функции отличается от ее
аргумента? Ведь если от параметра зависит возвращаемое
функцией значение (или
побочные эффекты ее работы) - казалось бы, его тоже
можно передать в качестве аргумента и дело с концом. Так
оно, собственно, и есть - в тех случаях, когда код,
вызывающий функцию,
находится под вашим контролем. Если же вам нужно
передать в чужой код (или даже в свой, но находящийся в
другом, вполне законченном модуле)
функцию определенной сигнатуры (то есть
принимающую и возвращающую аргументы заранее заданных
типов и в заранее заданном количестве), то все, что в
эту сигнатуру не входит, как раз удобно передать в
параметрах. Простейший пример: у вас есть модуль,
строящий графики функций.
Очевидно, в него следует передавать
функцию, принимающую
действительное число и возвращающую также действительное
число. Если же вы хотите нарисовать несколько полиномов
(например, четвертой степени, то есть имеющие пять
коэффициентов), то вполне разумно будет написать код
функции, вычисляющей
полином один раз, а значения коэффициентов передавать
через параметры. Другой пример - создание множества
обработчиков событий - будет приведен в этом параграфе.
Задача: обработчики
событий
Рассмотрим задачу
создания множества похожих обработчиков событий для
семейства родственных контролов. Поскольку мы пока не
имеем необходимых знаний о настоящих Флэш-контролах, то
создадим сейчас несколько объектов, ведущих себя как
контролы (по крайней мере, с точки зрения клиентского
кода). Дополнительное неудобство заключается в том, что
и с тем, как делаются на Флэше классы, мы пока также не
сталкивались. Тем не менее, все эти трудности
преодолимы. И в результате их преодоления может
родиться, например, вот такой код:
controlsNumber = 5;
for(var i = 1; i<=controlsNumber; i++){
set(
"control" + i,
{
name: "control" + i,
curValue: 2*i+1,
oldValue: 3*i,
setValue: function(newValue){
trace(this.name + ".setValue(" + newValue + ")
called");
this.oldValue
= this.curValue;
this.curValue
= newValue;
this.raiseEvent();
},
eventReactionFunction: null,
subscribe: function(func){
trace(this.name + ".subscribe() called");
this.eventReactionFunction = func;
},
raiseEvent: function(){
trace(this.name + ".raiseEvent() called");
this.eventReactionFunction(this.curValue);
}
}
);
}
// Здесь начинается "клиентский" код
_global.reactionFunction = function(){
trace("reaction");
}
for(var i = 1; i<=controlsNumber; i++){
eval("control" + i).subscribe(reactionFunction);
}
trace("------------------");
for(var i = 1; i<=controlsNumber; i++){
eval("control" + i).setValue(4*i + 1);
}
Как мы видим,
сгенерирован ряд объектов с именами, начинающимися со
слова control. Эти объекты
имеют функцию
subscribe, сохраняющую
функцию реакции, которую
нужно вызвать в том случае, если в контроле произошло
событие. В данном случае применена упрощенная схема,
когда такая функция реакции
может быть только одна. Устанавливаем значения контролов
мы снаружи, при этом и генерируется событие (вызывается
функция
raiseEvent).
В результате запуска
этого кода на выполнение мы получим:
control1.subscribe() called
control2.subscribe() called
control3.subscribe() called
control4.subscribe() called
control5.subscribe() called
------------------
control1.setValue(5) called
control1.raiseEvent() called
reaction
control2.setValue(9) called
control2.raiseEvent() called
reaction
control3.setValue(13) called
control3.raiseEvent() called
reaction
control4.setValue(17) called
control4.raiseEvent() called
reaction
control5.setValue(21) called
control5.raiseEvent() called
reaction
Пока что реакция на
события от всех контролов одинакова. И мы никак не можем
ее изменить, не подписывая в качестве
функции реакции на события
в данном контроле совсем другую
функцию. Здесь наглядно видно, что дополнительные
аргументы функции
reactionFunction дела не
решат: функция
raiseEvent() передает в
функцию реакции только один
аргумент, а код raiseEvent()
мы считаем не подлежащим изменениям. Попробуем
выкручиваться с помощью параметров.
Прикрепление параметра
к функции и доступ к параметру
Сначала сформулируем
задачу до конца. Предположим, что
функция реакции должна печатать разницу между
новым и старым значением, записанными в контроле. Однако
raiseEvent() передает нам
только текущее значение curValue.
Мы могли бы воспользоваться тем, что приватных полей во
Флэш МХ нет, поэтому функция
реакции может напрямую обратиться к контролу и прочитать
значение поля oldValue.
Однако для этого надо иметь ссылку на нужный контрол
(или по крайней мере его имя). Попробуем сделать это вот
так:
for(var i = 1;
i<=controlsNumber; i++){
var curControl = eval("control" + i);
reactionFunction.control = curControl;
curControl subscribe(reactionFunction);
// Тестируем:
curControl.setValue(4*i+3);
}
На первый взгляд все
неплохо. Теперь нужно информацию из параметра извлечь в
самой функции реакции.
Делаем это с использованием
arguments.callee:
_global.reactionFunction =
function(curValue){
var control = arguments.callee.control;
var result = curValue - control.oldValue;
trace("reaction: difference = " + result);
}
В результате полный
текст "клиентского" кода будет выглядеть так:
_global.reactionFunction =
function(curValue){
var control = arguments.callee.control;
var result = curValue - control.oldValue;
trace("reaction: difference = " + result);
}
for(var i = 1; i<=controlsNumber; i++){
var curControl = eval("control" + i);
reactionFunction.control = curControl;
curControl.subscribe(reactionFunction);
// Тестируем:
curControl.setValue(4*i+1);
}
trace("------------------");
for(var i = 1; i<=controlsNumber; i++){
eval("control" + i).setValue(4*i + 1);
}
Запускаем исправленный
код и получаем:
control1.subscribe() called
control1.setValue(5) called
control1.raiseEvent() called
reaction: difference = 2
control2.subscribe() called
control2.setValue(9) called
control2.raiseEvent() called
reaction: difference = 4
control3.subscribe() called
control3.setValue(13) called
control3.raiseEvent() called
reaction: difference = 6
control4.subscribe() called
control4.setValue(17) called
control4.raiseEvent() called
reaction: difference = 8
control5.subscribe() called
control5.setValue(21) called
control5.raiseEvent() called
reaction: difference = 10
------------------
control1.setValue(5) called
control1.raiseEvent() called
reaction: difference = -6
control2.setValue(9) called
control2.raiseEvent() called
reaction: difference = -2
control3.setValue(13) called
control3.raiseEvent() called
reaction: difference = 2
control4.setValue(17) called
control4.raiseEvent() called
reaction: difference = 6
control5.setValue(21) called
control5.raiseEvent() called
reaction: difference = 0
Сначала вроде бы все
хорошо: значения контролов были установлены в
2*i+1, а мы ставим их в
4*i+1, так что разница
должна составить 2*i, что
мы и видим (она равна для контролов с первого по пятый
четным числам от 2 до 10). Однако в самом конце у нас
стоит еще один цикл установки параметров контролов - мы
не стали его убирать. И правильно сделали, ибо в нем-то
и проявляются наши ошибки. Несмотря на то что мы опять
устанавливаем каждому контролу значение
4*i+1, то есть разница со
старыми значениями должна быть 0 для всех контролов, на
деле мы получаем иное. Возможно, вы с самого начала
увидели в чем состоит ошибка - тем не менее, мы хотели
продемонстрировать наглядно тот факт, что объект
функции реакции у нас
только один, так что одновременно установить ему пять
разных значений параметров у нас никак не получится. Вот
и вышло, что во втором цикле значение поля
control этого объекта
каждый раз было ссылкой на
control5. Вывод: нам нужны пять разных объектов
функции.
Перегенерация функций
для установки параметров
В языке AсtionScript
есть всего четыре способа создавать новые объекты (не
считая примитивные). Это оператор
new, с которым мы будем иметь дело далее при
создании объектов классов. Это команды краткого создания
объектов и массивов с помощью фигурных и квадратных
скобок соответственно. И, наконец, это оператор
function, создающий новую
функцию. Хотя компилятор,
создающий *.swf-файл, проверяет синтаксис и, видимо,
определяет, что в данном месте создается
функция (а также переводит
код этой функции в
применяемую в *.swf-файлах форму), сам объект
функции создается каждый
раз заново, когда интерпретатор *.swf-кода встречает
ключевое слово function,
используемое как оператор. То есть
определение функции "на лету" создает новый
объект каждый раз, когда этот код встречается инт
ерпретатору. А это как раз то, что нужно для наших
целей. Перенесем код создания
функции реакции на сообщение в цикл. Тогда
"клиентская" часть нашего примера будет выглядеть так:
for(var i = 1;
i<=controlsNumber; i++){
var curControl = eval("control" + i);
var reactionFunction = function(curValue){
var control = arguments.callee.control;
var result = curValue - control.oldValue;
trace("reaction: difference = " + result);
}
reactionFunction.control = curControl;
curControl.subscribe(reactionFunction);
// Тестируем:
curControl.setValue(4*i+1);
}
trace("------------------");
for(var i = 1; i<=controlsNumber; i++){
eval("control" + i).setValue(4*i + 1);
}
На выходе мы на сей
раз получим:
control1.subscribe() called
control1.setValue(5) called
control1.raiseEvent() called
reaction: difference = 2
control2.subscribe() called
control2.setValue(9) called
control2.raiseEvent() called
reaction: difference = 4
control3.subscribe() called
control3.setValue(13) called
control3.raiseEvent() called
reaction: difference = 6
control4.subscribe() called
control4.setValue(17) called
control4.raiseEvent() called
reaction: difference = 8
control5.subscribe() called
control5.setValue(21) called
control5.raiseEvent() called
reaction: difference = 10
------------------
control1.setValue(5) called
control1.raiseEvent() called
reaction: difference = 0
control2.setValue(9) called
control2.raiseEvent() called
reaction: difference = 0
control3.setValue(13) called
control3.raiseEvent() called
reaction: difference = 0
control4.setValue(17) called
control4.raiseEvent() called
reaction: difference = 0
control5.setValue(21) called
control5.raiseEvent() called
reaction: difference = 0
Теперь все в порядке:
мы устанавливаем в каждый контрол то значение, которое
там и было, и в качестве разницы между старым и новым
числами действительно получаем ноль.
Обратите еще раз
внимание на то, что внутри цикла мы везде, где можно,
поставили ключевое слово var.
Казалось бы, до тех пор, пока оно не заключено в
функцию, смысла это слово
не имеет. Но в том-то и дело, что любой код рано или
поздно мы можем захотеть вынести в отдельную
функцию и вызывать его
потом из разных мест. Потому и писать мы стараемся так,
чтобы перенос написанного в
функцию впоследствии не вызвал затруднений.
Принцип сохранения
контекста
В предыдущем параграфе
перед нами стояла проблема: "пометить" каким-то образом
разные экземпляры одной функции,
чтобы они в результате работали с разными объектами. В
качестве места, где можно расположить индивидуальную
информацию, связанную с данной копией
функции, мы выбрали объект,
представляющий эту функцию.
Казалось бы, какие еще варианты могут быть? И тем не
менее, эти дополнительные варианты существуют. И сейчас
мы с ними познакомимся.
Возвращаем функцию из
другой функции
Рассмотрим такой
пример.
genFunc = function(str){
return function(quantity){
var tempStr;
for (var i=0; i<quantity; i++) tempStr += str;
return tempStr;
}
}
func = genFunc("_carriage_");
anotherFunc = genFunc("_truck_");
trace(func(3));
trace(anotherFunc(4));
Запустив его, мы
получим строчки
_carriage__carriage__carriage_
_truck__truck__truck__truck_
(небольшой состав из
трех вагонов, а за ним - из четырех грузовиков).
Получается, что с
помощью функции
genFunc мы сгенерировали
другую функцию
func, которая занимается
тем, что выводит строку, указанную нами при ее
генерации, заданное количество раз. (А потом то же самое
проделали еще раз, получив функцию
anotherFunc).
Зачем такое может
понадобиться? Первое, что приходит в голову: для
реализации функций реакции
на события, с которыми мы имели дело в прошлом
параграфе. Если та функция,
что вызовет нашу функцию
реакции, передает ей лишь число, указывающее, сколько
раз нужно скопировать исходную строку - то сама строчка
для копирования может быть задана в качестве аргумента
genFunc. Однако как же в
этом случае решается проблема параметризации? Каким
образом нам только что удалось "сделать одинаковые
функции разными"?
Парадоксы
На первый взгляд, все
понятно. Кажется, будто бы значение аргумента
str было подставлено на
место, где встретилось слово str
при генерации функции. От
этой мысли приходится тут же отказаться, поскольку у нас
здесь не препроцессор С/С++.
Функция генерируется компилятором (если бы
интерпретатор мог сгенерировать
функцию, то оператор eval
умел бы гораздо больше, как в JavaScript), а оператор
function лишь создает
новый объект, соответствующий этой
функции. Учтем теперь, что
genFunc вызывается, разумеется, уже в процессе
выполнения, и об аргументах своих она узнает никак не
ранее... Следующей в голову приходит другая идея, более
основательная. Мы уже видели в параграфе о
локальных переменных, что
генерируемая функция имеет
доступ к параметрам и локальным
переменным генерирующей
функции. Таким образом, при генерации
функции нужно было
определить, что переменная str
(в данном случае - аргумент генерирующей
функции) будет в дальнейшем
использоваться генерируемой
функцией. Тогда сборщик мусора не станет ее
удалять сразу после того, как
genFunc вернет управление. Вроде бы вполне
логичное объяснение, которое даже подтверждается вот
таким примером:
genFunc = function(str){
var ret = function(quantity){
var tempStr;
for (var i=0; i<quantity; i++) tempStr += str;
return tempStr;
}
str = "_cart_";
return ret;
}
func = genFunc("_carriage_");
anotherFunc = genFunc("_truck_");
trace(func(3));
trace(anotherFunc(4));
Измененный таким
образом код выведет
_cart__cart__cart_
_cart__cart__cart__cart_
То есть происходит
обращение именно к измененному (уже после генерации
возвращаемой функции)
значению переменной str.
Таким образом, похоже
на то, что действительно в процессе генерации объекта
функции делаются ссылки на
те локальные переменные
функции-генератора, которые
будут потом использоваться... Однако всегда ли такие
переменные можно однозначно идентифицировать? Рассмотрим
еще один пример:
genFunc = function(str){
var localStr = "_tank_";
var ret = function(quantity, name){
var tempStr;
for (var i=0; i<quantity; i++) tempStr +=
eval(name);
return tempStr;
}
localStr = "_cart_";
return ret;
}
func = genFunc("_carriage_");
anotherFunc = genFunc("_truck_");
trace('func(3, "str") =' + func(3, "str"));
trace('func(3, "localStr") =' + func(3,
"localStr"));
trace('anotherFunc(4, "str") =' + anotherFunc(4,
"str"));
trace('anotherFunc(4, "localStr") =' +
anotherFunc(4,
"localStr"));
Он выдает на выходе
func(3, "str")
=_carriage__carriage__carriage_
func(3, "localStr") =_cart__cart__cart_
anotherFunc(4, "str") =_truck__truck__truck__truck_
anotherFunc(4, "localStr") =_cart__cart__cart__cart_
На этот раз мы видим:
несмотря на то, что имя переменной, хранящей повторяемую
строку, мы задаем только при вызове сгенерированной
функции, мы все равно имеем
доступ к аргументам и локальным
переменным функции-генератора...
Хотя, казалось бы, все, что было в
функции-генераторе в момент ее вызова, должно
быть давным-давно убрано сборщиком мусора (нельзя же
допустить, чтобы поведение функций
зависело от того, успел или не успел сборщик мусора
отработать)... В чем же дело?
Разгадка парадоксов
Единственное решение
всех загадок, которые мы только что сами себе загадали,
состоит вот в чем. Раз генерируемая
функция имеет доступ к
контексту вызова
генерирующей; и раз неизвестно, какие именно
локальные переменные (или
аргументы) из этого контекста
ей понадобятся - то сохранять необходимо весь
контекст! Иными словами,
раз функция
genFunc в операторе
return возвратила
объект-функцию, то
контекст вызова
genFunc не будет уничтожен
до тех пор, пока сгенерированный
объект-функция существует! Это, конечно, может
привести к изрядному расходу памяти в том случае, когда
последовательность функций,
генерирующих друг друга, достаточно длинна. Ведь
сохранять придется все их
контексты, вплоть до самого первого! (К тому же,
в некоторых случаях вместо цепочки мы будем иметь дело с
деревом или даже более общего вида графом.) Тем не
менее, никаких ограничений на генерацию
функций в ActionScript не
накладывается. Видимо, авторы языка справедливо
посчитали, что в тех (в общем-то, довольно редких)
случаях, когда без многоступенчатой генерации
функций не обойтись,
полученный выигрыш в гибкости намного превзойдет
сложности, связанные с дополнительными затратами памяти
(да и не так уж много ее тратится, все-таки). Так что
теперь самое время продемонстрировать наиболее
впечатляющие "фокусы", возможность проделывать которые
нам дает механизм многоступенчатой генерации
функций.
Использование
генераторов функций
Одна из наиболее
показательных задач - это построение сложной
функции из более простых на
основе действий пользователя (задача, часто
встречающаяся при разработке образовательных продуктов
по математике). Предположим, что пользователю
предоставлена таблица достаточно простых и
распространенных функций,
которые он может складывать, умножать, вычитать, делить,
возводить в степени, применять друг к другу и т.д. Затем
программа должна построить график получившейся
функции (или просто вывести
значения этой функции в
нескольких точках). Сейчас мы на крайне упрощенном
примере, с очень ограниченным набором
функций и их
преобразований, увидим, как применяются в этом случае
генераторы функций.
// Сначала определяем три генератора:
// сложение функций
_global.funcPlus = function(func1, func2){
return function(x){return func1(x) + func2(x);};
}
// умножение функций
_global.funcTimes = function(func1, func2){
return function(x){return func1(x)*func2(x);};
}
// композиция функций
_global.funcComposition = function(func1, func2){
return function(x){return func1(func2(x));};
}
// и формируем массивы генераторов и обычных
функций
var operators_array = [funcPlus, funcTimes,
funcComposition];
var functions_array = [function(x){return 1/x;},
Math.sin,
Math.exp];
// Массив instructions_array, передаваемый в
следующую функцию,
// содержит указания пользователя - какие
операции с какими
// функциями производить. Этот двумерный массив,
содержит
// много субмассивов из двух элементов. Первый
элемент -
// операция, второй - одна из функций.
_global.processInstructions = function(start_func,
instruc-
tions_array){
var func = start_func;
for (var i=0; i<instructions_array.length; i++){
func = operators_array[instructions_array[i][0]](
func, functions_array[instructions_array[i][1]]
);
}
return func;
}
var start_func1 = function(x){return x*x;}
// Этот массив должен был бы генерироваться на основе
// действий пользователя
var instriction_set1 = [[1,1], [2,0]];
// Описывает функцию sin(1/x)/(x*x)
var resultFunc = processInstructions(start_func1,
instriction_set1);
trace("resultFunc(0.5) = " + resultFunc(0.5));
trace("resultFunc(0.1) = " + resultFunc(0.1));
// Протестируем, действительно ли сгенерировано
sin(1/x)/(x*x)
testFunc = function(x){return Math.sin(1/x)/(x*x);}
trace("testFunc(0.5) = " + testFunc(0.5));
trace("testFunc(0.5) = " + testFunc(0.1));
На выходе этой
программы получаем:
resultFunc(0.5) =
3.63718970730273
resultFunc(0.1) = -54.402111088937
testFunc(0.5) = 3.63718970730273
testFunc(0.5) = -54.402111088937
Tо есть
генераторы функций в данном
случае работают именно так, как мы того ожидали.
Что лучше: генератор
или функция с параметром?
Этот вопрос напоминает
другой, о том, кто сильнее, слон или кит. Тем не менее,
поразмышляем над ним. Генератор
функций, как мы уже видели, тоже дает возможность
параметризовать (сгенерированную)
функцию. Однако, если использовать его только в
целях параметризации, то приходится все-таки, писать две
функции: и генерирующую
(хоть и совсем простую), и генерируемую. В случае
функции с параметром
достаточно одной. По этой же причине
функции с параметром легче
укладываются в голове. Таким образом, если вам нужна
только параметризация (и параметр у вас один, в крайнем
случае - два) - скорее всего, вас устроит именно
функция с параметром,
прикрепленным в качестве поля
объекта-функции. Если у вас параметров штук пять
(например, коэффициент ы полинома) - уже имеет смысл
подумать о генераторе. Ведь
из аргументов генератора
параметры доставать удобнее, чем из полей
объекта-функции. Впрочем, в
случае полинома можно обойтись и одним полем в виде
массива. Но уж если у вас возникает задача, вроде
описанной выше задачи построения произвольных
функций, -
генераторы, безусловно,
лучшее, что здесь можно придумать.
Реализация приватных
полей во Флэш МХ
Напоследок рассмотрим
совсем необычный способ применения эффекта сохранения
контекста
генератора при генерации
функций. С++ и
Java-программистов может раздражать то, что при работе
на ActionScript (в отличие от ActionScript 2) нет
возможности скрыть от пользователя детали реализации.
Всегда существует опасность, что не в меру любопытный и
самоуверенный пользователь начнет менять значения полей
наших объектов, доступ к которым мы не хотим ему
предоставлять. Конечно, во Флэше существуют свойства. То
есть у объекта можно сделать фиктивные поля, при записи
в которые будет вызываться соответствующая
функция. Но ведь полученную
этой функцией информацию
нужно где-то хранить. И, желательно, хранить в таком
месте, до которого добраться просто так нельзя. Так вот,
если функции для получения
и уст ановки значений свойства генерировать другой
специальной функцией, то
полученные функции будут
иметь доступ к контексту
вызова генератора. Там-то
мы и спрячем наше поле! (И, кстати, делать именно
свойство нам совершенно необязательно. Мы вполне можем
удовольствоваться приватным полем и
функциями для доступа к
нему. Так мы и поступим - чтобы работа с нашим приватным
полем была максимально похожа на то, что мы писали бы на
C++ или Java.) Вот код, выражающий эти идеи.
_global.makePrivateField =
function(getter, newGetterName, setter,
newSetterName){
var field;
this[newGetterName] = function(){
return getter.call(this, field);
}
this[newSetterName] = function(val){
field = setter.call(this, val);
}
}
car = {};
makePrivateField.call(
car,
function(wheelsN){ // Функция - getter
return "Wheels number = " + wheelsN;
},
"getWheelsNumber",
function(wheelsN){ // Функция - setter
return Math.round(wheelsN);
},
"setWheelsNumber"
);
car.setWheelsNumber(3.8);
trace(car.getWheelsNumber());
Возвращает такой код
строчку
Wheels number = 4
Выведя в отладочное
окно список переменных программы (Ctrl+Alt+V), мы
получим следующее:
Global Variables:
Variable _global.makePrivateField = [function
'makePrivateField']
Level #0:
Variable _level0.$version = "WIN 6,0,21,0"
Variable _level0.car = [object #2, class 'Object'] {
getWheelsNumber:[function 'getWheelsNumber'],
setWheelsNumber:[function 'setWheelsNumber']
}
Мы видим, что в
объекте car нет никаких
видимых полей, зато появились
функции доступа к созданному нами приватному полю.
Правда, особо злостный нарушитель порядка может заменить
эти наши функции своими.
Однако и здесь можно помочь делу - если вы очень хотите
защитить ваши объекты от вмешательства, вы можете
запретить изменение переменных, ссылающихся на
объекты-функции доступа к
приватному полю. Делается, это, правда, только с помощью
недокументированных функций,
но, тем не менее, возможность такая есть. А о деталях вы
узнаете в следующей лекции. |