9 Классы и модули

содержание 9 Классы и модули содержание

Введение в объекты JavaScript было дано в главе 6, где каждый объект трактовался как уникальный набор свойств, отличающих его от любых других объектов. Однако часто бывает полезнее определить класс объектов, обладающих общими свойствами. Члены, или экземпляры, класса обладают собственными свойствами, определяющими их состояние, но они также обладают свойствами (обычно методами), определяющими их поведение. Эти особенности поведения определяются классом и являются общими для всех экземпляров. Например, можно объявить класс Complex для представления комплексных чисел и выполнения арифметических операций с ними. Экземпляр класса Complex мог бы обладать свойствами для хранения действительной и мнимой частей комплексного числа. А класс Complex мог бы определять методы, выполняющие операции сложения и умножения (поведение) этих чисел.

Классы в языке JavaScript основаны на использовании механизма наследования прототипов. Если два объекта наследуют свойства от одного и того же объекта – прототипа, говорят, что они принадлежат одному классу. С прототипами и наследованием мы познакомились в разделах 6.1.3 и 6.2.2. Сведения из этих разделов вам обязательно потребуются для понимания того, о чем рассказывается в этой главе. В этой главе прототипы будут рассматриваться в разделе 9.1.

Если два объекта наследуют один и тот же прототип, обычно (но не обязательно) это означает, что они были созданы и инициализированы с помощью одного конструктора. С конструкторами мы познакомились в разделах 4.6, 6.1.2 и 8.2.3. Дополнительные сведения о них в этой главе приводятся в разделе 9.2.

Те, кто знаком со строго типизированными объектно-ориентированными языками программирования, такими как Java или C++, могут заметить, что классы в языке JavaScript совершенно не похожи на классы в этих языках. Конечно, есть некоторые синтаксические сходства, и имеется возможность имитировать многие особенности «классических» классов в JavaScript. Но лучше будет с самого начала понять, что классы и механизм наследования на основе прототипов в языке JavaScript существенно отличаются от классов и механизма наследования на основе

классов в языке Java и подобных ему. В разделе 9.3 демонстрируются приемы имитации классических классов на языке JavaScript. Еще одной важной особенностью классов в языке JavaScript является возможность динамического расширения. Эта особенность описывается в разделе 9.4. Классы можно также интерпретировать как типы данных, и в разделе 9.5 будет представлено несколько способов определения класса объекта. В этом разделе вы также познакомитесь с философией программирования, известной как «утиная типизация» («duck-typing»), которая во главу угла ставит не тип объекта, а его возможности.

После знакомства со всеми этими основами объектно-ориентированного программирования в JavaScript мы перейдем в этой же главе к изучению более практического материала. В разделе 9.6 будут представлены два примера непростых классов и продемонстрировано несколько практических объектно-ориентированных приемов расширения этих классов. В разделе 9.7 будет показано (на множестве примеров), как расширять или наследовать другие классы и как создавать иерархии классов в языке JavaScript. В разделе 9.8 рассматриваются дополнительные приемы работы с классами с использованием новых возможностей, появившихся в ECMAScript 5.

Определение классов – это один из способов создания модульного программного кода многократного использования. В последнем разделе этой главы мы в общих чертах поговорим о модулях в языке JavaScript.

содержание 9.1. Классы и прототипы содержание

В языке JavaScript класс – это множество объектов, наследующих свойства от общего объекта-прототипа. Таким образом, объект-прототип является центральной особенностью класса. В примере 6.1 была определена функция inherit(), возвращающая вновь созданный объект, наследующий указанный объект-прототип. Если определить объект–прототип и затем воспользоваться функцией inherit() для создания объектов, наследующих его, фактически будет создан класс JavaScript. Обычно экземпляры класса требуют дополнительной инициализации, поэтому обычно определяется функция, которая создает и инициализирует новые объекты. В примере 9.1 демонстрируется такая функция: она определяет объект–прототип класса, представляющего диапазон значений, а также «фабричную» функцию, которая создает и инициализирует новые экземпляры класса.

Пример 9.1. Простой класс JavaScript

В примере 9.1 есть несколько интересных моментов, которые следует отметить особо. Здесь определяется фабричная функция range(), которая используется для создания новых объектов range. Обратите внимание, что для хранения объекта- прототипа, определяющего класс, используется свойство range.methods функции range(). В таком способе хранения объекта–прототипа нет ничего необычного. Во- вторых, отметьте, что функция range() определяет свойства from и to для каждого объекта range. Эти не общие, не унаследованные свойства определяют уникальную информацию для каждого отдельного объекта range. Наконец, обратите внимание, что все общие, унаследованные методы, определяемые свойством range.methods, используют свойства from и to и ссылаются на них с помощью ключевого слова this, указывающего на объект, относительно которого вызываются эти методы. Такой способ использования this является фундаментальной характеристикой методов любого класса.

(См. фрагмент статьи "Выделение: Range, TextRange и Selection")

содержание 9.2. Классы и конструкторы содержание

В примере 9.1 демонстрируется один из способов определения класса в языке JavaScript. Однако это не самый типичный способ, потому что он не связан с определением конструктора. Конструктор – это функция, предназначенная для инициализации вновь созданных объектов. Как описывалось в разделе 8.2.3, конструкторы вызываются с помощью ключевого слова new. Применение ключевого слова new при вызове конструктора автоматически создает новый объект, поэтому конструктору остается только инициализировать свойства этого нового объекта. Важной особенностью вызова конструктора является использование свойства prototype конструктора в качестве прототипа нового объекта. Это означает, что все объекты, созданные с помощью одного конструктора, наследуют один и тот же объект-прототип и, соответственно, являются членами одного и того же класса. В примере 9.2 демонстрируется, как можно было бы реализовать класс range, представленный в примере 9.1, не с помощью фабричной функции, а с помощью функции конструктора:

Пример 9.2. Реализация класса Range с помощью конструктора

Имеет смысл детально сравнить примеры 9.1 и 9.2 и отметить различия между этими двумя способами определения классов. Во-первых, обратите внимание, что при преобразовании в конструктор фабричная функция range() была переименована в Range(). Это обычное соглашение по оформлению: функции-конструкторы в некотором смысле определяют классы, а имена классов начинаются с заглавных символов. Имена обычных функций и методов начинаются со строчных символов.

Далее отметьте, что конструктор Range() вызывается (в конце примера) с ключевым словом new, тогда как фабричная функция range() вызывается без него. В примере 9.1 для создания нового объекта использовался вызов обычной функции (раздел 8.2.1), а в примере 9.2 – вызов конструктора (раздел 8.2.3). Поскольку конструктор Range() вызывается с ключевым словом new, отпадает необходимость вызывать функцию inherit() или предпринимать какие-либо другие действия по созданию нового объекта. Новый объект создается автоматически перед вызовом конструктора и доступен в конструкторе как значение this. Конструктору Range() остается лишь инициализировать его. Конструкторы даже не должны возвращать вновь созданный объект. Выражение вызова конструктора автоматически создает новый объект, вызывает конструктор как метод этого объекта и возвращает объект. Тот факт, что вызов конструктора настолько отличается от вызова обычной функции, является еще одной причиной, почему конструкторам принято давать имена, начинающиеся с заглавного символа. Конструкторы предназначены для вызова в виде конструкторов, с ключевым словом new, и обычно при вызове в виде обычных функций они не способны корректно выполнять свою работу. Соглашение по именованию конструкторов, обеспечивающее визуальное отличие имен конструкторов от имен обычных функций, помогает программистам не забывать использовать ключевое слово new.

Еще одно важное отличие между примерами 9.1 и 9.2 заключается в способе именования объекта-прототипа. В первом примере прототипом было свойство range.methods. Это было удобное, описательное имя, но в значительной мере произвольное. Во втором примере прототипом является свойство Range.prototype, и это имя является обязательным. Выражение вызова конструктора Range() автоматически использует свойство Range.prototype как прототип нового объекта Range. Наконец, обратите также внимание на одинаковые фрагменты примеров 9.1 и 9.2: в обоих классах методы объекта range определяются и вызываются одинаковым способом.

содержание 9.2.1. Конструкторы и идентификация класса

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

Хотя конструкторы не играют такую же важную роль в идентификации класса, как прототипы, тем не менее конструкторы выступают в качестве фасада класса. Например, имя конструктора обычно используется в качестве имени класса. Так, принято говорить, что конструктор Range() создает объекты класса Range. Однако более важным применением конструкторов является их использование в операторе instanceof при проверке принадлежности объекта классу. Если имеется объект r, и необходимо проверить, является ли он объектом класса Range, такую проверку можно выполнить так:

r instanceof Range // вернет true, если r наследует Range.prototype

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

содержание 9.2.2. Свойство constructor

В примере 9.2 свойству Range.prototype присваивался новый объект, содержащий методы класса. Хотя было удобно определить методы как свойства единственного объекта-литерала, но при этом совершенно не необходимо создавать новый объект. Роль конструктора в языке JavaScript может играть любая функция, поскольку выражению вызова конструктора необходимо лишь свойство prototype.. Следовательно, любая функция (кроме функций, возвращаемых методом Function.bind() в ECMAScript 5) автоматически получает свойство prototype. Значением этого свойства является объект, который имеет единственное неперечислимое свойство constructor. Значением свойства constructor является объект функции:

Наличие предопределенного объекта-прототипа со свойством constructor означает, что объекты обычно наследуют свойство constructor, которое ссылается на их конструкторы. Поскольку конструкторы играют роль идентификаторов классов, свойство constructor определяет класс объекта:

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

Рис. 9.1.
Рис. 9.1. Функция-конструктор, ее прототип и экземпляры

Обратите внимание, что в качестве примера для рис. 9.1 был взят наш конструктор Range(). Однако в действительности класс Range, определенный в примере 9.2, замещает предопределенный объект Range.prototype своим собственным. А новый объект-прототип не имеет свойства constructor. По этой причине экземпляры класса Range, как следует из определения, не имеют свойства constructor. Решить эту проблему можно, явно добавив конструктор в прототип:

Другой типичный способ заключается в том, чтобы использовать предопределенный объект-прототип, который уже имеет свойство constructor, и добавлять методы в него:

содержание 9.3. Классы в стиле Java содержание

Если вам приходилось программировать на языке Java или других объектно-ориентированных языках со строгим контролем типов, вы, возможно, привыкли считать, что классы могут иметь четыре типа членов:

Поля экземпляра

Это свойства, или переменные экземпляра, хранящие информацию о конкретном объекте.

Методы экземпляров

Методы, общие для всех экземпляров класса, которые вызываются относительно конкретного объекта.

Поля класса

Это свойства, или переменные, всего класса в целом, а не конкретного экземпляра.

Методы класса

Методы всего класса в целом, а не конкретного экземпляра.

Одна из особенностей языка JavaScript, отличающая его от языка Java, состоит в том, что функции в JavaScript являются значениями, и поэтому нет четкой границы между методами и полями. Если значением свойства является функция, это свойство определяется как метод. В противном случае это обычное свойство, или «поле». Но несмотря на эти отличия, имеется возможность имитировать все четыре категории членов классов в языке JavaScript. Определение любого класса в языке JavaScript вовлекает три различных объекта (рис. 9.1), а свойства этих трех объектов действуют подобно различным категориям членов класса:

Объект-конструктор

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

Объект-прототип

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

Объект экземпляра

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

Процесс определения класса в языке JavaScript можно свести к трем этапам. Во-первых, написать функцию-конструктор, которая будет определять свойства экземпляра в новом объекте. Во-вторых, определить методы экземпляров в объекте-прототипе конструктора. В-третьих, определить поля класса и свойства класса в самом конструкторе. Этот алгоритм можно упростить еще больше, определив простую функцию defineClass(). (В ней используется функция extend() из примера 6.2 с исправлениями из примера 8.3):

В примере 9.3 приводится более длинное определение класса. В нем создается класс, представляющий комплексные числа, и демонстрируется, как имитировать члены класса в стиле Java. Здесь все делается «вручную» – без использования функции defineClass(), представленной выше.

В примере 9.3 приводится более длинное определение класса. В нем создается класс, представляющий комплексные числа, и демонстрируется, как имитировать члены класса в стиле Java. Здесь все делается «вручную» – без использования функции defineClass(), представленной выше.

Пример 9.3. Complex.js: Класс комплексных чисел

Определение класса Complex, представленное в примере 9.3, позволяет использовать конструктор, поля экземпляра, методы экземпляров, поля класса и методы класса, как показано ниже:

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

В языке Java поддерживается возможность объявлять поля со спецификатором final, чтобы показать, что они являются константами, и объявлять поля и методы со спецификатором private, чтобы показать, что они являются приватными для реализации класса и недоступны пользователям класса. В языке JavaScript эти ключевые слова отсутствуют, поэтому, чтобы обозначить приватные свойства (имена которых начинаются с символа подчеркивания) и свойства, доступные только для чтения (имена которых содержат только заглавные символы), в примере 9.3 используются соглашения по именованию. Мы еще вернемся к этим двум темам, ниже в этой главе: приватные свойства можно имитировать с помощью локальных переменных в замыканиях (раздел 9.6.6), а возможность определения свойств–констант поддерживается стандартом ECMAScript 5 (раздел 9.8.2).

содержание 9.4. Наращивание возможностей классов содержание

Механизм наследования на основе прототипов, используемый в языке JavaScript, имеет динамическую природу: объекты наследуют все свойства своих прототипов, даже если они были добавлены в прототипы уже после создания объектов. Это означает, что в JavaScript имеется возможность наращивать возможности классов простым добавлением новых методов в объекты-прототипы. Ниже приводится фрагмент, который добавляет метод вычисления сопряженного комплексного числа в класс Complex из примера 9.3:

Объект-прототип встроенных классов JavaScript также «открыт» для подобного наращивания, а это означает, что есть возможность добавлять новые методы к числам, строкам, массивам, функциям и т. д. Данная возможность уже использовалась в примере 8.5. Там мы добавляли метод bind() к классу функций в реализации ECMAScript 3, где он отсутствует:

Ниже приводятся несколько примеров расширения классов:

Методы можно также добавлять в Object.prototype, тем самым делая их доступными для всех объектов. Однако делать это не рекомендуется, потому что в реализациях, появившихся до ECMAScript 5, отсутствует возможность сделать эти дополнительные методы неперечислимыми. При добавлении новых свойств в Object.prototype они становятся доступны для перечисления в любом цикле for/in. В разделе приводится пример использования метода Object.defineProperty(), определяемого стандартом ECMAScript 5, для безопасного расширения Object.prototype.

Возможность подобного расширения классов, определяемых средой выполнения (такой как веб-браузер), зависит от реализации самой среды. Во многих веб-браузерах, например, допускается добавлять методы в Object.HTMLElement, и такие методы будут наследоваться объектами, представляющими теги HTML в текущем документе. Однако данная возможность не поддерживается в текущей версии Microsoft Internet Explorer, что сильно ограничивает практическую ценность этого приема в клиентских сценариях.

содержание 9.5. Классы содержание

В главе 3 уже говорилось, что в языке JavaScript определяется небольшое количество типов: null, undefined, логические значения, числа, строки, функции и объекты. Оператор typeof (раздел 4.13.2) позволяет отличать эти типы. Однако часто бывает желательно интерпретировать каждый класс как отдельный тип данных и иметь возможность отличать объекты разных классов. Отличать встроенные объекты базового языка JavaScript (и объекты среды выполнения в большинстве реализаций клиентского JavaScript) можно по их атрибуту class (раздел 6.8.2), используя прием, реализованный в функции classof() из примера 6.4. Но когда класс определяется с помощью приемов, продемонстрированных в этой главе, экземпляры объектов всегда содержат в атрибуте class значение «Object», поэтому функция classof() в данной ситуации оказывается бесполезной.

В следующих подразделах описываются три приема определения класса произвольного объекта: оператор instanceof, свойство constructor и имя функции-конструктора. Однако ни один из этих приемов не дает полной гарантии, поэтому в разделе 9.5.4 мы обсудим прием грубого определения типа (duck-typing) – философии программирования, в которой центральное место отводится возможностям объекта (т. е. наличию тех или иных методов), а не его принадлежности к какому- либо классу.

содержание 9.5.1. Оператор instanceof

Оператор instanceof был описан в разделе 4.9.4. Слева от оператора должен находиться объект, для которого выполняется проверка принадлежности к классу, а справа – имя функции-конструктора, представляющей класс. Выражение o instanceof c возвращает true, если объект o наследует c.prototype. При этом наследование необязательно может быть непосредственным. Если o наследует объект, который наследует объект, наследующий c.prototype, выражение все равно вернет true.

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

Если необходимо проверить, входит ли некоторый определенный прототип в цепочку прототипов объекта без использования функции-конструктора как промежуточного звена, можно воспользоваться методом isPrototypeOf(). Например, ниже показано, как проверить принадлежность объекта r к классу range, определенному в примере 9.1:

range.methods.isPrototypeOf(r); // range.methods is the prototype object.

Один из недостатков оператора instanceof и метода isPrototypeOf() состоит в том, что они не позволяют узнать класс объекта. Они лишь проверяют принадлежность объекта указанному классу. Еще более серьезные проблемы начинают возникать в клиентских сценариях JavaScript, когда веб-приложение использует несколько окон или фреймов. Каждое окно или фрейм имеет свой собственный контекст выполнения, и в каждом из них имеется свой глобальный объект со своим собственным набором функций-конструкторов. Два массива, созданные в двух разных фреймах, унаследуют идентичные, но разные объекты прототипов, и массив, созданный в одном фрейме, не будет распознаваться оператором instanceof как экземпляр конструктора Аггау() в другом фрейме.

содержание9.5.2. Свойство constructor

Другой способ определения класса объекта заключается в использовании свойства constructor. Поскольку конструкторы считаются именами классов, определение класса выполняется очень просто. Например:

Обратите внимание, что выражения в этом примере, следующие за ключевыми словами case, являются функциями. Если бы мы использовали оператор typeof или извлекали значение атрибута class объекта, они были бы строками.

Для приема, основанного на использовании свойства constructor, характерны те же проблемы, что и для приема на основе оператора instanceof. Он не всегда будет работать при наличии нескольких контекстов выполнения (например, при наличии нескольких фреймов в окне браузера), совместно использующих общие значения. Каждый фрейм имеет собственный набор функций-конструкторов: конструктор Array в одном фрейме не будет считаться идентичным конструктору Array в другом фрейме.

Кроме того, язык JavaScript не требует, чтобы каждый объект имел свойство constructor: это всего лишь соглашение, по которому по умолчанию объект-прототип создается для каждой функции, и очень просто по ошибке или преднамеренно опустить свойство constructor в прототипе. Например, первые два класса в этой главе (в примерах 9.1: и 9.2:) были определены так, что их экземпляры не имеют свойства constructor.

содержание9.5.3. Имя конструктора

Основная проблема использования оператора instanceof или свойства constructor для определения класса объекта проявляется при наличии нескольких контекстов выполнения и, соответственно, при наличии нескольких копий функций–конструкторов. Эти функции могут быть совершенно идентичными, но разными объектами и, как следствие, не равными друг другу.

Одно из возможных решений проблемы заключается в том, чтобы использовать в качестве идентификатора класса имя функции-конструктора вместо самой функции. Конструктор Array в одном окне не будет равен конструктору Array в другом окне, но их имена будут равны. Некоторые реализации JavaScript обеспечивают доступ к имени функции через нестандартное свойство name объекта функции. Для реализаций, где свойство name отсутствует, можно преобразовать функцию в строку и извлечь имя из нее. (Этот прием использовался в разделе 9.4, где демонстрировалось добавление в класс Function метода getName().)

В примере 9.4 определяется функция type(), возвращающая тип объекта в виде строки. Простые значения и функции она обрабатывает с помощью оператора typeof. Для объектов она возвращает либо значение атрибута class, либо имя конструктора. В своей работе функция type() использует функцию classof() из примера 6.4 и метод Function.getName() из раздела 9.4. Для простоты реализация этой функции и метода включена в пример.

Пример 9.4. Функция type() для определения типа значения

Этот прием, основанный на использовании имени конструктора для идентификации класса объекта, имеет ту же проблему, что и прием на основе использования свойства constructor: не все объекты имеют свойство constructor. Кроме того, не все функции имеют имена. Если определить конструктор, используя выражение определения неименованной функции, метод getName() будет возвращать пустую строку:

содержание9.5.4. Грубое определение типа

Ни один из приемов определения класса объекта, описанных выше, не свободен от проблем, по крайней мере, в клиентском JavaScript. Альтернативный подход состоит в том, чтобы вместо вопроса «какому классу принадлежит объект?» задать вопрос «что может делать этот объект?». Этот подход является типичным в таких языках программирования, как Python и Ruby, и носит название грубое определение типа (duck-typing, или «утиная типизация») в честь высказывания (часто приписываемого поэту Джеймсу Уиткомбу Райли (James Whitcomb Riley)):

Когда я вижу птицу, которая ходит, как утка, плавает, как утка и крякает, как утка, я называю ее уткой.

Для программистов на языке JavaScript этот афоризм можно интерпретировать так: «Если объект может ходить, плавать и крякать как объект класса Duck, его можно считать объектом класса Duck, даже если он не наследует объект-прототип класса Duck».

Примером может служить класс Range из примера 9.2. Этот класс предназначен для представления диапазонов чисел. Однако обратите внимание, что конструктор Range() не проверяет типы аргументов, чтобы убедиться, что они являются числами. Аналогично метод includes() использует оператор <=, но не делает никаких предположений о типах значений границ диапазона. Благодаря тому, что класс не ограничивается определенным типом значений, его метод includes() способен обрабатывать значения границ любых типов, которые могут сравниваться с помощью операторов отношения:

Метод foreach() класса Range также не проверяет типы значений границ, но он использует функцию Math.ceil() и оператор ++, вследствие чего может применяться только к числовым значениям границ диапазона.

В качестве еще одного примера вспомним объекты, подобных массивам, обсуждавшиеся в разделе 7.11. Во многих случаях нам не требуется знать, действительно ли объект является экземпляром класса Array: вполне достаточно знать, что он имеет свойство length с неотрицательным целочисленным значением. Если посчитать, что целочисленное свойство length – это способ массивов «ходить», то мы могли бы сказать, что любой объект, который умеет «ходить» так же, можно (во многих случаях) отнести к массивам.

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

Примеры грубого определения типа, представленные выше, опираются на возможность сравнения объектов с помощью оператора < и на особенности поведения свойства length. Однако чаще всего под грубым определением типа подразумевается проверка наличия в объекте одного или более методов. Строго типизированная функция triathlon() могла бы потребовать, чтобы ее аргумент был объектом класса TriAthlete. Альтернативная реализация, выполняющая грубую проверку типа, могла бы принимать любой объект, имеющий методы walk(), swim() и bike(). Если говорить более конкретно, можно было бы переписать класс Range так, чтобы вместо операторов < и ++ он использовал бы методы compareTo()и succ() объектов значений границ.

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

В примере 9.5 определяется функция quacks() (более подходящим было бы имя «implements» (реализует), но implements является зарезервированным словом), которая может пригодиться для грубого определения типа. Функция quacks() проверяет наличие в объекте (первый аргумент функции) методов, указанных в остальных аргументах. Для каждого последующего аргумента, если тот является строкой, проверяется наличие метода с этим именем. Если аргумент – объект, проверяется наличие в первом объекте методов с теми же именами, что и во втором объекте. Если аргумент – функция, предполагается, что она – конструктор, и в этом случае проверяется наличие в первом объекте методов с теми же именами, что и в объекте-прототипе.

Пример 9.5. Функция грубой проверки типа

Есть два важных момента, касающиеся функции quacks(), которые нужно иметь в виду. Во-первых, она просто проверяет наличие в объекте одного или более методов с заданными именами. Присутствие этих свойств ничего не говорит ни о том, что делают эти функции, ни о том, сколько и какого типа аргументы они принимают. Однако это и есть сущность грубого определения типа. Определяя интерфейс, в котором вместо строгой проверки используется прием грубого определения типа, вы получаете более гибкий прикладной интерфейс, но при этом перекладываете на пользователя всю ответственность за правильное его использование. Второй важный момент, касающийся функции quacks(), заключается в том, что она не может работать со встроенными классами. Например, нельзя выполнить проверку quacks(o, Array), чтобы убедиться, что объект o обладает всеми методами класса Array. Это обусловлено тем, что методы встроенных классов недоступны для перечисления и цикл for/in в quacks() просто не заметит их. (Следует отметить, что это ограничение можно преодолеть в ECMAScript 5 с помощью функции Object.getOwnPropertyNames().)

содержание9.6. Приемы объектно-ориентированного
программирования в JavaScript содержание

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

содержание9.6.1. Пример: класс множества

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

Пример 9.6. Set.js: произвольное множество значений

содержание9.6.2. Пример: типы-перечисления

Перечислениями называются типы, которые могут принимать конечное количество значений, объявляемых (или «перечисляемых») при определении типа. В языке C и его производных типы-перечисления объявляются с помощью ключевого слова enum. В ECMAScript 5 enum – это зарезервированное (но не используемое) слово, оставленное на тот случай, если когда-нибудь в JavaScript будут peaлизованы встроенные типы-перечисления. А пока в примере 9.7 демонстрируется, как можно определить собственный тип-перечисление на языке JavaScript. Обратите внимание, что здесь используется функция inherit() из примера 6.1.

Пример 9.7 содержит единственную функцию enumeration). Однако она не является конструктором: она не определяет класс с именем «enumeration». Но она является фабричной функцией: при каждом вызове она создает и возвращает новый класс. Здесь показано, как ее можно использовать:

Цель этого примера – демонстрция того, что классы в языке JavaScript являются более гибкими и динамичными, чем статические классы в таких языках, как C++ и Java.

Пример 9.7. Типы-перечисления в JavaScript

Типичным начальным примером использования типов-перечислений может служить реализация перечисления для представления колоды игральных карт. Пример 9.8 использует функцию enumeration() именно для этого, а также определяет классы для представления карт и колод карт.

(Этот пример основан на примере для языка Java, написанном Джошуа Блохом (Joshua Bloch) и доступном по адресу: http://jcp.org/aboutJava/communityprocess/jsr/tiger/enum. html.)

Пример 9.8. Представление игральных карт в виде типов-перечислений

содержание9.6.3. Стандартные методы преобразований

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

Первым и наиболее важным является метод toString(). Назначение этого метода в том, чтобы возвращать строковое представление объекта. Интерпретатор JavaScript автоматически вызывает этот метод, когда объект используется там, где ожидается строка – в качестве имени свойства, например, или с оператором +, выполняющим конкатенацию строк. Если отказаться от реализации этого метода, ваш класс унаследует от Object.prototype реализацию по умолчанию и будет преобразовываться в довольно бесполезную строку «[object Object]». Метод toString() может возвращать более удобочитаемую строку, подходящую для отображения на экране перед конечным пользователем вашего приложения. Однако даже если в этом нет необходимости, часто бывает полезно определить свой метод toString(), чтобы упростить отладку. Классы Range и Complex, представленные в примерах 9.2 и 9.3, имеют собственные реализации метода toString(), как и типы-перечисления, реализация которых приводится в примере 9.7. Ниже мы определим метод toString() для класса Set. из примера 9.6.

С методом toString() тесно связан метод toLocaleString(): он должен преобразовывать объект в строку с учетом региональных настроек. По умолчанию объекты наследуют метод toLocaleString(), который просто вызывает их метод toString(). Некоторые встроенные типы имеют более полезные реализации метода toLocaleString(), которые возвращают строки с учетом региональных настроек. Если в реализации своего метода toString() вам придется преобразовывать в строки другие объекты, вы также должны определить свой метод toLocaleString(), выполняющий те же преобразования вызовом метода toLocaleString() объектов. Ниже мы реализуем этот метод для класса Set.

Третьим методом является метод valueOf(). Его цель – преобразовать объект в простое значение. Метод valueOf() вызывается автоматически, когда объект используется в числовом контексте, например, с арифметическими операторами (отличными от +) и с операторами отношения. Большинство объектов не имеют сколько-нибудь осмысленного простого представления и потому не определяют этот метод. Однако типы-перечисления в примере 9.7 представляют случай, когда метод valueOf() имеет большое значение.

Четвертый метод – toJSON() – вызывается автоматически функцией JSON.stringifу(). Формат JSON предназначен для сериализации структур данных и может использоваться для представления простых значений, массивов и простых объектов. При преобразовании в этот формат не делается никаких предположений о классах, и при сериализации объекта игнорируются его прототип и конструктор. Если вызвать функцию JSON.stringifу() для сериализации объекта Range или Complex, например, она вернет строку вида {"from":1, "to":3} или {"r":1, "i":-1}.. Если передать такую строку функции JSON.parse(), она вернет простой объект со свойствами, соответствующими объекту Range или Complex, но не наследующий методы класса Range или Complex.

Такой формат сериализации вполне подходит для классов, таких как Range и Complex, но для более сложных классов может потребоваться написать собственный метод toJSON(), чтобы определить иной формат сериализации. Если объект имеет метод toJSON(), функция JSON.stringifу() не будет выполнять сериализацию самого объекта, а вызовет метод toJSON() и произведет сериализацию значения (простого значения или объекта), которое он вернет. Например, объекты Date имеют собственный метод toJSON(), возвращающий строковое представление даты. Типы–перечисления в примере 9.7 делают то же самое: их метод toJSON() возвращает то же значение, что и метод toString(). Самым близким к представлению множества в формате JSON является массив, поэтому ниже мы определим метод toJSON(), который будет преобразовывать объект Set. в массив значений.

Класс Set, представленный в примере 9.6, не определяет ни один из этих методов. Множество не может быть представлено простым значением, поэтому нет смысла определять метод valueOf(), но было бы желательно определить в этом классе методы toString(), toLocaleString() и toJSON(). Можно это сделать, как показано ниже. Обратите внимание, что для добавления методов в Set.prototype используется функция extend() (пример 6.2):

содержание9.6.4. Методы

Операторы сравнения в языке JavaScript сравнивают объекты по ссылке, а не по значению. Так, если имеются две ссылки на объекты, то выясняется, ссылаются они на один и тот же объект или нет, но не выясняется, обладают ли разные объекты одинаковыми свойствами с одинаковыми значениями.

(То есть являются эквивалентными копиями-экземплярами одного класса. – Прим. науч. ред. )

Часто бывает удобным иметь возможность сравнить объекты на равенство или определить порядок их следования (например, с помощью операторов отношения и >). Если вы определяете новый класс и хотите иметь возможность сравнивать экземпляры этого класса, вам придется определить соответствующие методы, выполняющие сравнение.

В языке программирования Java сравнение объектов производится с помощью методов, и подобный подход можно с успехом использовать в JavaScript. Чтобы иметь возможность сравнивать экземпляры класса, можно определить метод экземпляра с именем equals(). Этот метод должен принимать единственный аргумент и возвращать true, если аргумент эквивалентен объекту, метод которого был вызван. Разумеется, вам решать, что следует понимать под словом «эквивалентен» в контексте вашего класса. Для простых классов часто достаточно просто сравнить свойства constructor, чтобы убедиться, что оба объекта имеют один и тот же тип, и затем сравнивать свойства экземпляра двух объектов, чтобы убедиться, что они имеют одинаковые значения. Класс Complex из примера 9.3 как раз обладает таким методом equals(), и для нас не составит труда написать похожий метод для класса Range:

Задание метода equals() для нашего класса Set оказывается несколько сложнее. Мы не можем просто сравнить свойства values двух множеств – требуется выполнить глубокое сравнение:

Иногда бывает полезно реализовать операции сравнения, чтобы выяснить порядок следования объектов. Так, для некоторых классов вполне можно сказать, что один экземпляр «меньше» или «больше» другого. Например, объекты Range можно упорядочивать, опираясь на значение нижней границы. Типы-перечисления можно было бы упорядочивать в алфавитном порядке по именам или в числовом порядке – по значениям, присваиваемым именам (предполагается, что именам присваиваются числовые значения). С другой стороны, объекты класса Set не имеют какого-то естественного порядка следования.

При попытке сравнения объектов с помощью операторов отношения, таких как < и <=, интерпретатор сначала вызовет методы valueOf() объектов и, если методы вернут значения простых типов, сравнит эти значения. Типы–перечисления, возвращаемые методом enumeration() из примера 9.7, имеют метод valueOf() и могут сравниваться с помощью операторов отношения. Однако большинство классов не имеют метода valueOf(). Чтобы сравнивать объекты этих типов для выяснения порядка их следования по вашему выбору, необходимо (опять же, следуя соглашениям, принятым в языке программирования Java) реализовать метод с именем compareTo().

Метод compareTo() должен принимать единственный аргумент и сравнивать его с объектом, метод которого был вызван. Если объект this меньше, чем объект, представленный аргументом, метод compareTo() должен возвращать значение, меньшее нуля. Если объект this больше, чем объект, представленный аргументом, метод должен возвращать значение больше нуля. И если оба объекта равны, метод должен возвращать ноль. Эти соглашения о возвращаемом значении весьма важны, потому что позволяют выполнять замену операторов отношения следующими выражениями:

Выражение
отношения
Выражение
замены
а < b a.compareTo(b) < 0
а <= b a.compareTo(b) <= 0
а > b a.compareTo(b) > 0
а >= b a.compareTo(b) >= 0
а == b a.compareTo(b) == 0
а != b a.compareTo(b) != 0

Класс Card в примере 9.8 определяет подобный метод compareTo(), и мы можем написать похожий метод для класса Range, чтобы упорядочивать диапазоны по их нижним границам:

Обратите внимание, что вычитание, выполняемое этим методом, возвращает значение меньше нуля, равное нулю или больше нуля в соответствии с порядком следования двух объектов Range. Поскольку перечисление Card.Rank в примере 9.8 имеет метод valueOf(), мы могли бы использовать тот же прием и в методе compareTo() класса Card.

Методы equals(), представленные выше, выполняют проверку типов своих аргументов и возвращают false как признак неравенства, если аргументы имеют не тот тип. Метод compareTo() не имеет специального возвращаемого значения, с помощью которого можно было бы определить, что «эти два значения не могут сравниваться», поэтому обычно методы compareTo() возбуждают исключение при передаче им аргументов неверного типа.

Примечательно, что метод compareTo() класса Range, представленный выше, возвращает 0, когда два диапазона имеют одинаковые нижние границы. Это означает, что в данной реализации метод compareTo() считает равными любые два диапазона, которые имеют одинаковые нижние границы. Однако такое определение равенства не согласуется с определением, положенным в основу метода equals(), который требует равенства обеих границ. Подобные несоответствия в определениях равенства могут стать причиной опасных ошибок, и было бы лучше привести методы equals() и compareTo() в соответствие друг с другом. Ниже приводится обновленная версия метода compareTo() класса Range. Он соответствует методу equals() и дополнительно возбуждает исключение при передаче ему несопоставимого значения:

Одна из причин, по которым может потребоваться сравнивать экземпляры класса, – обеспечить возможность сортировки массива экземпляров этого класса. Метод Array.sort() может принимать в виде необязательного аргумента функцию сравнения, которая должна следовать тем же соглашениям о возвращаемом значении, что и метод compareTo(). При наличии метода compareTo(), представленного выше, достаточно просто организовать сортировку массива объектов Range, как показано ниже:

Сортировка имеет настолько большое значение, что следует рассмотреть возможность реализации статического метода сравнения в любом классе, где определен метод экземпляров compareTo(). Особенно если учесть, что первый может быть легко реализован в терминах второго, например:

При наличии этого метода сортировка массива может быть реализована еще проще:

Некоторые классы могут быть упорядочены более чем одним способом. Например, класс Card определяет один метод класса, упорядочивающий карты по масти, а другой – упорядочивающий по значению.

содержание9.6.5. Заимствование методов

В методах JavaScript нет ничего необычного – это обычные функции, присвоенные свойствам объекта и вызываемые «посредством» или «в контексте» объекта. Одна и та же функция может быть присвоена двум свойствам и играть роль двух методов. Мы использовали эту возможность в нашем классе Set, например, когда скопировали метод toArray() и заставили его играть вторую роль в виде метода toJSON().

Одна и та же функция может даже использоваться как метод сразу нескольких классов. Большинство методов класса Array, например, являются достаточно универсальными, чтобы копировать их из Array.prototype в прототип своего класса, экземпляры которого являются объектами, подобными массивам. Если вы смотрите на язык JavaScript через призму классических объектно-ориентированных языков, тогда использование методов одного класса в качестве методов другого класса можно рассматривать как разновидность множественного наследования. Однако JavaScript не является классическим объектно-ориентированным языком, поэтому я предпочитаю обозначать такой прием повторного использования методов неофициальным термином заимствование.

Заимствоваться могут не только методы класса Array: мы можем реализовать собственные универсальные методы. В примере 9.9 определяются обобщенные методы toString() и equals(), которые с успехом могут использоваться в таких классах, как Range, Complex и Card. Если бы класс Range не имел собственного метода equals(), мы могли бы заимствовать обобщенный метод equals(), как показано ниже:

Range.prototype.equals = generic.equals;

Обратите внимание, что метод generic.equals() выполняет лишь поверхностное сравнение и не подходит для использования в классах, свойства экземпляров которых ссылаются на объекты с их собственными методами equals(). Отметьте также, что этот метод включает специальный случай для обработки свойства, добавляемого к объектам при включении их в множество Set (пример 9.6).

Пример 9.9. Обобщенные методы, пригодные для заимствования.

содержание9.6.6. Приватные члены

В классическом объектно-ориентированном программировании зачастую целью инкапсуляции, или сокрытия данных объектов внутри объектов, является обеспечение доступа к этим данным только через методы объекта и запрет прямого доступа к важным данным. Для достижения этой цели в таких языках, как Java, поддерживается возможность объявления «приватных» (private) полей экземпляров класса, доступных только через методы экземпляров класса и невидимые за пределами класса.

Реализовать приватные поля экземпляра можно с помощью переменных (или аргументов), хранящихся в замыкании, образуемом вызовом конструктора, который создает экземпляр. Для этого внутри конструктора объявляются функции (благодаря чему она получает доступ к аргументам и локальным переменным конструктора), которые присваиваются свойствам вновь созданного объекта. Этот прием демонстрируется в примере 9.10, где он используется для создания инкапсулированной версии класса Range. Вместо простых свойств from и to, определяющих границы диапазона, экземпляры этой новой версии класса предоставляют методы from и to, возвращающие значения границ. Методы from() и to() не наследуются от прототипа, а определяются отдельно для каждого объекта Range. Остальные методы класса Range определяются в прототипе как обычно, но изменены так, чтобы вместо чтения значений границ напрямую из свойств они вызывали бы методы from() и to().

Пример 9.10. Класс Range со слабо инкапсулированными границами

Новый класс Range определяет методы для чтения значений границ диапазона, но в нем отсутствуют методы или свойства для изменения этих значений. Это обстоятельство делает экземпляры этого класса неизменяемыми: при правильном использовании границы объекта Range не должны изменяться после его создания. Однако если не использовать возможности ECMAScript 5 (раздел 9.8.3), свойства from и to по-прежнему остаются доступными для записи, и в действительности объекты Range не являются неизменяемыми:

Имейте в виду, что такой прием инкапсуляции имеет отрицательные стороны. Класс, использующий замыкание для инкапсуляции, практически наверняка будет работать медленнее и занимать больше памяти, чем эквивалент с простыми свойствами.

содержание9.6.7. Перегрузка конструкторов и фабричные методы

Иногда бывает необходимо реализовать возможность инициализации объектов несколькими способами. Например, можно было бы предусмотреть возможность инициализации объекта Complex значениями радиуса и угла (полярные координаты) вместо вещественной и мнимой составляющих. Или создавать объекты множеств Set, членами которых являются элементы массива, а не аргументы конструктора.

Один из способов реализовать такую возможность заключается в создании перегруженных версий конструктора и выполнении в них различных способов инициализации в зависимости от передаваемых аргументов. Ниже приводится пример перегруженной версии конструктора Set:

Такое определение конструктора Set() позволяет явно перечислять элементы множества в вызове конструктора или передавать ему массив элементов множества. К сожалению, этот конструктор имеет одну неоднозначность: его нельзя использовать для создания множества, содержащего единственный элемент–массив (для этого потребуется создать пустое множество, а затем явно вызвать метод add()).

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

А так можно реализовать фабричный метод для инициализации объекта Set массивом:

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

В ECMAScript 5 функции имеют метод bind() особенности которого позволяют создавать подобные вспомогательные конструкторы (раздел 8.7.4).

содержание9.7. Подклассы содержание

В объектно-ориентированном программировании класс B может расширять, или наследовать, другой класс A. Класс A в этом случае называется суперклассом, а класс B – подклассом. Экземпляры класса B наследуют все методы экземпляров класса A. Класс B может определять собственные методы экземпляров, некоторые из которых могут переопределять методы класса A с теми же именами. Если метод класса B переопределяет метод класса A, переопределяющий метод класса B может вызывать переопределенный метод класса A: этот прием называется вызовом метода базового класса. Аналогично конструктор B() подкласса может вызывать конструктор A() суперкласса. Это называется вызовом конструктора базового класса. Подклассы сами могут наследоваться другими подклассами и, работая над созданием иерархии классов, иногда бывает полезно определять абстрактные классы. Абстрактный класс – это класс, в котором объявляется один или более методов без реализации. Реализация таких абстрактных методов возлагается на конкретные подклассы абстрактного класса.

Ключом к созданию подклассов в языке JavaScript является корректная инициализация объекта-прототипа. Если класс B расширяет класс A, то объект B.prototype должен наследовать A.prototype. В этом случае экземпляры класса B будут наследовать свойства от объекта B.prototype, который в свою очередь наследует свойства от A.prototype. В этом разделе демонстрируются все представленные выше термины, связанные с подклассами, а также рассматривается прием, альтернативный наследованию, который называется композицией.

Используя в качестве основы класс Set из примера 9.6, этот раздел продемонстрирует, как определять подклассы, как вызывать конструкторы базовых классов и переопределенные методы, как вместо наследования использовать прием композиции и, наконец, как отделять интерфейсы от реализации с помощью абстрактных классов. Этот раздел завершает расширенный пример, в котором определяется иерархия классов Set. Следует отметить, что первые примеры в этом разделе служат цели продемонстрировать основные приемы использования механизма наследования, и некоторые из них имеют существенные недостатки, которые будут обсуждаться далее в этом же разделе.

содержание9.7.1. Определение подкласса

В языке JavaScript объекты наследуют свойства (обычно методы) от объекта-прототипа своего класса. Если объект o является экземпляром класса B, а класс B является подклассом класса A, то объект o также наследует свойства класса A. Добиться этого можно за счет наследования объектом-прототипом класса B свойств объекта-прототипа класса A, как показано ниже, с использованием функции inherit() (пример 6.1):

В.prototype = inherit(A.prototype); // Подкласс наследует суперкласс
В.prototype.constructor = В; // Переопределить унаследованное св. constructor

Эти две строки являются ключом к созданию подклассов в JavaScript. Без них объект-прототип будет обычным объектом – объектом, наследующим свойства от Object.prototype, – а это означает, что класс будет подклассом класса Object, подобно всем остальным классам. Если добавить эти две строки в функцию defineClass() (раздел 9.3), ее можно будет преобразовать в функцию defineSubclass() и в метод Function.prototype.extend(), как показано в примере 9.11.

Пример 9.11. Вспомогательные инструменты определения подклассов

Пример 9.12 демонстрирует, как определить подкласс «вручную», без использования функции defineSubclass(). В этом примере определяется подкласс SingletonSet класса Set. Класс SingletonSet представляет специализированное множество, доступное только для чтения и состоящее из единственного постоянно элемента.

Пример 9.12. SingletonSet: простой подкласс

Класс SingletonSet имеет очень простую реализацию, состоящую из пяти простых методов. Этот класс не только реализует пять основных методов класса Set, но и наследует от своего суперкласса такие методы, как toString(), toArray() и equals(). Возможность наследования методов является одной из основных причин определения подклассов. Метод equals() класса Set (определен в разделе 9.6.4), например, может сравнивать любые экземпляры класса Set, имеющие методы size() и foreach(), с любыми экземплярами класса Set, имеющими методы size() и contains(). Поскольку класс SingletonSet является подклассом класса Set, он автоматически наследует его метод equals() и не обязан иметь собственную реализацию этого метода. Безусловно, учитывая чрезвычайно упрощенную структуру множества, содержащего единственный элемент, можно было бы реализовать для класса SingletonSet более эффективную версию метода equals():

SingletonSet.prototype.equals = function(that) {
  return that instanceof Set && that.size()==1 && that.contains(this.member);
};

Обратите внимание, что класс SingletonSet не просто заимствует список методов из класса Set: он динамически наследует методы класса Set. Если в Set.prototype добавить новый метод, он тут же станет доступен всем экземплярам классов Set и SingletonSet (в предположении, что класс SingletonSet не определяет собственный метод с таким же именем).

содержание9.7.2. Вызов конструктора и методов базового класса

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

Пример 9.13 демонстрирует применение этого приема. Он определяет подкласс NonNullSet класса Set: тип множеств, которые не могут содержать элементы со значениями null и undefined. Чтобы исключить возможность включения в множество таких элементов, класс NonNullSet должен выполнить в методе add() проверку значений добавляемых элементов на равенство значениям null и undefined. Но при этом не требуется включать в класс полную реализацию метода add() – можно просто вызвать версию метода из суперкласса. Обратите также внимание, что конструктор NonNullSet() тоже не реализует все необходимые операции: он просто передает свои аргументы конструктору суперкласса (вызывая его как функцию, а не как конструктор), чтобы конструктор суперкласса мог инициализировать вновь созданный объект.

Пример 9.13. Вызов из подкласса конструктора и метода базового суперкласса

Теперь обобщим понятие «множество без пустых элементов» до понятия «фильтрованное множество»: множество, элементы которого должны пропускаться через функцию-фильтр перед добавлением. Определим фабричную функцию (подобную функции enumeration() из примера 9.7), которая будет получать функцию– фильтр и возвращать новый подкласс класса Set. В действительности можно пойти еще дальше по пути обобщений и определить фабричную функцию, принимающую два аргумента: наследуемый класс и функцию-фильтр, применяемую к методу add(). Новой фабричной функции можно было бы дать имя filteredSetSubclass() и использовать ее, как показано ниже:

Реализация этой фабричной функции приводится в примере 9.14. Обратите внимание, что эта функция вызывает метод и конструктор базового класса подобно тому, как это реализовано в классе NonNullSet.

] Пример 9.14. Вызов конструктора и метода базового класса

В примере 9.14 есть один интересный момент, который хотелось бы отметить. Он заключается в том, что, обертывая операцию создания подкласса функцией, мы получаем возможность использовать аргумент superclass в вызовах конструктора и метода базового класса и избежать указания фактического имени суперкласса. Это означает, что в случае изменения имени суперкласса достаточно будет изменить имя в одном месте, а не отыскивать все его упоминания в программном коде. Такого способа стоит придерживаться даже в случаях, не связанных с определением фабричных функций. Например, с помощью функции-обертки можно было бы переписать определение класса NonNullSet и метода Function.prototype.extend() (пример 9.11), как показано ниже:

В заключение хотелось бы подчеркнуть, что возможность создания подобных фабрик классов обусловлена динамической природой языка JavaScript. Фабрики классов представляют собой мощный и гибкий инструмент, не имеющий аналогов в языках, подобных Java и C++.

содержание9.7.3. Композиция в сравнении с наследованием

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

Однако существует более простой путь решения этой задачи. В объектно-ориентированном программировании существует известный принцип «предпочтения композиции перед наследованием».

(См. Э. Гамма и др. «Приемы объектно-ориентированного проектирования. Паттерны проектирования» или Дж. Блох «Java. Эффективное программирование». )

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

Пример 9.15. Композиция множеств вместо наследования

Одно из преимуществ применения приема композиции в данном случае заключается в том, что требуется определить только один подкласс FilteredSet. Экземпляры этого класса могут накладывать ограничения на элементы любого другого экземпляра множества. Например, вместо класса NonNullSet, представленного выше, реализовать подобные ограничения можно было бы так:

var s = new FilteredSet(new Set(), function(x) { return x !== null; });

Можно даже наложить еще один фильтр на фильтрованное множество:

var t = new FilteredSet(s, { function(x} { return !(x instanceof Set); });

содержание9.7.4. Иерархии классов и абстрактные классы

В предыдущем разделе было предложено «предпочесть композицию наследованию». Но для иллюстрации этого принципа мы создали подкласс класса Set. Сделано это было для того, чтобы получившийся подкласс был instanceof Set и наследовал полезные методы класса Set, такие как toString() и equals(). Это достаточно уважительные причины, но, тем не менее, было бы неплохо иметь возможность использовать прием композиции без необходимости наследовать некоторую определенную реализацию множества, такую как класс Set. Аналогичный подход можно было бы использовать и при создании класса SingletonSet (пример 9.12) – этот класс был определен как подкласс класса Set чтобы унаследовать вспомогательные методы, но его реализация существенно отличается от реализации суперкласса. Класс SingletonSet – это не специализированная версия класса Set, а совершенно иной тип множеств. В иерархии классов SingletonSet должен был бы находиться на одном уровне с классом Set, а не быть его потомком.

Решение этой проблемы в классических объектно-ориентированных языках, а также в языке JavaScript заключается в том, чтобы отделить интерфейс от реализации. Представьте, что мы определили класс AbstractSet, реализующий вспомогательные методы, такие как toString(), в котором отсутствуют реализации базовых методов, таких как foreach(). Тогда все наши реализации множеств – Set, SingletonSet и FilteredSet – могли бы наследовать класс AbstractSet. При этом классы FilteredSet и SingletonSet больше не наследовали бы ненужные им реализации.

Пример 9.16 развивает этот подход еще дальше и определяет иерархию абстрактных классов множеств. Класс AbstractSet определяет только один абстрактный метод, contains(). Любой класс, претендующий на роль множества, должен будет определить хотя бы один такой метод. Далее в примере определяется класс AbstractEnumerableSet, наследующий класс AbstractSet. Этот класс определяет абстрактные методы size() и foreach() и реализует конкретные полезные методы (toString(), toArray(), equals() и т. д.). AbstractEnumerableSet не определяет методы add() или remove() и представляет класс множеств, доступных только для чтения. Класс SingletonSet может быть реализован как конкретный подкласс. Наконец, в примере определяется класс AbstractWritableSet, наследующий AbstractEnumerableSet. Этот последний абстрактный класс определяет абстрактные методы add() и remove() и реализует использующие их конкретные методы, такие как union() и intersection(). Класс AbstractWritableSet отлично подходит на роль суперкласса для наших классов Set и FilteredSet. Однако они не были добавлены в пример, а вместо них была включена новая конкретная реализация с именем ArraySet

Пример 9.16 довольно объемен, но он заслуживает детального изучения. Обратите внимание, что для простоты создания подклассов в нем используется функция Function.prototype.extend().

Пример 9.16. Иерархия абстрактных и конкретных классов множеств

содержание9.8. Классы в ECMAScript 5 содержание

Стандарт ECMAScript 5 добавляет методы, позволяющие определять атрибуты свойств (методы чтения и записи, а также признаки доступности для перечисления, записи и настройки) и ограничивать возможность расширения объектов. Эти методы были описаны в разделах 6.6, 6.7 и 6.8.3 и могут пригодиться при определении классов. В следующих подразделах демонстрируется, как использовать новые возможности ECMAScript 5 для повышения надежности своих классов.

содержание9.8.1. Определение неперечислимых свойств

Класс Set, представленный в примере 9.6, вынужден использовать уловку, чтобы обеспечить возможность сохранения объектов: он добавляет свойство «object id» всем объектам, добавляемым в множество. Если позднее в каком-то другом месте программы будет выполнен обход свойств этого объекта с помощью цикла for/in, это свойство будет обнаружено. Стандарт ECMAScript 5 позволяет исключить такую возможность, сделав свойство неперечислимым. В примере 9.17 демонстрируется, как это сделать с помощью Object.defineProperty() а также показывает, как определить метод чтения и как проверить возможность расширения объекта.

Пример 9.17. Определение неперечислимых свойств

содержание9.8.2. Определение неизменяемых классов

Помимо возможности делать свойства неперечислимыми, стандарт ECMAScript 5 позволяет делать свойства доступными только для чтения, что может быть довольно удобно при создании классов, экземпляры которых не должны изменяться. В примере 9.18 приводится неизменяемая версия класса Range, который использует эту возможность, применяя функции Object.defineProperties() и Object.create(). Кроме того, функция Object.defineProperties() используется в нем также для добавления свойств в объект-прототип класса, что делает методы экземпляров недоступными для перечисления, подобно методам встроенных классов. Но и это еще не все: определяемые в примере методы экземпляров создаются доступными только для чтения и не могут быть удалены, что исключает возможность динамического изменения класса. Наконец, в примере 9.18 использован один интересный трюк – при вызове без ключевого слова new функция-конструктор класса действует как фабричная функция.

Пример 9.18. Неизменяемый класс со свойствами и методами, доступными только для чтения

Для определения неизменяемых и неперечислимых свойств в примере 9.18 используются функции Object.defineProperties() и Object.create(). Они предоставляют широкие возможности, но необходимость определять для них объекты дескрипторов свойств может сделать программный код более сложным для чтения. Чтобы избежать этого, можно определить вспомогательные функции для изменения атрибутов свойств, которые уже были определены. Две такие вспомогательные функции демонстрируются в примере 9.19.

Пример 9.19. Вспомогательные функции для работы с дескрипторами свойств

Функции Object.defineProperty() и Object.defineProperties() могут использоваться и для создания новых свойств, и для изменения атрибутов уже существующих свойств. При создании новых свойств все опущенные атрибуты по умолчанию принимают значение false.. Однако при изменении атрибутов уже существующих свойств опущенные атрибуты не изменяются. Например, в функции hideProps() выше указывается только атрибут enumerable, потому что функция должна изменять только его.

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

Пример 9.20. Более простое определение неизменяемого класса

содержание9.8.3. Сокрытие данных объекта

В разделе 9.6.6 и в примере 9.10 было показано, как можно использовать переменные и аргументы функции-конструктора для сокрытия данных объекта, создаваемого этим конструктором. Недостаток этого приема заключается в том, что в ECMAScript 3 допускается возможность замещения методов доступа к этим данным. Стандарт ECMAScript 5 позволяет обеспечить более надежное сокрытие приватных данных за счет определения методов доступа к свойствам, которые не могут быть удалены. Этот способ демонстрируется в примере 9.21.

Пример 9.21. Класс Range

содержание9.8.4. Предотвращение расширения класса

Возможность расширения классов за счет добавления новых методов в объект–прототип обычно рассматривается как характерная особенность языка JavaScript. Стандарт ECMAScript 5 позволяет при желании предотвратить такую возможность. Функция Object.preventExtensions() делает объект нерасширяемым (раздел 6.8.3) – в такой объект невозможно добавить новые свойства. Функция Object.seal() идет еще дальше: она не только предотвращает добавление новых свойств, но и делает все имеющиеся свойства ненастраиваемыми, предотвращая возможность их удаления. (Однако ненастраиваемое свойство по-прежнему может быть доступно для записи и по-прежнему может быть преобразовано в свойство, доступное только для чтения.) Чтобы предотвратить возможность расширения объекта Object.prototype можно просто записать:

Object.seal(Object.prototype);

Другая динамическая особенность языка JavaScript – возможность замены методов объекта:

Предотвратить такую замену можно, объявив методы экземпляров доступными только для чтения. Сделать это можно с помощью вспомогательной функции freezeProps(), объявленной выше. Другой способ добиться этого эффекта заключается в использовании функции Object.freeze(), которая выполняет те же действия, что и функция Object.seal(), и дополнительно делает все свойства ненастраиваемыми и доступными только для чтения.

Свойства, доступные только для чтения, обладают одной особенностью, о которой необходимо помнить при работе с классами. Если объект o наследует свойство p, доступное только для чтения, попытка присвоить значение свойству o.p будет завершаться неудачей без создания нового свойства в объекте o. Если потребуется переопределить унаследованное свойство, доступное только для чтения, можно воспользоваться функциями Object.defineProperty(), Object.defineProperties() или Object.create(), чтобы создать новое свойство. Это означает, что, когда методы экземпляров класса делаются доступными только для чтения, это существенно осложняет возможность их переопределения в подклассах.

На практике обычно не требуется блокировать возможность изменения объектов–прототипов таким способом, но в некоторых случаях предотвращение расширения объектов может оказаться полезным. Вспомните фабричную функцию enumeration() из примера 9.7. Она сохраняет все экземпляры перечислений в свойствах объекта–прототипа и в свойстве–массиве values конструктора. Эти свойства и массив играют роль официального перечня экземпляров перечислений, и их определенно имеет смысл зафиксировать, чтобы исключить возможность добавления новых экземпляров и изменения или удаления существующих. Для этого достаточно добавить в функцию enumeration() следующие строки:

Object.freeze(enumeration.values);
Object.freeze(enumeration);

Обратите внимание, что применение функции Object.freeze() к типу перечисления исключает возможность использования свойства objectId, как было показано в примере 9.17. Решение этой проблемы состоит в том, чтобы прочитать значение свойства objectId (вызвать соответствующий метод чтения и установить внутреннее свойство) перечисления только один раз, перед тем как его зафиксировать.

содержание9.8.5. Подклассы и ECMAScript 5

В примере 9.22 демонстрируется порядок создания подклассов с использованием возможностей ECMAScript 5. В нем определяется класс stringSet, наследующий класс AbstractWritableSet из примера 9.16. Основная особенность этого примера заключается в использовании функции Object.create() для создания объекта-прототипа, наследующего прототип суперкласса, и в определении свойств вновь созданного объекта. Как уже отмечалось выше, основная сложность этого подхода заключается в необходимости использовать неудобные дескрипторы свойств.

Другой интересной особенностью этого примера является передача значения null функции Object.create() при создании объекта, не наследующего ничего. Этот объект используется для хранения элементов множества, а тот факт, что он не имеет прототипа, позволяет вместо метода hasOwnProperty() использовать оператор in.

Пример 9.22. StringSet: определение подкласса множества с использованием ECMAScript 5

содержание9.8.6. Дескрипторы свойств

В разделе 6.7 дается описание дескрипторов свойств, введенных стандартом ECMAScript 5, но там отсутствуют примеры, демонстрирующие различные случаи их использования. Мы завершим этот раздел, посвященный особенностям ECMAScript 5, расширенным примером, демонстрирующим многие операции со свойствами, допустимые в ECMAScript 5. Программный код в примере 9.23 добавляет в Object.prototype метод properties() (разумеется, недоступный для перечисления). Значение, возвращаемое этим методом, является объектом, представляющим список свойств и обладающим полезными методами для отображения свойств и атрибутов (которые могут пригодиться при отладке). Его можно использовать для получения дескрипторов свойств (на случай, если потребуется реализовать копирование свойств вместе с их атрибутами) и для установки атрибутов свойств (благодаря чему он может использоваться как альтернатива функциям hideProps() и freezeProps(), объявленным ранее). Этот единственный пример демонстрирует большинство особенностей свойств в ECMAScript 5, а также применение методики модульного программирования, о которой будет рассказываться в следующем разделе.

Пример 9.23. Особенности свойств в ECMAScript 5

содержание9.9. Модули содержание

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

Многие библиотеки и клиентские фреймворки JavaScript включают собственные инструменты поддержки модулей. Например, библиотеки Dojo и Google Closure определяют функции provide() и require() для объявления и загрузки модулей. А в рамках проекта CommonJS по стандартизации серверного JavaScript (http://commonjs.org) разработана спецификация, определяющая модули, в которой также используется функция require(). Подобные инструменты поддержки модулей часто берут на себя такие функции, как загрузка модулей и управление зависимостями, но их обсуждение выходит за рамки этой дискуссии. Если вы пользуетесь одним из таких фреймворков, то вам следует использовать и определять модули, следуя соглашениям, принятым в этом фреймворке. А в этом разделе мы обсудим лишь самые простые соглашения.

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

содержание9.9.1. Объекты как пространства имен

Один из способов обойтись в модуле без создания глобальных переменных заключается в том, чтобы создать объект и использовать его как пространство имен. Вместо того чтобы создавать глобальные функции и переменные, их можно сохранять в свойствах объекта (на который может ссылаться глобальная переменная). Рассмотрим в качестве примера класс Set из примера 9.6. Он определяет единственную глобальную функцию-конструктор Set. Он определяет различные методы экземпляров, но сохраняет их как свойства объекта Set.prototype, благодаря чему они уже не являются глобальными. В этом примере также определяется вспомогательная функция _v2s(), но она также сохраняется как свойство класса Set.

Далее рассмотрим пример 9.16. Этот пример объявляет несколько абстрактных и конкретных классов множеств. Для каждого класса создается единственное глобальное имя, но модуль целиком (файл с программным кодом) определяет довольно много глобальных имен. С точки зрения сохранения чистоты глобального пространства имен было бы лучше, если бы модуль с классами множеств определял единственное глобальное имя:

var sets = {};

Объект sets мог бы играть роль пространства имен модуля, а каждый из классов множеств определялся бы как свойство этого объекта:

sets.SingletonSet = sets.AbstractEnumerableSet.extend(...);

Когда возникла бы потребность использовать класс, объявленный таким способом, мы могли бы просто добавлять пространство имен при ссылке на конструктор:

var s = new sets.SingletonSet(1);

Автор модуля не может заранее знать, с какими другими модулями будет использоваться его модуль, поэтому он должен принять все меры против конфликтов, используя подобные пространства имен. Однако программист, использующий модуль, знает, какие модули он использует и какие имена в них определяются. Этот программист не обязан использовать имеющиеся пространства имен ограниченно и может импортировать часто используемые значения в глобальное пространство имен. Программист, который собирается часто использовать класс Set из пространства имен sets, мог бы импортировать класс, как показано ниже:

Иногда авторы модулей используют глубоко вложенные пространства имен. Если модуль с определениями классов множеств является частью обширной коллекции модулей, для него может использоваться пространство имен collections.sets, а сам модуль может начинаться со следующих определений:

Иногда пространство имен верхнего уровня используется для идентификации разработчика или организации – автора модуля и для предотвращения конфликтов между пространствами имен. Например, библиотека Google Closure определяет свой класс Set в пространстве имен goog.structs. Для определения глобально уникальных префиксов, которые едва ли будут использоваться другими авторами модулей, индивидуальные разработчики могут использовать компоненты доменного имени. Поскольку мой веб-сайт имеет имя davidflanagan.com я мог бы поместить свой модуль с классами множеств в пространство имен com.davidflanagan.collections.sets.

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

var sets = com.davidflanagan.collections.sets;

В соответствии с соглашениями имя файла модуля должно совпадать с его пространством имен. Модуль sets должен храниться в файле с именем sets.js. Если модуль использует пространство имен collections.sets, то этот файл должен храниться в каталоге collections/ (этот каталог мог бы также включать файл maps.js). А модуль, использующий пространство имен com.davidflanagan.collections.sets, должен храниться в файле com/fdavidflanagan/Collections/sets.js.

содержание9.9.2. Область видимости функции как частное пространство имен

Модули имеют экспортируемый ими общедоступный прикладной интерфейс (API): это функции, классы, свойства и методы, предназначенные для использования другими программистами. Однако зачастую для внутренних нужд модуля требуются дополнительные функции или методы, которые не предназначены для использования за пределами модуля. Примером может служить функция Set._v2s() из примера 9.6 – для нас было бы нежелательно, чтобы пользователи класса Set вызывали эту функцию, поэтому было бы неплохо сделать ее недоступной извне.

Этого можно добиться, определив модуль (в данном случае класс Set) внутри функции. Как описывалось в разделе 8.5, переменные и функции, объявленные внутри другой функции, являются локальными по отношению к этой функции и недоступны извне. Таким образом, область видимости функции (называемой иногда «функцией модуля») можно использовать как частное пространство имен модуля. Пример 9.24 демонстрирует, как это может выглядеть применительно к нашему классу Set.

Пример 9.24. Класс Set внутри функции модуля

Обратите внимание, что такой прием вызова функции сразу после ее определения является характерным для языка JavaScript. Программный код, выполняемый в приватном пространстве имен, предваряется текстом «(function() {» и завершается «}());». Открывающая круглая скобка в начале сообщает интерпретатору, что это выражение определения функции, а не инструкция, поэтому в префикс можно добавить любое имя функции, поясняющее ее назначение. В примере 9.24 было использовано имя «invocation», чтобы подчеркнуть, что функция вызывается сразу же после ее объявления. Точно так же можно было бы использовать имя «namespace», чтобы подчеркнуть, что функция играет роль пространства имен.

После того как модуль окажется заперт внутри функции, ему необходим некоторый способ экспортировать общедоступный API для использования за пределами функции модуля. В примере 9.24 функция модуля возвращает конструктор, который тут же присваивается глобальной переменной. Сам факт возврата значения из функции ясно говорит о том, что оно экспортируется за пределы области видимости функции. Модули, определяющие более одного элемента API, могут возвращать объект пространства имен. Для нашего модуля с классами множеств можно было бы написать такой программный код:

Можно предложить похожий прием, определив функцию модуля как конструктор, который будет вызываться с ключевым словом new и экспортировать значения за счет их присваивания:

Если объект глобального пространства имен уже определен, функция модуля может просто присваивать значения свойствам этого объекта и вообще ничего не возвращать:

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