Если возникает необходимость написать JavaScript_модуль, предназначенный для использования в любом сценарии или любым другим модулем, очень важно следовать правилу, согласно которому необходимо избегать объявления гло_ бальных переменных. Всякий раз, когда объявляется глобальная переменная, возникает риск, что эта переменная будет переопределена другим модулем или программистом, применяющим этот модуль. Решение проблемы заключается в создании специально для данного модуля пространства имен и определении всех свойств и методов внутри этого пространства.
Язык JavaScript не обладает встроенной поддержкой пространств имен1, но для этих целей прекрасно подходят JavaScript_объекты. Рассмотрим вспомогатель_ ные методы provides() и defineClass(), представленные в примерах 9.8 и 9.10 со_ ответственно. Имена обоих методов являются глобальными символами. Если предполагается создать модуль функций для работы с JavaScript_классами, эти методы не должны объявляться в глобальном пространстве имен. С целью со_ блюдения этого соглашения, реализацию методов можно записать так:
// Создать пустой объект, который будет выполнять функции пространства имен
// Это единственное глобальное имя будет вмещать все остальные имена
var Class = {};
// Определить функции в пространстве
имен
Class.define =
function(data)
{
/*
здесь находится реализация метода */ }
Class.provides
= function(o, c) {
/*
здесь находится реализация метода */ }
Обратите внимание: здесь не объявляются методы экземпляра (и даже не объяв_ ляются статические методы) JavaScript_класса. Здесь объявляются обычные функции, ссылки на которые сохраняются в свойствах специально созданного объекта.
Этот фрагмент иллюстрирует первое правило разработки JavaScript_модулей:
модуль никогда не должен выставлять больше одного имени в глобальном про' странстве имен. Существуют также два дополнения к этому правилу:
• Если модуль добавляет имя в глобальное пространство имен, документация к модулю должна четко и ясно отражать назначение этого имени.
1 Как, например, определение namespace в языке C++ или одноименная встроенная команда в интерпретирующем языке Tcl (а это уже совсем близкий JavaScript язык). – Примеч. науч. ред.
200 Глава 10. Модули и пространства имен
• Если модуль добавляет имя в глобальное пространство имен, это имя должно быть однозначно связано с именем файла, из которого загружен модуль.
Так, если модуль называется Class, необходимо поместить его в файл с именем Class.js, а сам файл должен начинаться с комментария, который может выгля_деть примерно следующим образом:
/**
* Class.js: Модуль вспомогательных функций для работы с классами.
*
* Данный модуль определяет единственное глобальное имя "Class".
* Имя Class является пространством имен объекта, а все функции
* сохраняются как ссылки в свойствах этого пространства имен. **/
Классы в JavaScript имеют чрезвычайно важное значение, поэтому для работы с ними может существовать множество модулей. Что же произойдет, если най_ дутся такие два модуля, которые будут использовать имя Class для определения своих пространств имен? В этом случае произойдет конфликт имен. С помощью пространств имен можно существенно снизить риск появления конфликтов, но полностью свести риск к нулю невозможно. В этом отношении существенную помощь может оказать следование правилу именования файлов. Если оба кон_ фликтующих модуля будут иметь одинаковое имя Class.js, их не удастся сохра_ нить в одном каталоге. Загрузить оба модуля сценарий сможет только из разных каталогов, например utilities/Class.js и flanagan/Class.js.
А если сценарии сохраняются в подкаталогах, тогда имена подкаталогов долж_ ны являться частью имени модуля. Это означает, что модуль Class, определяе_ мый здесь, в действительности должен называться flanagan.Class. Вот как это может быть реализовано на практике:
/**
* flanagan/Class.js: Модуль вспомогательных функций для работы с классами.
*
* Данный модуль определяет единственное глобальное имя "flanagan",
* если оно еще не существует. Затем создается объект пространства имен,
* который сохраняется в свойстве Class объекта flanagan. Все вспомогательные
* функции размещаются в пространстве имен flanagan.Class.
**/
var flanagan; // Объявление единственного глобального имени "flanagan"
if
(!flanagan)
flanagan
=
{}; //
Создается
объект, если
он еще не определен
flanagan.Class
= {}
//
Создается
пространство
имен flanagan.Class
//
Теперь пространство имен заполняется вспомогательными
методами
flanagan.Class.define =
function(data) {
/* реализация метода */ };
flanagan.Class.provides
=
function(o, c)
{
/* реализация
метода */ };
В данном фрагменте глобальный объект flanagan является пространством имен для других пространств имен. Если, к примеру, я напишу другой модуль вспо_ могательных функций для работы с датами, то сохраню эти функции в про_ странстве имен flanagan.Date. Примечательно, что этот фрагмент объявляет гло_ бальное имя flanagan с помощью инструкции var и лишь затем проверяет его на_ личие. Сделано это потому, что попытка чтения из необъявленной глобальной переменной приводит к генерации исключения, тогда как попытка чтения из объявленной, но не определенной переменной просто возвращает значение unde_
10.1. Создание модулей и пространств имен
fined. Такое поведение характерно только для глобальных элементов. Если по_ пытаться прочитать значение несуществующего свойства объекта пространства имен, будет просто получено значение undefined.
При наличии двухуровневых пространств имен вероятность конфликтов имен снижается еще больше. Однако если некоторый разработчик, тоже имеющий фа_ милиею Flanagan, решит написать модуль вспомогательных функций для работы с классами, программист, пожелавший использовать оба модуля, окажется в ту_ пиковой ситуации. Хотя такой ход событий выглядит достаточно маловероятным, для полной уверенности можно попробовать следовать соглашению языка про_ граммирования Java, согласно которому для придания уникальности именам па_ кетов нужно использовать префиксы, начинающиеся с имени вашего домена в Ин_ тернете. При этом порядок следования имен доменов следует менять на обратный, чтобы имя домена верхнего уровня (.com или нечто подобное) стояло первым, и указывать получившееся имя в качестве префикса для всех ваших JavaScript_ модулей. Поскольку мой сайт называется davidflanagan.com, я должен буду со_ хранить свои модули в файле с именем com/davidflanagan/Class.js и использовать пространство имен com.davidflanagan.Class. Если все JavaScript_разработчики бу_ дут следовать этому соглашению, никто не сможет создать пространство имен com.davidflanagan, поскольку только я владею доменом davidflanagan.com.
Это соглашение может оказаться излишним для большинства JavaScript_моду_ лей, и вам не обязательно следовать ему в точности. Но вы должны знать о его су_ ществовании. Старайтесь не создавать по ошибке пространства имен, имена ко_ торых могут быть именем чьего_то домена: никогда не определяйте пространст' ва имен с использованием имен доменов, не являющихся вашей собственностью.
Пример 10.1 демонстрирует порядок создания пространства имен com.david_ flanagan.Class. Здесь добавлен код проверки ошибок, отсутствующий в предыду_ щем примере; в процессе этой проверки генерируется исключение, если про_ странство имен com.davidflanagan.Class уже существует или существуют про_ странства имен com или com.davidflanagan, но они не являются объектами. Здесь также демонстрируется, как можно создать и заполнить пространство имен с по_ мощью единственного литерала объекта.
Пример 10.1. Создание пространства имен на основе имени домена
// Создать глобальный символ "com", если его еще не существует
// Генерировать исключение, если он существует, но не является объектом var com;
if (!com) com = {};
else if (typeof com != "object")
throw new Error("имя com существует, но не является объектом");
// Повторить процедуру создания и проверки типов на более низких уровнях if (!com.davidflanagan) com.davidflanagan = {}
else if (typeof com.davidflanagan != "object")
throw new Error("com.davidflanagan существует, но не является объектом");
// Генерировать исключение, если com.davidflanagan.Class уже существует
if (com.davidflanagan.Class)
throw new Error("com.davidflanagan.Class уже существует");
// В противном случае создать и заполнить пространство имен