Модульное программирование на JavaScript

Введение

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

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

Языки программирования служат для записи алгоритмов в форме, пригодной для выполнения на целевой платформе. На языках программирования C и C++ пишут программы, которые должны работать на микропроцессорной аппаратуре. Языки Java и C# используют для программирования виртуальных машин JVM и .NET, являющихся надстройкой над аппаратным слоем. А язык JavaScript был разработан для программирования интернет-браузеров.

Уровни компьютерных систем и языки программирования

Рис. 1. Программирование различных уровней компьютерных систем.

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

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

Строительные блоки JavaScript

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

function funcName (p) {
  var v, r;
  ...
  return r;
}

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

Вызов функции записывается так:

funcName (b);

Эта запись приводит к тому, что значение переменной b присваивается параметру p, который внутри функции funcName может использоваться наравне с локальными переменными v и r. После этого начинается выполнение команд, находящихся между фигурными скобками. Последняя команда return приводит к завершению выполнения функции и размещению значения локальной переменной r в том месте программы, где функция была вызвана. Это назвается возвратом значения переменной r. В приведенном примере возвращённое значение не используется и будет потеряно. Однако в других ситуациях оно может быть использовано в выражении или присвоено переменной:

var a = b + funcName (c);

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

var obj = {
  fld1: "value1",
  fld2: "value2",
  mth1: function () {
    alert (this.fld1);
  },
  mth2: function () {
    alert (this.fld2);
  }
}

В результате выполнения этого кода будет создан объект obj с двумя свойствами fld1 и fld2, которые инициализированы значениями "value1" и "value2" соответственно, и двумя методами mth1 и mth2, которые выводят значения соответствующих свойств. Из этого примера видно, что в тексте методов обращение членам объекта производится с помощью зарезервированной переменной this. Значение переменной this поддерживается средой выполнения сценариев JavaScript таким образом, что эта переменная всегда ссылается на текущий объект — объект, который был использован для вызова метода.

Доступ к членам объектов осуществляется с помощью "точечной" нотации, как это принято в большинстве языков программирования:

obj.fld1 = "new value";
obj.mth1 ();

Выполнение сценариев JavaScript

Текст сценария JavaScript может быть внедрён в атрибут элемента HTML-документа, включён блоком кода в HTML-документ или записан в отдельном JS-файле. Независимо от способа оформления текста сценария, исполняющая система собирает все его части в единое пространство. Это означает, что функции и переменные, определённые в разных JS-файлах или блоках кода, видимы друг для друга точно так же, как если бы они были изначально записаны одним большим последовательным текстом. После сборки текстов исполняющая система сначала обрабатывает все определения функций, а потом начинает выполнять сценарий. Благодаря этому функции могут быть вызваны в тексте сценария раньше, чем записаны их определения:

var a = b + funcName (c);
...
function funcName (p) {
  ...
}

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

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

Области видимости

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

В JavaScript области видимости организуются с помощью функций. Тело каждой функции открывает свою область видимости. Поскольку определения функций в JavaScript могуть быть вложенными, то и области видимости имеют ту же самую вложенность (рис. 2).

Вложенность функций и области видимости

Рис. 2. Области видимости вложенных функций.

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

Пример 1. Область видимости.

<script>
/* Глобальная область видимости. */
var V = "global variable";
window.alert ("1: " + V);
function F () {
  /* Начало области видимости функции F. */
  var V = "local variable of the function F";
  window.alert ("2: " + V)
  function FF () {
    /* Область видимости функции FF. */
    var V = "local variable of the function FF";
    window.alert ("3: " + V);
    /* Конец области видимости функции FF. */
  }
  FF (); // вызов функции FF
  /* Конец области видимости функции F. */
}
F (); // вызов функции F
</script>

Чтобы изолировать область видимости, в JavaScript можно использовать анонимную функцию с непосредственным вызовом:

Пример 2. Приём для изоляции области видимости.

<script>
(function (global) {
  /* Изолированная область видимости, */
  /* global — ссылка на глобальную область видимости. */
  global.alert ("Hello, world!");
}) (window); // вызов анонимной функции
</script>

Контекст и пространства имён

Контекст — это значение указателя this в конкретной позиции кода сценария. JavaScript — объектно-ориентированный язык, поэтому весь код сценария выполняется в контексте какого-нибудь объекта. Функции, которые явно не определены как методы объекта, выполняются в контексте глобального объекта и фактически являются его методами.

Если метод вызывает внутреннюю функцию, то внутренняя функция будет выполняться в контексте глобального объекта (если сценарий выполняется в интернет-браузере, то таким объектом является window), а не того, из метода которого она была вызвана. Эту особенность обходят, вводя в методе переменную that и присваивая ей значение переменной this:

Пример 3. Доступ к контексту метода из внутренней функции.

var obj = {
  prop: "Obj.prop value",
  meth: function () {
    /* Начало контекста объекта (this ссылается на obj). */
    var that = this; // вспомогательная переменная для доступа к объекту
    func (); // вызов внутренней функции метода
    function func () {
      /* Начало глобального контекста (this ссылается на window). */
      window.alert (that.prop);
      /* Конец глобального контекста. */
    }
    /* Конец контекста объекта. */
  }
}
obj.meth ();

Пространство имён в JavaScript можно организовать с помощью литерала объекта:

Пример 4. Объект NS играет роль пространства имён.

/* Создание пустого объекта. */
var NS = {};
/* Наполнение объекта функциями и переменными. */
NS.debug = function (msg) {
  window.alert ("DEBUG: " + msg);
}
/* Вызов функции из пространства имён NS. */
NS.debug ("Hello, world!");

Конструирование объектов

Выше был описан простой способ создания объектов JavaScript через литеральное описание его свойств с помощью фигурных скобок. Этот способ прекрасно подходит, когда нужно создать один уникальный объект, такой, как пространство имён. Но иногда требуется создать набор однотипных объектов: окна приложения, геометрические фигуры и т. п. Если создавать эти объекты, используя литеральное описание, то для каждого отдельного объекта из набора придётся записывать его методы, хотя они абсолютно одинаковые:

var circle1 = {
  r: 5,
  area: function () {
    return 3.1416 * this.r * this.r;
  }
}
var circle2 = {
  r: 7,
  area: function () {
    return 3.1416 * this.r * this.r;
  }
}

Это приводит к дублированию кода. К счастью, в JavaScript существует возможность создания объектов с помощью конструктора. Конструктор — это функция, которая инициализирует свойства объекта. Например, конструктор объекта "круг" может выглядеть так:

function Circle (r) {
  this.r = r;
}

Использовать конструктор нужно строго определённым образом вместе с оператором new:

var circle1 = new Circle (5);
var circle2 = new Circle (7);

Оператор new незаметно для программиста выполняет такие действия:

  1. Заставляет исполняющую систему JavaScript создать новый пустой объект. Назовём его для удобства newObj.
  2. Изменяет контекст, в котором выполняется функция, на созданный объект. То есть в самом начале функции выполняет присваивание this = newObj.
  3. Если выполнение функции завершается оператором return без параметров или просто достижением конца тела функции, то функция возвращает ссылку на созданный на шаге 1 объект. Другми словами, выполняется оператор return this.

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

Чтобы не забывать использовать оператор new с конструкторами, имена функций-конструкторов начинают с прописной буквы, а имена остальных функций — со строчной. А что произойдёт, если функция-конструктор будет вызвана без оператора new? Как было сказано выше, переменная this при выполнении кода, не связанного с объектами программы, ссылается на глобальный объект. Поэтому при таком ошибочном вызове конструктор инициализирует свойства глобального объекта и по завершении своей работы вернёт не ссылку на объект, а неопределённое значение.

Так с помощью конструктора задаются свойства, присущие конкретному объекту. Чтобы инициализировать свойства, общие для всего набора объектов, создаваемых с помощью конструктора, нужно воспользоваться свойством prototype самого конструктора. Свойство prototype имеется у каждого конструктора. Значение этого свойства — объект, свойства которого, в свою очередь, находятся в контексте всех объектов, созданных с помощью конструктора. Добавляя новые свойства объекту prototype, можно создать свойства, которые будут видимы для всех объектов, созданных с помощью конструктора, свойства объекта prototype которого были расширены.

Вот, как это работает:

Circle.prototype.area = function () {
  return 3.1416 * this.r * this.r;
}

После этого метод area может быть использован во всех объектах, созданных с помощью конструктора Circle (ссылка на объект prototype при этом не указывается, а неявно подразумевается исполняющей системой JavaScript):

var s1 = circle1.area ();
var s2 = circle2.area ();

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

Circle.fn = Circle.prototype;

После этого добавление общих свойств набора объектов можно выполнять так:

Circle.fn.perimeter = function () {
  return 2 * 3.1416 * this.r;
}

Модульная структура приложения

С учётом сказанного выше можно разработать правила кодирования для модульного программирования на JavaScript. В качестве примера напишем программу для работы с геометрическими фигурами: прямоугольником и кругом. Для удобства программирования каждую фигуру представим отдельным объектом. Определения объектов заключим в модули, для чего поместим их тексты в отдельные JS-файлы и примем меры для их изоляции.

Ограничение пространства имён модуля осуществляется с помощью конструкции вызова безымянной функции с передачей в неё глобальных объектов, которые могут понадобиться для работы:

(function (global) {
...
}) (window);

Полностью изолированный от программы модуль бесполезен. Необходимо каким-то образом реализовать интерфейс для взаимодействия с ним других частей программы. Внутри функции параметр global может использоваться для обращения к глобальному объекту. Пространство имён приложения заключим в объекте app, который будет являться свойством глобального объекта.

(function (global) {
var app = (global.app === undefined? global.app = {}: global.app);
...
}) (window);

Локальная переменная app получает ссылку на объект global.app, который уже был инициализирован ранее другими модулями или, если такого объекта не было к моменту выполнения текущего модуля, то на вновь созданный пустой объект. Приведенный текст можно считать универсальным прологом и эпилогом JavaScript-модуля.

Модульная структура программы на JavaScript

Рис. 3. Модульная структура JavaScript-приложения.

Добавим в объект app ссылку на конструктор объекта, который будет определён в нашем модуле.

(function (global) {
var app = (global.app === undefined? global.app = {}: global.app);
app.Rectangle = Rectangle;
function Rectangle (a, b) {
  this.name = "rectangle";
  this.a = a;
  this.b = b;
}
}) (window);

Параметрами конструктора являются значения, которыми инициализируются длины сторон объекта-прямоугольника. Уточним описание объекта.

(function (global) {
var app = (global.app === undefined? global.app = {}: global.app);
app.Rectangle = Rectangle;
function Rectangle (a, b) {
  this.name = "rectangle [" + a + "x" + b + "]";
  this.a = a;
  this.b = b;
}
Rectangle.fn = Rectangle.prototype;
Rectangle.fn.getName = function () {
  return this.name;
}
Rectangle.fn.perimeter = function () {
  return 2 * (this.a + this.b);
}
Rectangle.fn.area = function () {
  return this.a * this.b;
}
}) (window);

Модуль для работы с прямоугольником готов. Запишем созданный текст в файл square.js.

По аналогии напишем модуль для работы с кругом:

(function (global) {
var app = (global.app === undefined? global.app = {}: global.app);
app.Circle = Circle;
function Circle (r) {
  this.name = "circle (" + r + ")" ;
  this.r = r;
}
Circle.fn = Circle.prototype;
Circle.fn.pi = 3.1416;
Circle.fn.getName = function () {
  return this.name;
}
Circle.fn.perimeter = function () {
  return 2 * this.pi * this.r;
}
Circle.fn.area = function () {
  return this.pi * this.r * this.r;
}
}) (window);

Обратите внимание, что значение числа pi, которое одинаково для всех кругов, вынесено в общее для всех кругов свойство.

Текст модуля работы с кругом запишем в файл circle.js и займёмся основной программой, использующей объекты "прямоугольник" и "круг". Для этого создадим простой HTML-файл:

<html>
<head>
<title>JavaScript application demo</title>
</head>
<body>
</body>
</html>
</code>

и наполним его кодом JavaScript. Прежде всего, подключим модули с определениями объектов "прямоугольник" и "круг". Благодаря использованию в этих модулях стандартных пролога и эпилога, порядок подключения модулей значения не имеет. В противном случае следовало бы озаботиться обеспечением существования свойства global.app к моменту подключения модулей.

<html>
<head>
<title>JavaScript application demo</title>
<script src="rectangle.js" type="text/javascript"></script>
<script src="module.js" type="text/javascript"></script>
</head>
<body>
</body>
</html>
</code>

Теперь можно написать текст самой программы. В отличие от модулей, для наглядности поместим его прямо в HTML-файл demo.html:

<html>
<head>
<title>JavaScript application demo</title>
<script src="rectangle.js" type="text/javascript"></script>
<script src="circle.js" type="text/javascript"></script>
</head>
<body>
<script>
var aObj = new Array (
  new app.Rectangle (7, 9),
  new app.Circle (5),
  new app.Rectangle (3, 4),
  new app.Circle (2)
);
for (i in aObj) {
  obj = aObj[i];
  document.writeln ("<p>Perimeter of " + obj.getName () + " is " + obj.perimeter () + ".</p>");
  document.writeln ("<p>Area of " + obj.getName () + " is " + obj.area () + ".</p>");
}
</script>
</body>
</html>

Помимо использования объектов, описания которых размещены в изолированных модулях, программа демонстрирует полиморфизм созданных объектов. Чтобы этого достичь, создаваемые объекты были помещены в массив aObj. С помощью языковой конструкции for ... in ... из массива извлекается очередной объект, и для него вызываются методы getName (), perimeter () и area (). В зависимости от того, какого рода объект - круг или прямоугольник - был извлечён на очередной итерации цикла, срабатывает метод, специфичный для этого типа объектов.

Заключение

В JavaScript используется "плоская" модель выполнения программы. Чтобы внести элементы структуры для изоляции участков программного кода нужно применять специальные приёмы, основанные на областях видимости и контексте. Область видимости в JavaScript ограничивается телом функции, а контекст привязан к объекту, для которого выполняется функция. Если при вызове функции отсутствовало указание на объект, то контекстом является глобальный объект приложения. Для создания программных модулей удобно использовать конструкцию вызываемых на месте анонимных функций, а для реализации интерфейсов модулей -- объекты, размещённые в качестве свойств в глобальном объекте.

Литература

  1. Рейслинг Дж. JavaScript. Профессиональные приёмы программирования. — СПб.: Питер, 2008
  2. Крокфорд Д. JavaScript: сильные стороны. — СПб.: Питер, 2012
  3. Маккоу А. Веб-приложения на JavaScript. — СПб.: Питер, 2012

Начальные сведения о языке программирования JavaScript с простыми примерами программирования web-страниц можно почерпнуть из книги:
Дмитриева М. В. Самоучитель JavaScript

Для систематизации знаний JavaScript и изучения приёмов программирования нужно прочитать книгу:
Крокфорд Д. JavaScript: сильные стороны

Освоить вершины мастерства программирования на JavaScript и научиться использованию популярных библиотек поможет книга:
Маккоу А. Веб-приложения на JavaScript

Лицензионные электронные книги
Магазин книг в электронном виде

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

Изображения для свободного использования
Бесплатные изображения

Надёжный белорусский хостинг
Белорусский хостинг

Яндекс.Метрика