Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Ajax в действии

.pdf
Скачиваний:
92
Добавлен:
01.05.2014
Размер:
6.34 Mб
Скачать

Приложение Б. Инструменты для профессиональной работы с Ajax

613

Б.З. Методы и функции

 

 

__

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

Б.3.1. Функции как независимые элементы

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

Программисты, использовавшие языки семейства С, скажут, что функции JavaScript — это то же самое, что указатели на функции в C++ . Они будут правы, но не совсем.

В JavaScript Function — это встроенный объект. Конечно же, он содержит исполняемый код и допускает вызов, но он также является потомком Object и может делать то же, что и любой объект JavaScript, в том числе и хранить значения свойств. К объекту Function можно присоединить в качестве методов другие объекты Function (многие разработчики поступают так).

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

var result=MyObject.doSomething(x,у, z)

Однако Function — это также независимый объект, и к нему можно обратиться с помощью метода call() (а также метода apply (), предоставляющего аналогичные возможности).

var result=MyObj ect.doSomething.call(MyOtherObject, x, y, z) Допустимо даже следующее выражение:

v a r r e s u l t = M y O b j e c t [ ' d o S o m e t h i n g 1 ] . c a l l ( M y O t h e r O b j e c t , x , у , z )

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

Следует заметить, что число параметров функции JavaScript не фиксировано. В Java или С# при попытке вызывать метод, указав количество параметров больше или меньше объявленного, на этапе компиляции генерируется сообщение об ошибке. JavaScript лишь игнорирует лишние параметры и при-

614 Часть V. Приложения

зваивает недостающим значение undefined. Искусно реализованные функции чогут даже запрашивать список параметров посредством свойства arguments л присваивать недостающим параметрам осмысленные значения по умолчанию. Они также могут генерировать исключения и предпринимать другие черы по обеспечению работоспособности программы. Например, можно реачизовать в рамках одной функции get- и set-метод.

function area(value){ if (value){

this.area=value;

}

return this.area;

}

Если мы вызовем агеа{) без параметров, значение value будет не определено, присвоение не будет иметь смысла и функция выступит в роли getлетода. Указав параметр, можно превратить функцию в set-метод. Данный годход широко использовал Майк Фостер в своей библиотеке х (см. ссылку в сонце данного приложения и главу 3). Если вы поработаете с библиотекой х, данный подход вскоре станет привычен вам.

Тот факт, что функции могут быть независимыми от конкретных объекюв, открывает ряд интересных возможностей.

5.3.2.Присоединениефункцийкобъектам

Тоскольку JavaScript — функциональный язык, он позволяет определять функции в отсутствии объектов, например:

f u n c t i o n doSomething(x,у,z){ . . . }

Тело функции также можно определить непосредственно при вызове. v a r d o S o m e t h i n g = f u n c t i o n ( х , у , z ) { . . . }

Чтобы отдать должное объектному подходу, разработчики JavaScript [редусмотрели возможность присоединения функций к объектам. При этом |ни становятся похожими на методы Java или С# . Связать функцию с объктом можно несколькими способами.

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

myObj.doSomethingNew=doSomething;

myObj.doSomethingNew(x,y,z);

Мы также можем присоединить функцию таким образом, что к ней бу- ,ет иметь доступ любой экземпляр класса. Для этого надо добавить функ- ,ию (либо предопределенную, либо объявленную в процессе присоединения) конструкторе (см. раздел Б.2.2) или связать ее с помощью прототипа.

Несмотря на возможность присоединения функции к объекту, связь меж- у ними нельзя считать очень прочной.

Приложение Б. Инструменты для профессиональной работы с Ajax 615

Б.3.3. Заимствование функций из других объектов

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

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

function Tree(name, leaf, bark){ this.name=name;

this.leaf=leaf; this.bark=bark;

}

Свяжем с этим классом функцию, предоставляющую описание дерева.

Tree.prototype.describe=function(){

return this.name+": leaf="+this.leaf+", bark="+this.bark;

}

Если мы создадим экземпляр объекта Tree и запросим у него описание, то получим вполне предсказуемый результат.

var Beech=new Tree("green, serrated edge","smooth"); alert{Beech.describe());

В окне отображается текст Beech: leaf=green, serrated edge, bark=- smooth. Пока никаких неожиданностей не предвидится. Теперь определим класс, представляющий собаку.

function Dog(name,bark){ this.name=name; this.bark=bark;

}

Создадим экземпляр класса Dog.

var Snowy=new Dog("snowy","wau! wau!");

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

var tmpFunc=Beech.describe; tmpFunc.call(Snowy);

Как вы помните, первый параметр, передаваемый function.call(), — это объект контекста, на который ссылается специальная переменная this. Приведенный выше фрагмент кода генерирует окно с сообщением, в котором отображается текст Snowy: leaf=undefined, bark=wau! wau!. Формат сообщения нельзя назвать идеальным, но все же это лучше, чем ничего.

Но как это происходит? Почему собака получает возможность вызвать функцию, которая на самом деле принадлежит дереву? Ответ несколько неожиданный: на самом деле функция не принадлежит дереву. При при-

516 Часть V. Приложения

оединении функции к прототипу Tree связывание осуществляется лишь гастолько, насколько это необходимо для поддержки выражений типа lyTree.describe (). Если рассмотреть внутреннюю реализацию программы, •о можно увидеть, что функция хранится как фрагмент текста и выполнятся при каждом вызове. При этом конкретное значение this изменяется от

.бращения к обращению.

Заимствование функций — интересный прием; освоив, вы можете применять его при написания своего кода. Однако для повышения надежности мы ,се же рекомендуем реализовать для Snowy собственный метод bark(). Мы бсуждаем данный вопрос лишь потому что при написании кода для поддержки событий, вам необходимо знать, какие действия браузер выполняет ез вашего участия.

13.4.Обработка событий в Ajax-программах

иконтексты функций

)бработчики событий в Ajax-приложениях похожи на обработчики, реализумые инструментальными средствами создания пользовательских интерфейов для многих языков. Как было показано в главе 4, события, связанные с [ышью и клавиатурой, выделяются в отдельные категории. В рассмотренном ами примере использовался обработчик onclick; это событие генерируется о щелчку мышью на видимом элементе. Подробное обсуждение обработки обытий в DHTML не входит в круг задач, рассматриваемых в данной книге; [ы ограничимся лишь одной особенностью, которая может стать ловушкой ля неопытных программистов. Обработчик события может определяться в [TML-дескрипторе, например:

<div id='rayDiv' onclick='alert:alert(this.id)'x/div> Его также можно задать из программы.

function clickHandler(){ a l e r t ( t h i s . i d ) ; } myDiv.onclick=clickHandler;

Заметьте, что во втором случае мы передаем ссылку на объект Funcion (после clickHandler круглые скобки не указываются). При объявлении >ункции в HTML-дескрипторе создается безымянная функция. Эта запись <вивалентна приведенной ниже.

myDiv.onclick=function{){ a l e r t ( t h i s . i d ) ; }

Заметьте, что в обоих случаях параметры функции не предусмотрены: е существует способа задать их при щелчке мышью. Если же мы щелкаtf на элементе DOM, при вызове функции передается в качестве параметра 5ъект Event, а сам элемент выполняет роль объекта контекста. Зная это, ожно избежать многих неприятностей, в особенности при написании объстного кода. Источником проблем может стать тот факт, что узел DOM -егда используется в качестве контекста, даже если функция присоединена ^средством прототипа к другому объекту.

В приведенном ниже примере мы определяем простой объект с обработаком события для видимого интерфейсного элемента. В данном случае объ-

Приложение Б. Инструменты для профессиональной работы с Ajax 617

ект можно рассматривать как модель, обработчик события выполняет роль контроллера, а элемент DOM является представлением.

function myObj(id,div){ this.id=id;

this.div=div; this.div.onclick=this.clickHandler;

}

Конструктору передается идентификатор и элемент DOM, с которым связывается обработчик события. Сам обработчик определен следующим образом:

myObj.prototype.clickHandler=function(event){ alert(this.id);

}

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

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

Ссылка на модель по имени

Согласно описываемому подходу, каждому экземпляру объекта модели ставится в соответствие уникальный идентификатор и поддерживается глобальный массив объектов. Имея ссылку на элемент DOM, мы можем обратиться к объекту модели, используя в качестве ключа для поиска в массиве часть идентификатора. Данный подход иллюстрируется на рис. Б.1.

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

В качестве примера рассмотрим объект типа myObj, который содержит строку заголовка, реагирующую на щелчок мышью путем вызова функции myObj.foo{).

Ниже представлен глобальный массив. var MyObjects=new Array();

Функция конструктора, регистрирующая объект модели в этом массиве, выглядит следующим образом:

Рис. Б.18. Ссылка на модель из обработчика события по имени. Производится разбор идентификатора, и результаты разбора используются в качестве ключевого значения для поиска в глобальном массиве

function myObj(id){ this.uid=id; MyObjects[this.uid]=this;

this.render();

}

^S

Ниже приведен метод объекта myObj. Мы вызываем его щелчком на стро- ! заголовка.

myObj.prototype.f00=function(){ alert('foooo!M f+this.uid);

}

Метод render () объекта создает узлы DOM. myObj.prototype.render-function(){

this.body=docuraent.createElement("div"); « this.body.id=this.uid+"_body";

this.titleBar=document.createElement("div"); this.titleBar.id=this.uid+"_titleBar"; this.titleBar.onclick=fooEventHandler;

}

Если мы создаем в представлении DOM-узлы для модели, то связываем ними идентификаторы, содержащие идентификаторы модели.

Заметьте, что ссылка на функцию f ooEventHandler () присваивается юйству onclick элемента DOM, соответствующего строке-заголовка.

function fooEventHandler(event){ var modelObj=getMyObj(this.id);

if (modelObj){ modelObj.foo(); } }}

Рис. Б.19. Присоединение ссылки на модель непосредственно к узлу DOM упрощает поиск модели функцией обработки события

Чтобы вызвать метод f оо (), функция обработчика события должна найти экземпляр myob j. Для этой цели мы создаем специальный метод.

function getMyObj(id){

var key-id.split(•_")[0]; return MyObjectsfkey];

}

Этот метод использует свойство id DOM-узла для того, чтобы извлечь ключ, с помощью которого можно получить объект модели из глобального массива.

Метод ссылки на модель по имени хорошо зарекомендовал себя на практике, однако существует более простой путь решения той же задачи, при котором нет необходимости снабжать узлы DOM длинными идентификаторами. (Трудно сказать, хорош этот способ или нет. С одной стороны, он приводит к излишнему использованию памяти, а с другой стороны — упрощает отладку в Mozilla DOM Inspector.)

Присоединение модели к узлу DOM

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

Данный подход существенно упрощает обработку события. Функция конструктора для объекта модели не выполняет специальных действий с идентификатором, а метод f оо () определяется так же, как и ранее. Создавая узлы DOM, мы используем особенность JavaScript, позволяющую присоединять к любому объекту произвольные атрибуты, и связываем объект модели непосредственно с узлом, получающим событие.

myObj.prototype.createView=function(){

this.body=document.createElement("div");

620 Часть V. Приложения

this.body.modelObj=thi s;

this.titleBar=document.createElement("div");

this.titleBar.modelObj=this;

this.titleBar.onclick=fooEventHandler;

}

При написании обработчика события мы можем непосредственно получить ссылку на модель.

function fooEventHandler(event){ var modelObj=this.modelObj;

if (modelObj){ modelObj.foo(); }

}

}

При этом не нужно специально выполнять поиск.

Необходимо, однако, учитывать одну важную особенность. При использовании данного подхода создается циклическая ссылка между DOM-узлом и переменной, не имеющей отношения к DOM. В некоторых браузерах это может вызвать проблемы в работе системы "сборки мусора". Если корректно реализовать данное решение, можно избежать неоправданного расходования памяти, но мы настоятельно рекомендуем вам изучить главу 7, перед тем как выполнять связывание модели с DOM-узлом.

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

Последняя особенность функций JavaScript, которую нам необходимо рассмотреть, — это способность создавать замыкания. В языках Java и С# понятие замыкания отсутствует, однако в некоторых языках сценариев, например Groovy и Boo, оно используется. С замыканиями также приходится сталкиваться в языке С# 2.0. Рассмотрим, что такое замыкания и как их использовать.

S.3.5. Замыкания в JavaScript

Сам по себе объект Function — неполный, и чтобы вызвать функцию, нам надо передать ей объект контекста и набор параметров (этот набор может быть пустым). Замыкание можно рассматривать как объект Function, связанный со всеми ресурсами, необходимыми для выполнения функции.

В JavaScript замыкания создаются неявно. Не существует конструктора, вызываемого с помощью выражения new ClosureO, и нет возможности получить ссылку на объект замыкания. Для создания замыкания достаточно объявить функцию в блоке кода (например, в теле другой функции) и обеспечить доступ к ней из-за пределов блока.

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

Приложение Б. Инструменты для профессиональной работы с Ajax 621

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

function Robot(){

var createTime=new Date(); this.getAge=function{){

var now=new Date();

var age=now-createTime; return age;

}

}

(Все роботы идентичны, поэтому мы не передаем конструктору в качестве параметра имя или какие-либо другие сведения.) Конечно, предпочтительнее бьуто бы оформить createTime в виде свойства так, как показано ниже, однако мы намеренно объявили локальную переменную, область видимости которой ограничивается блоком, где она определена (в данном случае конструктором).

this.createTime=new Date{);

Следующей строкой конструктора является определение функции getAge ( Заметьте, что мы определили внутреннюю функцию в составе другой функции и что внутренняя функция использует локальную переменную createTime, принадлежащую области видимости внешней функции. Поступив таким образом, мы создали замыкание. Если мы определим робота и запросим его возраст, то получим значение в пределах 10-50 миллисекунд — интервал между первым выполнением сценария и загрузкой страницы.

var robbie=new Robot(); window.onload=function(){

alert(robbie.getAge());

}

Несмотря на то что мы объявили createTime как локальную переменную для функции конструктора, процедура "сборки мусора" не затрагивает ее до тех пор, пока переменная robbie ссылается на объект Robot. Таким образом, произошло связывание посредством замыкания.

Замыкание присутствует, только если внутренняя функция создана в составе внешней. Предположим, что мы реструктуризировали код и вынесли функцию getAge () за пределы конструктора так, что ее могут совместно использовать все экземпляры класса Robot.

function Robot(){

var createTime=new Date(); this.getAge=roboAge;

}

function roboAge{){ var now=new Date{);

var age=now-createTime; return age;

};

В этом случае замыкание не будет создано и мы получим сообщение о том, что переменная createTime не определена.

122 Часть V. Приложения

Создать замыкание очень легко, и, к сожалению это можно сделать епреднамеренно. В результате локальные переменные будут связаны и про- ,едура "сборки мусора" не затронет их. Если под действие случайно созданого замыкания попадут узлы DOM, это приведет к значительному расходоанию памяти.

Чаще всего замыкания создаются при связывании функций обратного ызова, представляющих собой обработчики событий, с источниками этих обытий. Как было сказано в разделе Б.3.4, контекст и набор параметров, оторые получает функция обратного вызова, не всегда устраивают разраотчика. Мы рассмотрели способ создания для элемента DOM дополнительой ссылки на объект модели. В результате модель становится доступной осредством DOM-элемента. Замыкания обеспечивают альтернативный спооб решения данной задачи.

myOb j.prototype.createView=function(){

this.titleBar=docuroent.createElement("div"}; var modelObj=this; this.titleBar.onclick=function(){

fooEventHandler.call(modelObj);

}

}

В определенном нами анонимном обработчике формируется ссылка на окальную переменную modelObj. В результате создается ссылка и доступ modelObj становится возможным в процессе выполнения функции. Заметь- е, что замыкания создаются только для локальных переменных; ссылка поредством this не приводит к возникновению подобного эффекта.

Мы использовали данный подход в главе 2 при создании объекта ontentLoader. Функция обратного вызова onreadystatechange, предоставяемая браузером Internet Explorer, возвращает в качестве контекста объект indow. Поскольку window определен глобально, мы не знаем, для какого бъекта ContentLoader изменилось свойство readyState. Чтобы решить эту адачу, мы передаем ссылку на соответствующий объект посредством замыания.

Мы настоятельно рекомендуем программистам, работающим над Ajaxриложениями, по возможности избегать замыканий и принимать альтеративные решения. Если вы будете использовать прототип для добавления эункций к объекту, то избежите дублирования функций и не создадите замыания. Перепишем наш класс Robot в соответствии с данной рекомендацией.

function Robot(){ this.createTirae=new Date{);

}

Robot.prototype.getAge=function(){ var now=new Date();

var age=now-this.createTime; return age;

Функция getAgeO определяется только один раз, и, поскольку она свяана с прототипом, каждый созданный объект Robot имеет доступ к ней.