javascript

JavaScript. Подробное руководство, 5 е издание Перевод А. Киселева Главный редактор А. Галунов Зав. редакцией Н. Макарова Научный редактор О. Цилюрик Редактор А. Жданов Корректор С. Минин Верстка Д. Орлова Флэнаган Д. JavaScript. Подробное руководство. – Пер. с англ. – СПб: СимволПлюс, 2008. – 992 с., ил. ISBN10: 5932861037 ISBN13: 9785932861035 Пятое издание бестселлера «JavaScript. Подробное руководство» полностью обновлено. Рассматриваются взаимодействие с протоколом HTTP и примене ние технологии Ajax, обработка XMLдокументов, создание графики на сторо не клиента с помощью тега , пространства имен в JavaScript, необхо димые для разработки сложных программ, классы, замыкания, Flash и встра ивание сценариев JavaScript в Javaприложения. Часть I знакомит с основами JavaScript. В части II описывается среда разра ботки сценариев, предоставляемая вебброузерами. Многочисленные примеры демонстрируют, как генерировать оглавление HTMLдокумента, отображать анимированные изображения DHTML, автоматизировать проверку правиль ности заполнения форм, создавать всплывающие подсказки с использованием Ajax, как применять XPath и XSLT для обработки XMLдокументов, загру женных с помощью Ajax. Часть III – обширный справочник по базовому Java Script (классы, объекты, конструкторы, методы, функции, свойства и кон станты, определенные в JavaScript 1.5 и ECMAScript v3). Часть IV – справоч ник по клиентскому JavaScript (API вебброузеров, стандарт DOM API Level 2 и недавно появившиеся стандарты: объект XMLHttpRequest и тег ). ISBN10: 5932861037 ISBN13: 9785932861035 ISBN 0596101996 (англ) © Издательство СимволПлюс, 2008 Authorized translation of the English edition © 2006 O’Reilly Media, Inc. This trans lation is published and sold by permission of O’Reilly Media, Inc., the owner of all rights to publish and sell the same. Все права на данное издание защищены Законодательством РФ, включая право на полное или час тичное воспроизведение в любой форме. Все товарные знаки или зарегистрированные товарные зна ки, упоминаемые в настоящем издании, являются собственностью соответствующих фирм. Издательство «СимволПлюс». 199034, СанктПетербург, 16 линия, 7, тел. (812) 3245353, www.symbol.ru. Лицензия ЛП N 000054 от 25.12.98. Налоговая льгота – общероссийский классификатор продукции ОК 00593, том 2; 953000 – книги и брошюры. Подписано в печать 14.02.2008. Формат 70×100 1/16 . Печать офсетная. Объем 62 печ. л. Тираж 2000 экз. Заказ N Отпечатано с готовых диапозитивов в ГУП «Типография «Наука» 199034, СанктПетербург, 9 линия, 12. Эта книга посвящается всем, кто учит жить мирно и противостоит насилию. Оглавление Предисловие . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 1. Введение в JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 1.1. Что такое JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 1.2. Версии JavaScript. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 1.3. Клиентский JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 1.4. Другие области использования JavaScript. . . . . . . . . . . . . . . . . . . . . . . . . . 28 1.5. Изучение JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 Часть I. Основы JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 2. Лексическая структура. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 2.1. Набор символов. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 2.2. Чувствительность к регистру . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 2.3. Символыразделители и переводы строк . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 2.4. Необязательные точки с запятой. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 2.5. Комментарии. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 2.6. Литералы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 2.7. Идентификаторы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 2.8. Зарезервированные слова . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 3. Типы данных и значения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 3.1. Числа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 3.2. Строки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 3.3. Логические значения. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 3.4. Функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 3.5. Объекты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 3.6. Массивы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 3.7. Значение null. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 3.8. Значение undefined . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 3.9. Объект Date . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 3.10. Регулярные выражения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 3.11. Объекты Error. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 3.12. Преобразование типов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 3.13. Объектыобертки для элементарных типов данных. . . . . . . . . . . . . . . . . 58 8 Оглавление 3.14. Преобразование объектов в значения элементарных типов . . . . . . . . . . 60 3.15. По значению или по ссылке . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 4. Переменные . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 4.1. Типизация переменных. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 4.2. Объявление переменных. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 4.3. Область видимости переменной. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 4.4. Элементарные и ссылочные типы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 4.5. Сборка мусора . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 4.6. Переменные как свойства . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 4.7. Еще об области видимости переменных . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 5. Выражения и операторы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 5.1. Выражения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 5.2. Обзор операторов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 5.3. Арифметические операторы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 5.4. Операторы равенства . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 5.5. Операторы отношения. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 5.6. Строковые операторы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 5.7. Логические операторы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 5.8. Поразрядные операторы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 5.9. Операторы присваивания . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 5.10. Прочие операторы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 6. Инструкции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 6.1. Инструкциивыражения. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 6.2. Составные инструкции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 6.3. Инструкция if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 6.4. Инструкция else if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102 6.5. Инструкция switch. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 6.6. Инструкция while. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 6.7. Цикл do/while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 6.8. Инструкция for. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 6.9. Инструкция for/in . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 6.10. Метки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 6.11. Инструкция break . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 6.12. Инструкция continue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 6.13. Инструкция var . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 6.14. Инструкция function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 6.15. Инструкция return. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114 6.16. Инструкция throw . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 6.17. Инструкция try/catch/finally . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 6.18. Инструкция with . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118 6.19. Пустая инструкция . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 Оглавление 9 6.20. Итоговая таблица JavaScriptинструкций. . . . . . . . . . . . . . . . . . . . . . . . 119 7. Объекты и массивы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 7.1. Создание объектов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 7.2. Свойства объектов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 7.3. Объекты как ассоциативные массивы . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125 7.4. Свойства и методы универсального класса Object . . . . . . . . . . . . . . . . . . 127 7.5. Массивы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 7.6. Чтение и запись элементов массива . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130 7.7. Методы массивов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133 7.8. Объекты, подобные массивам. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138 8. Функции. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139 8.1. Определение и вызов функций. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139 8.2. Аргументы функций . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143 8.3. Функции как данные . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148 8.4. Функции как методы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150 8.5. Функцияконструктор. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 8.6. Свойства и методы функций. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 8.7. Практические примеры функций . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 8.8. Область видимости функций и замыкания . . . . . . . . . . . . . . . . . . . . . . . . 156 8.9. Конструктор Function() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163 9. Классы, конструкторы и прототипы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165 9.1. Конструкторы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165 9.2. Прототипы и наследование. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166 9.3. Объектноориентированный язык JavaScript . . . . . . . . . . . . . . . . . . . . . . 172 9.4. Общие методы класса Object. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178 9.5. Надклассы и подклассы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182 9.6. Расширение без наследования . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186 9.7. Определение типа объекта . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189 9.8. Пример: вспомогательный метод defineClass() . . . . . . . . . . . . . . . . . . . . . 194 10. Модули и пространства имен. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198 10.1. Создание модулей и пространств имен . . . . . . . . . . . . . . . . . . . . . . . . . . . 199 10.2. Импорт символов из пространств имен . . . . . . . . . . . . . . . . . . . . . . . . . . 204 10.3. Модуль со вспомогательными функциями . . . . . . . . . . . . . . . . . . . . . . . 208 11. Шаблоны и регулярные выражения. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214 11.1. Определение регулярных выражений . . . . . . . . . . . . . . . . . . . . . . . . . . . 214 11.2. Методы класса String для поиска по шаблону . . . . . . . . . . . . . . . . . . . . 223 11.3. Объект RegExp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226 10 Оглавление 12. Разработка сценариев для Javaприложений . . . . . . . . . . . . . . . . . . . . . 229 12.1. Встраивание JavaScript. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229 12.2. Взаимодействие с Javaкодом . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237 Часть II. Клиентский JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249 13. JavaScript в вебброузерах . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251 13.1. Среда вебброузера . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252 13.2. Встраивание JavaScriptкода в HTMLдокументы . . . . . . . . . . . . . . . . . 258 13.3. Обработчики событий в HTML. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264 13.4. JavaScript в URL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266 13.5. Исполнение JavaScriptпрограмм . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268 13.6. Совместимость на стороне клиента . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273 13.7. Доступность. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279 13.8. Безопасность в JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280 13.9. Другие реализации JavaScript во Всемирной паутине . . . . . . . . . . . . . 285 14. Работа с окнами броузера. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 287 14.1. Таймеры. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288 14.2. Объекты Location и History . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289 14.3. Объекты Window, Screen и Navigator. . . . . . . . . . . . . . . . . . . . . . . . . . . . 291 14.4. Методы управления окнами. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297 14.5. Простые диалоговые окна. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 302 14.6. Строка состояния . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 303 14.7. Обработка ошибок . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 304 14.8. Работа с несколькими окнами и фреймами . . . . . . . . . . . . . . . . . . . . . . . 306 14.9. Пример: панель навигации во фрейме . . . . . . . . . . . . . . . . . . . . . . . . . . . 311 15. Работа с документами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 314 15.1. Динамическое содержимое документа . . . . . . . . . . . . . . . . . . . . . . . . . . . 315 15.2. Свойства объекта Document . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 317 15.3. Ранняя упрощенная модель DOM: коллекции объектов документа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319 15.4. Обзор объектной модели W3C DOM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323 15.5. Обход документа. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 334 15.6. Поиск элементов в документе . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 335 15.7. Модификация документа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339 15.8. Добавление содержимого в документ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343 15.9. Пример: динамическое создание оглавления . . . . . . . . . . . . . . . . . . . . . 351 15.10. Получение выделенного текста . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356 15.11. IE 4 DOM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357 Оглавление 11 16. CSS и DHTML. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 360 16.1. Обзор CSS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 361 16.2. CSS для DHTML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 370 16.3. Использование стилей в сценариях . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 386 16.4. Вычисляемые стили. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395 16.5. CSSклассы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396 16.6. Таблицы стилей . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 397 17. События и обработка событий. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403 17.1. Базовая обработка событий . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 404 17.2. Развитые средства обработки событий в модели DOM Level 2 . . . . . . . 414 17.3. Модель обработки событий Internet Explorer . . . . . . . . . . . . . . . . . . . . . 425 17.4. События мыши . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435 17.5. События клавиатуры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 440 17.6. Событие onload . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 449 17.7. Искусственные события . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 450 18. Формы и элементы форм . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 453 18.1. Объект Form . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 454 18.2. Определение элементов формы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 455 18.3. Сценарии и элементы формы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 459 18.4. Пример верификации формы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 467 19. Cookies и механизм сохранения данных на стороне клиента . . . . . . . 472 19.1. Обзор cookies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 472 19.2. Сохранение cookie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 475 19.3. Чтение cookies. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 476 19.4. Пример работы с cookie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477 19.5. Альтернативы cookies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 481 19.6. Хранимые данные и безопасность . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 493 20. Работа с протоколом HTTP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 494 20.1. Использование объекта XMLHttpRequest . . . . . . . . . . . . . . . . . . . . . . . . 495 20.2. Примеры и утилиты с объектом XMLHttpRequest. . . . . . . . . . . . . . . . . 502 20.3. Ajax и динамические сценарии . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 509 20.4. Взаимодействие с протоколом HTTP с помощью тега После загрузки в броузер, поддерживающий JavaScript, этот сценарий выдаст результат, показанный на рис. 1.1. 24 Глава 1. Введение в JavaScript Как видно из этого примера, для встраивания JavaScriptкода в HTMLфайл были использованы теги . О теге 1.4. Другие области использования JavaScript JavaScript – это язык программирования общего назначения, и его использова ние не ограничено вебброузерами. Изначально JavaScript разрабатывался с прицелом на встраивание в любые приложения и предоставление возможности исполнять сценарии. С самых первых дней вебсерверы компании Netscape включали в себя интерпретатор JavaScript, что позволяло исполнять JavaScript сценарии на стороне сервера. Аналогичным образом в дополнение к Internet Ex plorer корпорация Microsoft использует интерпретатор JScript в своем вебсер вере IIS и в продукте Windows Scripting Host. Компания Adobe задействует про изводный от JavaScript язык для управления своим проигрывателем Flashфай лов. Компания Sun также встроила интерпретатор JavaScript в дистрибутив Java 6.0, что существенно облегчает возможность встраивания сценариев в лю бое Javaприложение (о том, как это делается, рассказывается в главе 12). И Netscape, и Microsoft сделали доступными свои реализации интерпретаторов JavaScript для компаний и программистов, желающих включить их в свои при ложения. Интерпретатор, созданный в компании Netscape, был выпущен как свободно распространяемое ПО с открытыми исходными текстами и ныне досту пен через организацию Mozilla (http://www.mozilla.org/js/). Mozilla фактически распространяет две разные версии интерпретатора JavaScript 1.5: один написан на языке C и называется SpiderMonkey, другой написан на языке Java и, что весьма лестно для автора книги, называется Rhino (носорог). Если вам придется писать сценарии для приложений, включающих интерпрета тор JavaScript, первая половина книги, где описываются основы этого языка, будет для вас особенно полезна. Однако информация из глав, в которых описы ваются особенности конкретных вебброузеров, скорее всего, будет непримени ма для ваших сценариев. 1.5. Изучение JavaScript 29 1.5. Изучение JavaScript Реальное изучение нового языка программирования невозможно без написания программ. Рекомендую вам при чтении этой книги опробовать возможности Java Script в процессе их изучения. Вот несколько приемов, призванных облегчить эти эксперименты. Наиболее очевидный подход к изучению JavaScript – это написание простых сценариев. Одно из достоинств клиентского JavaScript состоит в том, что любой, кто имеет вебброузер и простейший текстовый редактор, имеет и полноценную среду разработки. Для того чтобы начать писать программы на JavaScript, нет необходимости в покупке или загрузке специального ПО. Например, чтобы вместо факториалов вывести последовательность чисел Фибо наччи, пример 1.1 можно переписать следующим образом: Этот отрывок может показаться запутанным (и не волнуйтесь, если вы пока не понимаете его), но для того чтобы поэкспериментировать с подобными коротки ми программами, достаточно набрать код и запустить его в вебброузере в каче стве файла с локальным URLадресом. Обратите внимание, что для вывода ре зультатов вычислений используется метод document.write(). Это полезный прием при экспериментах с JavaScript. В качестве альтернативы для отображения тек стового результата в диалоговом окне можно применять метод alert(): alert("Fibonacci (" + i + ") = " + fib); Отметьте, что в подобных простых экспериментах с JavaScript можно опускать теги , и в HTMLфайле. Для еще большего упрощения экспериментов с JavaScript можно использовать URLадрес со спецификатором псевдопротокола javascript: для вычисления зна чения JavaScriptвыражения и получения результата. Такой URLадрес состоит из спецификатора псевдопротокола (javascript:), за которым указывается произ вольный JavaScriptкод (инструкции отделяются одна от другой точками с запя той). Загружая URLадрес с псевдопротоколом, броузер просто исполняет Java Scriptкод. Значение последнего выражения в таком URLадресе преобразуется в строку, и эта строка выводится вебброузером в качестве нового документа. На пример, для того чтобы проверить свое понимание некоторых операторов и инст рукций языка JavaScript, можно набрать следующие URLадреса в адресном по ле вебброузера: javascript:5%2 javascript:x = 3; (x < 5)? "значение x меньше": "значение x больше" javascript:d = new Date(); typeof d; javascript:for(i=0,j=1,k=0,fib=1; i<5; i++,fib=j+k,k=j,j=fib) alert(fib); javascript:s=""; for(i in navigator) s+=i+":"+navigator[i]+"\n"; alert(s); 30 Глава 1. Введение в JavaScript В вебброузере Firefox однострочные сценарии вводятся в JavaScriptконсоли, доступ к которой можно получить из меню Инструменты. Просто введите выраже ние или инструкцию, которую требуется проверить. При использовании Java Scriptконсоли спецификатор псевдопротокола (javascript:) можно опустить. Не любой код, написанный вами при изучении JavaScript, будет работать так, как ожидается, и вам захочется его отладить. Базовая методика отладки Java Scriptкода совпадает с методикой для многих других языков: вставка в код ин струкций, которые будут выводить значения нужных переменных так, чтобы можно было понять, что же на самом деле происходит. Как мы уже видели, ино гда для этих целей можно использовать метод document.write() или alert(). (Бо лее сложный способ отладки, основанный на выводе отладочных сообщений в файл, приводится в примере 15.9.) В отладке также может быть полезен цикл for/in (описанный в главе 6). Напри мер, его можно применять вместе с методом alert() для написания функции, отображающей имена и значения всех свойств объекта. Такая функция может быть удобна при изучении языка или при отладке кода. Если вам постоянно приходится сталкиваться с ошибками в JavaScriptсценари ях, вероятно, вас заинтересует настоящий отладчик JavaScript. В Internet Explo rer можно воспользоваться отладчиком Microsoft Script Debugger, в Firefox – мо дулем расширения, известным под названием Venkman. Описание этих инстру ментов выходит далеко за рамки темы этой книги, но вы без труда найдете его в Интернете, воспользовавшись какойнибудь поисковой системой. Еще один ин струмент, который, строго говоря, не является отладчиком, – это jslint; он спосо бен отыскивать распространенные ошибки в JavaScriptкоде программ (http:// jslint.com). Основы JavaScript Данная часть книги включает главы со 2 по 12 и описывает базовый язык Java Script. Этот материал задуман как справочный, и прочитав главы этой части один раз, вы, возможно, будете неоднократно возвращаться к ним, чтобы осве жить в памяти некоторые особенности языка. • Глава 2 «Лексическая структура» • Глава 3 «Типы данных и значения» • Глава 4 «Переменные» • Глава 5 «Выражения и операторы» • Глава 6 «Инструкции» • Глава 7 «Объекты и массивы» • Глава 8 «Функции» • Глава 9 «Классы, конструкторы и прототипы» • Глава 10 «Модули и пространства имен» • Глава 11 «Шаблоны и регулярные выражения» • Глава 12 «Разработка сценариев для Javaприложений» Лексическая структура Лексическая структура языка программирования – это набор элементарных правил, определяющих, как пишутся программы на этом языке. Это низкоуров невый синтаксис языка; он задает вид имен переменных, символы, используе мые для комментариев, и то, как одна инструкция отделяется от другой. Эта ко роткая глава документирует лексическую структуру JavaScript. 2.1. Набор символов При написании программ на JavaScript используется набор символов Unicode. В отличие от 7разрядной кодировки ASCII, подходящей только для английско го языка, и 8разрядной кодировки ISO Latin1, подходящей только для англий ского и основных западноевропейских языков, 16разрядная кодировка Unicode обеспечивает представление практически любого письменного языка. Эта воз можность важна для интернационализации и особенно для программистов, не говорящих на английском языке. Американские и другие англоговорящие программисты обычно пишут програм мы с помощью текстового редактора, поддерживающего только кодировки ASCII или Latin1, и потому у них нет простого доступа к полному набору символов Unicode. Однако никаких трудностей это не порождает, поскольку кодировки ASCII и Latin1 представляют собой подмножества Unicode, и любая JavaScript программа, написанная с помощью этих наборов символов, абсолютно коррект на. Программисты, привыкшие рассматривать символы как 8разрядные значе ния, могут быть сбиты с толку, узнав, что JavaScript представляет каждый сим вол с помощью двух байтов, однако на самом деле для программиста это обстоя тельство остается незаметным и может просто игнорироваться. Стандарт ECMAScript v3 допускает наличие Unicodeсимволов в любом месте JavaScriptпрограммы. Однако версии 1 и 2 стандарта допускают использование Unicodeсимволов только в комментариях и строковых литералах, заключен ных в кавычки, все остальные составляющие программы ограничены набором 34 Глава 2. Лексическая структура ASCIIсимволов.1 Версии JavaScript, предшествующие стандарту ECMAScript, обычно вообще не поддерживают Unicode. 2.2. Чувствительность к регистру JavaScript – это язык, чувствительный к регистру. Это значит, что ключевые слова, переменные, имена функций и любые другие идентификаторы языка должны всегда содержать одинаковые наборы прописных и строчных букв. На пример, ключевое слово while должно набираться как «while», а не «While» или «WHILE». Аналогично online, Online, OnLine и ONLINE – это имена четырех разных переменных. Заметим, однако, что язык HTML, в отличие от JavaScript, не чувствителен к ре гистру. По причине близкой связи HTML и клиентского JavaScript это различие может привести к путанице. Многие JavaScriptобъекты и их свойства имеют те же имена, что и теги и атрибуты языка HTML, которые они обозначают. Если в HTML эти теги и атрибуты могут набираться в любом регистре, то в JavaScript они обычно должны набираться строчными буквами. Например, атрибут обра ботчика события onclick чаще всего задается в HTML как onClick, однако в Java Scriptкоде (или в XHTMLдокументе) он должен быть обозначен как onclick. 2.3. Символыразделители и переводы строк JavaScript игнорирует пробелы, табуляции и переводы строк, присутствующие между лексемами в программе. Поэтому символы пробела, табуляции и перевода строки могут без ограничений использоваться в исходных текстах программ для форматирования и придания им удобочитаемого внешнего вида. Однако имеется небольшое ограничение, которое касается символов перевода строк и о котором рассказывается в следующем разделе. 2.4. Необязательные точки с запятой Простые JavaScriptинструкции обычно завершаются символами точки с запя той (;), как в C, C++ и Java. Точка с запятой служит для отделения инструкций друг от друга. Однако в JavaScript точку с запятой можно не ставить, если каж дая инструкция помещается в отдельной строке. Например, следующий фраг мент может быть записан без точек с запятой: a = 3; b = 4; 1 Для русскоязычных программистов это означает, что а) русскоязычный текст мо жет появляться только в комментариях и в строковых литералах, предназначен ных непосредственно для вывода; б) такие тексты представляются в кодировке UTF16 (Unicode – это единая система связывания символов любого языка с одно значным численным кодом, а для кодирования этого численного кода могут при меняться различные кодировки, например UTF8, UTF16 и др.); в) все остальные лексемы программы – операторы, имена переменных и т. д. – должны состоять из латинских литер; это достаточно обычная и привычная практика и для других языков программирования. – Примеч. науч. ред. 2.5. Комментарии 35 Однако если обе инструкции расположены в одной строке, то первая точка с за пятой должна присутствовать обязательно: a = 3; b = 4; Пропуск точек с запятой нельзя признать правильной практикой программиро вания, и поэтому желательно выработать привычку их использовать. Теоретически JavaScript допускает переводы строк между любыми двумя лексе мами, но привычка синтаксического анализатора JavaScript автоматически вставлять точки с запятой за программиста приводит к некоторым исключени ям из этого правила. Если в результате разделения строки программного кода та ее часть, которая предшествует символу перевода, оказывается законченной ин струкцией, синтаксический анализатор JavaScript может решить, что точка с запятой пропущена случайно, и вставить ее, изменив смысл программы. К по добным требующим внимания ситуациям относятся, среди прочих, инструкции return, break и continue (описанные в главе 6). Рассмотрим, например, следую щий фрагмент: return true; Синтаксический анализатор JavaScript предполагает, что программист имеет в виду следующее: return; true; Хотя на самом деле программист, видимо, хотел написать return true; Вот случай, когда следует быть внимательным, – данный код не вызовет синтак сической ошибки, но приведет к неочевидному сбою. Похожая неприятность возникает, если написать: break outerloop; JavaScript вставляет точку с запятой после ключевого слова break, что вызывает синтаксическую ошибку при попытке интерпретировать следующую строку. По аналогичным причинам постфиксные операторы ++ и  (см. главу 5) должны располагаться в той же строке, что и выражения, к которым они относятся. 2.5. Комментарии JavaScript, как и Java, поддерживает комментарии и в стиле C++, и в стиле C. Любой текст, присутствующий между символами // и концом строки, рассмат ривается как комментарий и игнорируется JavaScript. Любой текст между сим волами /* и */ также рассматривается как комментарий. Эти комментарии в сти ле C могут состоять из нескольких строк и не могут быть вложенными. Следую щие строки кода представляют собой корректные JavaScriptкомментарии: // Это однострочный комментарий. /* Это тоже комментарий */ // а это другой комментарий. /* 36 Глава 2. Лексическая структура * Это еще один комментарий. * Он располагается в нескольких строках. */ 2.6. Литералы Литерал – это значение, указанное непосредственно в тексте программы. Ниже приведены примеры литералов: 12 // Число двенадцать 1.2 // Число одна целая две десятых "hello world" // Строка текста 'Hi' // Другая строка true // Логическое значение false // Другое логическое значение /javascript/gi // Регулярное выражение (для поиска по шаблону) null // Отсутствие объекта В ECMAScript v3 также поддерживаются выражения, которые могут служить в качестве массивовлитералов и объектовлитералов. Например: { x:1, y:2 } // Инициализатор объекта [1,2,3,4,5] // Инициализатор массива Литералы – важная часть любого языка программирования, поскольку напи сать программу без них невозможно. Различные литералы JavaScript описаны в главе 3. 2.7. Идентификаторы Идентификатор – это просто имя. В JavaScript идентификаторы выступают в качестве названий переменных и функций, а также меток некоторых циклов. Правила формирования допустимых идентификаторов совпадают с правилами Java и многих других языков программирования. Первым символом должна быть буква, символ подчеркивания (_) или знак доллара ($).1 Последующие символы могут быть любой буквой, цифрой, символом подчеркивания или знаком долла ра. (Цифра не может быть первым символом, т. к. тогда интерпретатору труднее отличать идентификаторы от чисел.) Примеры допустимых идентификаторов: i my_variable_name v13 _dummy $str В ECMAScript v3 идентификаторы могут содержать буквы и цифры из полного набора символов Unicode. До этой версии стандарта JavaScriptидентификаторы были ограничены набором ASCII. ECMAScript v3 также допускает наличие 1 Знак $ недопустим в идентификаторах для более ранних версий, чем Java Script 1.1. Этот знак предназначен только для средств генерации кода, поэтому следует избегать его использования в идентификаторах. 2.8. Зарезервированные слова 37 в идентификаторах escapeпоследовательностей Unicode – символов \u, за кото рыми расположены 4 шестнадцатеричные цифры, обозначающие 16разрядный код символа. Например, идентификатор π можно записать как \u03c0. Этот син таксис неудобен, но обеспечивает возможность транслитерации JavaScriptпро грамм с Unicodeсимволами в форму, допускающую работу с ними в текстовых редакторах и других средствах, не поддерживающих полный набор Unicode. Наконец, идентификаторы не могут совпадать ни с одним из ключевых слов, предназначенных в JavaScript для других целей. В следующем разделе перечис лены ключевые слова, зарезервированные для специальных нужд JavaScript. 2.8. Зарезервированные слова В JavaScript имеется несколько зарезервированных слов. Они не могут быть идентификаторами (именами переменных, функций и меток циклов) в Java Scriptпрограммах. В табл. 2.1 перечислены ключевые слова, стандартизован ные в ECMAScript v3. Для интерпретатора JavaScript они имеют специальное значение, т. к. являются частью синтаксиса языка. Таблица 2.1. Зарезервированные ключевые слова JavaScript В табл. 2.2 перечислены другие ключевые слова. В настоящее время они в Java Script не используются, но зарезервированы ECMAScript v3 в качестве возмож ных будущих расширений языка. Таблица 2.2. Слова, зарезервированные для расширений ECMA Помимо нескольких только что перечисленных формально зарезервированных слов текущие проекты стандарта ECMAScript v4 рассматривают применение ключевых слов as, is, namespace и use. Хотя текущие интерпретаторы JavaScript не запрещают использование этих четырех слов в качестве идентификаторов, однако все равно следует этого избегать. break do if switch typeof case else in this var catch false instanceof throw void continue finally new true while default for null try with delete function return abstract double goto native static Boolean enum implements package super byte export import private synchronized char extends int protected throws class final interface public transient const float long short volatile debugger 38 Глава 2. Лексическая структура Кроме того, следует избегать использования идентификаторов глобальных пере менных и функций, предопределенных в языке JavaScript. Если попытаться создать переменную или функцию с таким идентификатором, то это будет при водить либо к ошибке (если свойство определено как доступное только для чте ния), либо к переопределению глобальной переменной или функции, чего точно не стоит делать, если вы не стремитесь к этому преднамеренно. В табл. 2.3 пере числены имена глобальных переменных и функций, определяемых стандартом ECMAScript v 3. Конкретные реализации могут содержать свои предопределен ные элементы с глобальной областью видимости, кроме того, каждая конкрет ная платформа JavaScript (клиентская, серверная и прочие) может еще больше расширять этот список.1 Таблица 2.3. Другие идентификаторы, которых стоит избегать 1 При описании объекта Window в четвертой части книги приведен список глобаль ных переменных и функций, определенных в клиентском JavaScript. arguments encodeURI Infinity Object String Array Error isFinite parseFloat SyntaxError Boolean escape isNaN parseInt TypeError Date eval Math RangeError undefined decodeURI EvalError NaN ReferenceError unescape decodeURIcomponent Function Number RegExp URIError Типы данных и значения Компьютерные программы работают, манипулируя значениями (values), таки ми как число 3,14 или текст «Hello World». Типы значений, которые могут быть представлены и обработаны в языке программирования, известны как типы данных (data types), и одной из наиболее фундаментальных характеристик язы ка программирования является поддерживаемый им набор типов данных. Java Script позволяет работать с тремя элементарными типами данных: числами, строками текста (или просто строками) и значениями логической истинности (или просто логическими значениями). В JavaScript также определяются два тривиальных типа данных, null и undefined, каждый из которых определяет только одно значение. В дополнение к этим элементарным типам данных JavaScript поддерживает со ставной тип данных, известный как объект (object). Объект (т. е. член объектно го типа данных) представляет собой коллекцию значений (либо элементарных, таких как числа и строки, либо сложных, например других объектов). Объекты в JavaScript имеют двойственную природу: объект может быть представлен как неупорядоченная коллекция именованных значений или как упорядоченная коллекция пронумерованных значений. В последнем случае объект называется массивом (array). Хотя в JavaScript объекты и массивы в основе являются од ним типом данных, они ведут себя совершенно поразному, и в этой книге рас сматриваются как отдельные типы. В JavaScript определен еще один специальный тип объекта, известный как функ' ция (function). Функция – это объект, с которым связан исполняемый код. Функ ция может вызываться (invoked) для выполнения определенной операции. По добно массивам, функции ведут себя не так, как другие виды объектов, и в Java Script определен специальный синтаксис для работы с ними. Поэтому мы будем рассматривать функции независимо от объектов и массивов. Помимо функций и массивов в базовом языке JavaScript определено еще не сколько специальных видов объектов. Эти объекты представляют собой не новые типы данных, а лишь новые классы (classes) объектов. Класс Date определяет объ екты, представляющие даты, класс RegExp – объекты, представляющие регуляр 40 Глава 3. Типы данных и значения ные выражения (мощное средство поиска по шаблону, описываемое в главе 11), и класс Error – объекты, представляющие синтаксические ошибки и ошибки времени выполнения, которые могут возникать в JavaScriptпрограмме. В оставшейся части этой главы подробно описан каждый из элементарных типов данных. В ней также приведены начальные сведения об объектах, массивах и функциях, которые более подробно рассмотрены в главах 7 и 8. И наконец, в ней приведен обзор классов Date, RegExp и Error, подробно документируемых в III час ти книги. Глава содержит некоторые узкоспециализированные подробности, ко торые можно пропустить при первом прочтении. 3.1. Числа Числа – это основной тип данных, не требующий особых пояснений. JavaScript отличается от таких языков программирования, как C и Java, тем, что не делает различия между целыми и вещественными значениями. Все числа в JavaScript представляются 64разрядными вещественными значениями (с плавающей точ кой), формат которых определяется стандартом IEEE 754.1 Этот формат спосо бен представлять числа от ±1,7976931348623157 × 10308 до ±5 × 10324. Число, находящееся непосредственно в коде JavaScriptпрограммы, называется числовым литералом. JavaScript поддерживает числовые литералы нескольких форматов, описанных в последующих разделах. Обратите внимание: любому чи словому литералу может предшествовать знак «минус» (), делающий числа от рицательными. Однако фактически минус представляет собой унарный опера тор смены знака (см. главу 5), не являющийся частью синтаксиса числовых ли тералов. 3.1.1. Целые литералы В JavaScript целые десятичные числа записываются как последовательность цифр. Например: 0 3 10000000 Числовой формат JavaScript позволяет точно представлять все целые числа в диа пазоне от –9007199254740992 (–253) до 9007199254740992 (253) включительно. Для целых значений вне этого диапазона может теряться точность в младших раз рядах. Следует отметить, что некоторые целые операции в JavaScript (в особенно сти битовые операторы, описанные в главе 5) выполняются с 32разрядными це лыми, принимающими значения от –2147483648 (–231) до 2147483647 (231–1). 3.1.2. Шестнадцатеричные и восьмеричные литералы Помимо десятичных целых литералов JavaScript распознает шестнадцатерич ные значения (по основанию 16). Шестнадцатеричные литералы начинаются с последовательности символов «0x» или «0X», за которой следует строка шест 1 Этот формат должен быть знаком Javaпрограммистам как формат типа double. Это также формат double почти во всех современных реализациях C и C++. 3.1. Числа 41 надцатеричных цифр. Шестнадцатеричная цифра – это одна из цифр от 0 до 9 или букв от a (или A) до f (или F), представляющих значения от 10 до 15. Ниже приводятся примеры шестнадцатеричных целых литералов: 0xff // 15*16 + 15 = 255 (по основанию 10) 0xCAFE911 Хотя стандарт ECMAScript не поддерживает представление целых литералов в восьмеричном формате (по основанию 8), некоторые реализации JavaScript до пускают подобную возможность. Восьмеричный литерал начинается с цифры 0, за ней следуют цифры, каждая из которых может быть от 0 до 7. Например: 0377 // 3*64 + 7*8 + 7 = 255 (по основанию 10) Поскольку некоторые реализации поддерживают восьмеричные литералы, а не которые нет, никогда не следует писать целый литерал с ведущим нулем, ибо нельзя сказать наверняка, как он будет интерпретирован данной реализацией – как восьмеричное число или как десятичное. 3.1.3. Литералы вещественных чисел Литералы вещественных чисел должны иметь десятичную точку; в них использу ется традиционный синтаксис вещественных чисел. Вещественное значение представлено как целая часть числа, за которой следуют десятичная точка и дроб ная часть числа. Литералы вещественных чисел могут также представляться в экспоненциаль ной нотации: вещественное число, за которым следует буква e (или E), а затем необязательный знак плюс или минус и целая экспонента. Эта нотация обозна чает вещественное число, умноженное на 10 в степени, определяемой значением экспоненты. Более лаконичное определение синтаксиса таково: [цифры][.цифры][(E|e)[(+|)]цифры] Например: 3.14 2345.789 .333333333333333333 6.02e23 // 6.02 X 1023 1.4738223E32 // 1.4738223 X 1032 Обратите внимание: вещественных чисел существует бесконечно много, но фор мат представления вещественных чисел в JavaScript позволяет точно выразить лишь ограниченное их количество (точнее 18437736874454810627). Это значит, что при работе с вещественными числами в JavaScript представление числа час то будет округлением реального числа. Точность округления, как правило, до статочна и на практике редко приводит к ошибкам. 3.1.4. Работа с числами Для работы с числами в JavaScriptпрограммах используются поддерживаемые языком арифметические операторы, к которым относятся операторы сложения 42 Глава 3. Типы данных и значения (+), вычитания (), умножения (*) и деления (/). Подробное описание этих и дру гих арифметических операторов имеется в главе 5. Помимо перечисленных основных арифметических операторов JavaScript под держивает выполнение более сложных математических операций с помощью большого количества математических функций, относящихся к базовой части языка. Для удобства эти функции хранятся в виде свойств одного объекта Math, и для доступа к ним всегда используется литеральное имя Math. Например, синус числового значения переменной x можно вычислить следующим образом: sine_of_x = Math.sin(x); А так вычисляется квадратный корень числового выражения: hypot = Math.sqrt(x*x + y*y); Подробные сведения обо всех математических функциях, поддерживаемых Java Script, приведены в описании объекта Math и соответствующих листингах треть ей части книги. 3.1.5. Преобразования чисел В языке JavaScript имеется возможность представлять числа в виде строк и пре образовывать строки в числа. Порядок этих преобразований описывается в раз деле 3.2. 3.1.6. Специальные числовые значения В JavaScript определено несколько специальных числовых значений. Когда ве щественное число превышает самое большое представимое конечное значение, результату присваивается специальное значение бесконечности, которое в Java Script обозначается как Infinity. А когда отрицательное число становится мень ше наименьшего представимого отрицательного числа, результатом является отрицательная бесконечность, обозначаемая как Infinity. Еще одно специальное числовое значение возвращается JavaScript, когда мате матическая операция (например, деление нуля на ноль) приводит к неопреде ленному результату или ошибке. В этом случае результатом является специаль ное значение «нечисло», обозначаемое как NaN. «Нечисло» (NotaNumber) ведет себя необычно: оно не равно ни одному другому числу, в том числе и самому себе! По данной причине для проверки на это значение имеется специальная функция isNaN(). Похожая функция, isFinite(), позволяет проверить число на неравенст во NaN или положительной/отрицательной бесконечности. В табл. 3.1 приведено несколько констант, определенных в JavaScript для обо значения специальных числовых значений. Таблица 3.1. Специальные числовые константы Константа Значение Infinity Специальное значение, обозначающее бесконечность NaN Специальное значение – «нечисло» Number.MAX_VALUE Максимальное представимое значение 3.2. Строки 43 Константы Infinity и NaN, определенные в ECMAScript v1, не были реализованы вплоть до JavaScript 1.3. Однако различные константы Number реализованы на чиная с JavaScript 1.1. 3.2. Строки Строка представляет собой последовательность букв, цифр, знаков пунктуации и прочих Unicodeсимволов и является типом данных JavaScript для представ ления текста. Как вы скоро увидите, строковые литералы можно использовать в своих программах, заключая их в согласованные пары одинарных или двой ных кавычек. Обратите внимание: в JavaScript нет символьного типа данных, такого как char в C, C++ и Java. Одиночный символ представлен строкой единич ной длины. 3.2.1. Строковые литералы Строковый литерал – это последовательность из нуля или более Unicodeсимво лов, заключенная в одинарные или двойные кавычки (' или "). Сами символы двойных кавычек могут содержаться в строках, ограниченных символами оди нарных кавычек, а символы одинарных кавычек – в строках, ограниченных символами двойных кавычек. Строковые литералы должны записываться в од ной строке программы и не могут разбиваться на две строки. Чтобы включить в строковый литерал символ перевода строки, следует использовать последова тельность символов \n, описание которой приведено в следующем разделе. При меры строковых литералов: "" // Это пустая строка: в ней ноль символов 'testing' "3.14" 'name="myform"' "Вы предпочитаете книги издательства O'Reilly, не правда ли?" "В этом строковом литерале\nдве строки" "π  это отношение длины окружности круга к его диаметру" Как иллюстрирует последний пример строки, стандарт ECMAScript v1 допуска ет Unicodeсимволы в строковых литералах. Однако в реализациях, более ран них, чем JavaScript 1.3, в строках обычно поддерживаются только символы из набора ASCII или Latin1. Как мы увидим в следующем разделе, Unicodeсимво лы также можно включать в строковые литералы с помощью специальных управляющих последовательностей. Это может потребоваться, если в тексто вом редакторе отсутствует полноценная поддержка Unicode. Обратите внимание: ограничивая строку одинарными кавычками, необходимо проявлять осторожность в обращении с апострофами, употребляемыми в анг Number.MIN_VALUE Наименьшее (ближайшее к нулю) представимое значение Number.NaN Специальное значение – «нечисло» Number.POSITIVE_INFINITY Специальное значение, обозначающее плюс бесконечность Number.NEGATIVE_INFINITY Специальное значение, обозначающее минус бесконечность Константа Значение 44 Глава 3. Типы данных и значения лийском языке для обозначения притяжательного падежа и в сокращениях, как, например, в словах «can't» и «O'Reilly's». Поскольку апостроф и одиночная кавычка – это одно и то же, необходимо при помощи символа обратного слэша (\) экранировать апострофы, расположенные внутри одиночных кавычек (под робнее об этом – в следующем разделе). Программы на клиентском JavaScript часто содержат строки HTMLкода, а HTML код, в свою очередь, часто содержит строки JavaScriptкода. Как и в JavaScript, в HTML для ограничения строк применяются либо одинарные, либо двойные ка вычки. Поэтому при объединении JavaScript и HTMLкода есть смысл придер живаться одного «стиля» кавычек для JavaScript, а другого – для HTML. В сле дующем примере строка «Спасибо» в JavaScriptвыражении заключена в оди нарные кавычки, а само выражение, в свою очередь, заключено в двойные ка вычки как значение HTMLатрибута обработчика событий: Щелкни на мне 3.2.2. Управляющие последовательности в строковых литералах Символ обратного слэша (\) имеет специальное назначение в JavaScriptстроках. Вместе с символами, следующими за ним, он обозначает символ, не представи мый внутри строки другими способами. Например, \n – это управляющая после' довательность (escape sequence), обозначающая символ перевода строки.1 Другой пример, упомянутый в предыдущем разделе, – это последовательность \', обозначающая символ одинарной кавычки. Эта управляющая последова тельность необходима для включения символа одинарной кавычки в строковый литерал, заключенный в одинарные кавычки. Теперь становится понятно, поче му мы называем эти последовательности управляющими – здесь символ обрат ного слэша позволяет управлять интерпретацией символа одинарной кавычки. Вместо того чтобы отмечать ею конец строки, мы используем ее как апостроф: 'You\'re right, it can\'t be a quote' В табл. 3.2 перечислены управляющие последовательности и обозначаемые ими символы. Две управляющие последовательности являются обобщенными; они могут применяться для представления любого символа путем указания кода символа из набора Latin1 или Unicode в виде шестнадцатеричного числа. Напри мер, последовательность \xA9 обозначает символ копирайта, который в кодиров ке Latin1 имеет шестнадцатеричный код A9. Аналогично управляющая последо вательность, начинающаяся с символов \u, обозначает произвольный Unicode символ, заданный четырьмя шестнадцатеричными цифрами. Например, \u03c0 обозначает символ π. Следует отметить, что управляющие последовательности для обозначения Unicodeсимволов требуются по стандарту ECMAScript v1, но обычно не поддерживаются в реализациях, вышедших ранее чем JavaScript 1.3. Некоторые реализации JavaScript также допускают задание символа Latin1 тремя восьмеричными символами, указанными после символа обратного слэша, 1 Тем, кто программирует на C, C++ и Java, эта и другие управляющие последова тельности JavaScript уже знакомы. 3.2. Строки 45 но такие управляющие последовательности не поддерживаются в стандарте ECMAScript v3 и не должны использоваться. Таблица 3.2. Управляющие последовательности JavaScript И наконец, следует заметить, что символ обратного слэша не может предшество вать символу перевода строки для продолжения строки (или другой JavaScript лексемы) на следующей строке или включения буквального перевода строки в строковый литерал. Если символ «\» предшествует любому символу, отлично му от приведенных в табл. 3.2, обратный слэш просто игнорируется (хотя буду щие версии могут, конечно, определять новые управляющие последовательно сти). Например, \# – это то же самое, что и #. 3.2.3. Работа со строками Одной из встроенных возможностей JavaScript является способность конкатени ровать строки. Если оператор + применяется к числам, они складываются, а ес ли к строкам, они объединяются, при этом вторая строка добавляется в конец первой. Например: msg = "Hello, " + "world"; // Получается строка "Hello, world" greeting = "Добро пожаловать на мою домашнюю страницу," + " " + name; Для определения длины строки – количества содержащихся в ней символов – используется свойство length. Так, если переменная s содержит строку, то длину последней можно получить следующим образом: s.length Для работы со строками существует несколько методов. Так можно получить по следний символ в строке s: Константа Значение \0 Символ NUL (\u0000) \b «Забой» (\u0008) \t Горизонтальная табуляция (\u0009) \n Перевод строки (\u000A) \v Вертикальная табуляция (\u000B) \f Перевод страницы (\u000C) \r Возврат каретки (\u000D) \" Двойная кавычка (\u0022) \' Одинарная кавычка (\u0027) \\ Обратный слэш (\u005C) \xXX Символ Latin1, заданный двумя шестнадцатеричными цифрами XX \uxXXXX Unicodeсимвол, заданный четырьмя шестнадцатеричными цифрами XXXX \XXX Символ из набора Latin1, заданный тремя восьмеричными цифрами XXX, с кодом в диапазоне от 1 до 377. Не поддерживается ECMAScript v3; такой способ записи не должен использоваться 46 Глава 3. Типы данных и значения last_char = s.charAt(s.length – 1) Чтобы извлечь второй, третий и четвертый символы из строки s, применяется инструкция: sub = s.substring(1,4); Определить позицию первого символа «a» в строке s можно следующим образом: i = s.indexOf('a'); Есть и еще ряд методов, которые можно использовать при работе со строками. Полностью эти методы документированы в описании объекта String и в листин гах третьей части книги. Из предыдущих примеров можно понять, что JavaScriptстроки (и, как мы уви дим позднее, массивы JavaScript) индексируются, начиная с 0. Другими слова ми, порядковый номер первого символа строки равен нулю. Программистам, ра ботавшим с C, С++ и Java, должно быть удобно это соглашение, однако програм мистам, привыкшим к языкам, в которых нумерация строк и массивов начина ется с единицы, придется какоето время привыкать к этому. В некоторых реализациях JavaScript отдельные символы могут извлекаться из строк (но не записываться в строки) при обращении к строкам как к массивам, в результате приведенный ранее вызов метода charAt() может быть записан сле дующим образом: last_char = s[s.length – 1]; Однако этот синтаксис не стандартизован в ECMAScript v3, не является перено симым и его следует избегать. Когда мы будем обсуждать объектный тип данных, вы увидите, что свойства и методы объектов используются так же, как в предыдущих примерах свойства и методы строк. Это не значит, что строки – это тип объектов. На самом деле строки – это отдельный тип данных JavaScript. Для доступа к их свойствам и ме тодам используется объектный синтаксис, но сами они объектами не являются. Почему это так, мы узнаем в конце данной главы. 3.2.4. Преобразование чисел в строки Преобразование чисел в строки производится автоматически, по мере необходи мости. Например, если число используется в операции конкатенации строк, оно будет преобразовано в строку: var n = 100; var s = n + " бутылок пива на стене."; Такая способность JavaScript к автоматическому преобразованию числа в стро ку реализует идиому программирования, которую часто можно встретить на практике: чтобы преобразовать число в строку, достаточно просто сложить его с пустой строкой: var n_as_string = n + ""; Для явного преобразования числа в строку используется функция String(): var string_value = String(number); 3.2. Строки 47 Еще один способ преобразования числа в строку заключается в вызове метода toString(): string_value = number.toString( ); Метод toString() объекта Number (примитивы чисел автоматически преобразуют ся в объекты типа Number, благодаря чему можно воспользоваться этим методом) может принимать один необязательный аргумент, который определяет базу, или основание, системы счисления для преобразования. Если основание систе мы счисления не указывается, по умолчанию она предполагается равной 10. Од нако существует возможность выполнять преобразования и в других системах счисления (от 2 до 36)1, например: var n = 17; binary_string = n.toString(2); // Вернет "10001" octal_string = "0" + n.toString(8); // Вернет "021" hex_string = "0x" + n.toString(16); // Вернет "0x11" Одним из недостатков реализаций JavaScript, существовавших до версии Java Script 1.5, было отсутствие встроенной возможности определить число десятич ных знаков, которые должны получиться в результате, или задать результат в экспоненциальном представлении. В связи с этим могут возникать определен ные сложности с представлением чисел в традиционных форматах, таких как денежные суммы. В стандарте ECMAScript v3 и JavaScript 1.5 эта проблема была решена за счет до бавления нового метода преобразования числа в строку в классе Number. Метод to Fixed() преобразует число в строку и отображает определенное число знаков по сле десятичной точки. Однако данный метод не выполняет преобразование числа в экспоненциальную форму. Эту задачу решает метод toExponential(), который преобразует число в экспоненциальное представление с одним знаком перед точ кой и с заданным числом десятичных знаков после точки. Для отображения опре деленного числа значащих разрядов числа используется метод toPrecision(). Он возвращает строку с экспоненциальным представлением числа, если заданного количества значащих разрядов недостаточно для точного отображения целой части числа. Обратите внимание: все три метода корректно выполняют округле ние результата. Ниже приводятся примеры обращения к этим методам: var n = 123456.789; n.toFixed(0); // "123457" n.toFixed(2); // "123456.79" n.toExponential(1); // "1.2e+5" n.toExponential(3); // "1.235e+5" n.toPrecision(4); // "1.235e+5" n.toPrecision(7); // "123456.8" 1 Спецификациями ECMAScript предусматривается возможность определения ос нования системы счисления в методе toString(), но при этом допускается возвра щать из метода строку в представлении, зависящем от конкретной реализации, если основание не равно 10. Таким образом, согласно стандарту метод может про сто игнорировать значение аргумента и всегда возвращать число в десятичном представлении. Однако на практике большинство реализаций возвращают кор ректный результат с учетом заданного основания системы счисления. 48 Глава 3. Типы данных и значения 3.2.5. Преобразование строк в числа Когда строка используется в числовом контексте, она автоматически преобразу ется в число. Например, следующее выражение является вполне допустимым: var product = "21" * "2"; // в результате получится число 42. Это обстоятельство можно взять на вооружение при необходимости преобразо вать строку в число; для этого достаточно просто вычесть из строки значение 0: var number = string_value  0; (Будьте внимательны: операция сложения в данной ситуации будет интерпрети рована как операция конкатенации строк.) Менее изощренный и более прямолинейный способ преобразования строки в чис ло заключается в обращении к конструктору Number() как к обычной функции: var number = Number(string_value); Недостаток такого способа преобразования строки в число заключается в его чрезмерной строгости. Этот способ может использоваться только для преобразо вания десятичных чисел, и хотя он допускает наличие ведущих и оконечных символов пробела, появление других нецифровых символов после числа в строке недопустимо. Более гибкий способ преобразования обеспечивается функциями parseInt() и par seFloat(). Эти функции преобразуют и возвращают произвольные числа, стоя щие в начале строки, игнорируя любые нецифровые символы, расположенные вслед за числом. Функция parseInt() выполняет только целочисленное преобра зование, тогда как parseFloat() может преобразовывать как целые, так и вещест венные числа. Если строка начинается с символов «0x» или «0X», функция par seInt() интерпретирует строку как шестнадцатеричное число.1 Например: parseInt("3 слепых мышки"); // Вернет 3 parseFloat("3.14 метров"); // Вернет 3.14 parseInt("12.34"); // Вернет 12 parseInt("0xFF"); // Вернет 255 В качестве второго аргумента функция parseInt() может принимать основание системы счисления. Корректными значениями являются числа в диапазоне от 2 до 36, например: parseInt("11", 2); // Вернет 3 (1*2 + 1) parseInt("ff", 16); // Вернет 255 (15*16 + 15) parseInt("zz", 36); // Вернет 1295 (35*36 + 35) parseInt("077", 8); // Вернет 63 (7*8 + 7) parseInt("077", 10); // Вернет 77 (7*10 + 7) 1 Стандарт ECMAScript утверждает, что если строка начинается с символа «0» (но не «0x» или «0X»), функция parseInt() может интерпретировать строку как число и в восьмеричном, и в десятичном представлении. Поскольку поведение функции четко не определено, следует избегать использования функции parseInt() для ин терпретации строк, начинающихся с «0», или явно указывать основание системы счисления. 3.3. Логические значения 49 Если методы parseInt() и parseFloat() оказываются не в состоянии выполнить преобразование, они возвращают значение NaN: parseInt("eleven"); // Вернет NaN parseFloat("$72.47"); // Вернет NaN 3.3. Логические значения Числовые и строковые типы данных имеют большое или бесконечное количест во возможных значений. Логический тип данных, напротив, имеет только два допустимых логических значения, представленных литералами true и false. Ло гическое значение говорит об истинности чегото, т. е. о том, является это чтото истинным или нет. Логические значения обычно представляют собой результат сравнений, выпол няемых в JavaScriptпрограммах. Например: a == 4 Это выражение проверяет, равно ли значение переменной a числу 4. Если да, ре зультатом этого сравнения будет логическое значение true. Если переменная a не равна 4, результатом сравнения будет false. Логические значения обычно используются в управляющих конструкциях JavaScript. Например, инструкция if/else в JavaScript выполняет одно дейст вие, если логическое значение равно true, и другое действие, если false. Обычно сравнение, создающее логическое значение, непосредственно объединяется с ин струкцией, в которой оно используется. Результат выглядит так: if (a == 4) b = b + 1; else a = a + 1; Здесь выполняется проверка, равна ли переменная a числу 4. Если да, к значе нию переменной b добавляется 1; в противном случае число 1 добавляется к зна чению переменной a. Вместо того чтобы интерпретировать два возможных логических значения как true и false, иногда удобно рассматривать их как «включено» (true) и «выключе но» (false) или «да» (true) и «нет» (false). 3.3.1. Преобразование логических значений Логические значения легко преобразуются в значения других типов, причем не редко такое преобразование выполняется автоматически.1 Если логическое значе 1 Тем, кто программировал на C, следует обратить внимание, что в JavaScript име ется отдельный логический тип данных, в отличие от языка C, в котором для ими тации логических значений служат целые числа. Javaпрограммистам следует иметь в виду, что хотя в JavaScript есть логический тип, он не настолько «чист», как тип данных boolean в Java – в JavaScript логические значения легко преоб разуются в другие типы данных и обратно, поэтому на практике в том, что касает ся работы с логическими значениями, JavaScript больше напоминает C, чем Java. 50 Глава 3. Типы данных и значения ние используется в числовом контексте, тогда значение true преобразуется в чис ло 1, а false – в 0. Если логическое значение используется в строковом контек сте, тогда значение true преобразуется в строку "true", а false – в строку "false". Когда в качестве логического значения используется число, оно преобразуется в значение true, если оно не равно значениям 0 или NaN, которые преобразуются в логическое значение false. Когда в качестве логического значения используется строка, она преобразуется в значение true, если это не пустая строка, в против ном случае в результате преобразования получается значение false. Специаль ные значения null и undefined преобразуются в false, а любые функция, объект или массив, значения которых отличны от null, преобразуются в true. Если вы предпочитаете выполнять преобразование явно, можно воспользовать ся функцией Boolean(): var x_as_boolean = Boolean(x); Другой способ явного преобразования заключается в использовании двойного оператора логического отрицания: var x_as_boolean = !!x; 3.4. Функции Функция – это фрагмент исполняемого кода, который определен в JavaScriptпро грамме или заранее предопределен в реализации JavaScript. Хотя функция опре деляется только один раз, JavaScriptпрограмма может исполнять или вызывать ее сколько угодно. Функции могут передаваться аргументы, или параметры, оп ределяющие значение или значения, для которых она должна выполнять вычис ления; также функция может возвращать значение, представляющее собой ре зультат этих вычислений. Реализации JavaScript предоставляют много предопре деленных функций, таких как функция Math.sin(), возвращающая синус угла. JavaScriptпрограммы могут также определять собственные функции, содержа щие, например, такой код: function square(x) // Функция называется square. Она принимает один аргумент, x. { // Здесь начинается тело функции. return x*x; // Функция возводит свой аргумент в квадрат и возвращает // полученное значение. } // Здесь функция заканчивается. Определив функцию, можно вызывать ее, указав имя, за которым следует за ключенный в скобки список необязательных аргументов, разделенных запяты ми. Следующие строки представляют собой вызовы функций: y = Math.sin(x); y = square(x); d = compute_distance(x1, y1, z1, x2, y2, z2); move(); Важной чертой JavaScript является то, что функции представляют собой значе ния, которыми можно манипулировать в JavaScriptкоде. Во многих языках, в том числе в Java, функции – это всего лишь синтаксические элементы языка, но не тип данных: их можно определять и вызывать. То обстоятельство, что функции 3.5. Объекты 51 в JavaScript представляют собой настоящие значения, придает языку большую гибкость. Это означает, что функции могут храниться в переменных, массивах и объектах, а также передаваться в качестве аргументов другим функциям. Очень часто это бывает очень удобно. Более подробно об определении и вызове функций, а также об использовании их в качестве значений рассказывается в главе 8. Поскольку функции представляют собой значения, такие же, как числа и стро ки, они могут присваиваться свойствам объектов. Когда функция присваивает ся свойству объекта (объектный тип данных и свойства объекта описаны в разде ле 3.5), она часто называется методом этого объекта. Методы – важная часть объектноориентированного программирования. Им посвящена глава 7. 3.4.1. Функциональные литералы В предыдущем разделе мы видели определение функции square(). С помощью этого синтаксиса обычно описывается большинство функций в JavaScriptпро граммах. Однако стандарт ECMAScript v3 предоставляет синтаксис (реализован ный в JavaScript 1.2 и более поздних версиях) для определения функциональных литералов. Функциональный литерал задается с помощью ключевого слова func tion, за которым следуют необязательное имя функции, список аргументов функции, заключенный в круглые скобки, и тело функции в фигурных скобках. Другими словами, функциональный литерал выглядит так же, как определение функции, правда, у него может не быть имени. Самое большое различие состоит в том, что функциональные литералы могут входить в другие JavaScriptвыра жения. То есть функцию square() не обязательно задавать в виде определения: function square(x) { return x*x; } Ее можно задать с помощью функционального литерала: var square = function(x) { return x*x; } Функции, определенные таким образом, иногда называют лямбдафункциями. Это дань уважения языку программирования LISP, который одним из первых допускал вставку неименованных функций в виде литералов внутрь программы. Хотя в данный момент польза от функциональных литералов может быть неоче видной, позднее, в сложных сценариях мы увидим, что они бывают довольно удобными и полезными. Имеется еще один способ определения функции: можно передать список аргу ментов и тело функции в виде строк в конструктор Function(). Например: var square = new Function("x", "return x*x;"); Такое определение функций используется редко. Обычно неудобно задавать тело функции в виде строки, и во многих реализациях JavaScript функции, опреде ленные подобным образом, менее эффективны, чем функции, определенные лю бым из двух других способов. 3.5. Объекты Объект – это коллекция именованных значений, которые обычно называют свой' ствами (properties) объекта. (Иногда они называются полями объекта, но упо 52 Глава 3. Типы данных и значения требление этого термина может сбить с толку.) Чтобы сослаться на свойство объ екта, надо указать имя объекта, затем точку и имя свойства. Например, если объ ект под названием image имеет свойства width и height, мы можем сослаться на эти свойства следующим образом: image.width image.height Свойства объектов во многом похожи на JavaScriptпеременные – они могут со держать любой тип данных, включая массивы, функции и другие объекты. По этому можно встретить вот такой JavaScriptкод: document.myform.button Этот фрагмент ссылается на свойство button объекта, который, в свой очередь, хранится в свойстве myform объекта с именем document. Как упоминалось раньше, функция, хранящаяся в свойстве объекта, часто на зывается методом, а имя свойства становится именем метода. При вызове метода объекта сначала используется оператор «точка» для указания функции, а затем () для вызова этой функции. Например, метод write() объекта с именем document можно вызвать так: document.write("это проверка"); Объекты в JavaScript могут выступать в качестве ассоциативных массивов, т. е. могут ассоциировать произвольные значения с произвольными строками. При такой работе с объектом обычно требуется другой синтаксис для доступа к его свойствам: строка, содержащая имя требуемого свойства, заключается в квад ратные скобки. Тогда к свойствам объекта image, упомянутого ранее, можно об ратиться посредством следующего кода: image["width"] image["height"] Ассоциативные массивы – это мощный тип данных; они полезны при реализа ции ряда технологий программирования. Об объектах, их традиционном приме нении и применении в качестве ассоциативных массивов рассказано в главе 7. 3.5.1. Создание объектов Как мы увидим в главе 7, объекты создаются путем вызова специальных функ цийконструкторов. Все следующие строки создают новые объекты: var o = new Object(); var now = new Date(); var pattern = new RegExp("\\sjava\\s", "i"); Создав собственный объект, можно его как угодно использовать и устанавливать его свойства: var point = new Object(); point.x = 2.3; point.y = 1.2; 3.6. Массивы 53 3.5.2. Объектные литералы В JavaScript определяется синтаксис объектных литералов, позволяющий соз давать объекты и указывать их свойства. Объектный литерал (также называе мый инициализатором объекта) представляет собой список разделенных запя тыми пар «свойство/значение», заключенный в фигурные скобки. Внутри пар роль разделителя играет двоеточие. Таким образом, объект point из предыдуще го примера также может быть создан и инициализирован следующей строкой: var point = { x:2.3, y:1.2 }; Объектные литералы могут быть вложенными. Например: var rectangle = { upperLeft: { x: 2, y: 2 }, lowerRight: { x: 4, y: 4 } }; Наконец, значениями свойств в объектных литералах не обязательно должны быть константы – это могут быть произвольные JavaScriptвыражения. Кроме того, в качестве имен свойств в объектных литералах допускается использовать строковые значения: var square = { "upperLeft": { x:point.x, y:point.y }, 'lowerRight': { x:(point.x + side), y:(point.y+side) }}; 3.5.3. Преобразование объектов Когда непустой объект используется в логическом контексте, результатом преоб разования является значение true. Когда объект используется в строковом кон тексте, преобразование выполняется методом toString() объекта и в дальнейших вычислениях участвует строка, возвращаемая этим методом. Когда объект ис пользуется в числовом контексте, сначала вызывается метод объекта valueOf(). Если этот метод возвращает числовое значение примитивного типа, в дальней ших вычислениях участвует это значение. Однако в большинстве случаев метод valueOf() возвращает сам объект. В такой ситуации объект сначала преобразует ся в строку вызовом метода toString(), а затем выполняется попытка преобразо вать строку в число. Проблема преобразования объектов в значения примитивных типов имеет свои тонкости, и мы еще вернемся к ней в конце главы. 3.6. Массивы Массив (array), как и объект, представляет собой коллекцию значений. Если ка ждое значение, содержащееся в объекте, имеет имя, то в массиве каждое значе ние имеет номер, или индекс. В JavaScript можно извлекать значения из масси ва, указав после имени массива индекс, заключенный в квадратные скобки. На пример, если a – это имя массива, а i – неотрицательное целое число, то a[i] яв ляется элементом массива. Индексы массива начинаются с нуля, т. е. a[2] ссылается на третий элемент массива a. Массивы могут содержать любой тип данных JavaScript, в том числе ссылки на другие массивы или на объекты или функции. Например: 54 Глава 3. Типы данных и значения document.images[1].width Этот код ссылается на свойство width объекта, хранящегося во втором элементе массива, в свою очередь хранящегося в свойстве images объекта document. Обратите внимание: описываемые здесь массивы отличаются от ассоциативных массивов (см. раздел 3.5). Здесь обсуждаются «настоящие» массивы, которые ин дексируются неотрицательными целыми числами. Ассоциативные массивы ин дексируются строками. Следует также отметить, что в JavaScript не поддержи ваются многомерные массивы (хотя допускается существование массивов из мас сивов). И наконец, поскольку JavaScript является нетипизированным языком, элементы массива не обязательно должны иметь одинаковый тип, как в типизи рованных языках, подобных Java. Подробнее о массивах мы поговорим в главе 7. 3.6.1. Создание массивов Массив может быть создан с помощью функцииконструктора Array(). Созданному массиву допустимо присваивать любое количество индексированных элементов: var a = new Array(); a[0] = 1.2; a[1] = "JavaScript"; a[2] = true; a[3] = { x:1, y:3 }; Массивы могут также быть инициализированы путем передачи элементов мас сива конструктору Array(). Таким образом, предыдущий пример создания и ини циализации массива можно записать так: var a = new Array(1.2, "JavaScript", true, { x:1, y:3 }); Если передать конструктору Array() только одно число, оно определит длину массива. Таким образом, следующее выражение создает новый массив с 10 неоп ределенными элементами: var a = new Array(10); 3.6.2. Литералы массивов В JavaScript определяется синтаксис литералов для создания и инициализации массивов. Литерал, или инициализатор, массива – это список разделенных за пятыми значений, заключенных в квадратные скобки. Значения в скобках по следовательно присваиваются элементам массива с индексами, начиная с нуля. Например, программный код, создающий и инициализирующий массив из пре дыдущего раздела, можно записать следующим образом: var a = [1.2, "JavaScript", true, { x:1, y:3 }]; Как и объектные литералы, литералы массивов могут быть вложенными: var matrix = [[1,2,3], [4,5,6], [7,8,9]]; Как и в объектных литералах, элементы в литерале массива могут быть произ вольными выражениями и не обязательно должны быть константами: var base = 1024; var table = [base, base+1, base+2, base+3]; 3.7. Значение null 55 Для того чтобы включить в литерал массива неопределенный элемент, достаточ но пропустить значение между запятыми. Следующий массив содержит пять элементов, в том числе три неопределенных: var sparseArray = [1,,,,5]; 3.7. Значение null Ключевое слово null в JavaScript имеет специальный смысл. Обычно считается, что у значения null объектный тип и оно говорит об отсутствии объекта. Значе ние null уникально и отличается от любых других. Если переменная равна null, следовательно, в ней не содержится допустимого объекта, массива, числа, стро ки или логического значения.1 Когда значение null используется в логическом контексте, оно преобразуется в значение false, в числовом контексте оно преобразуется в значение 0, а в стро ковом контексте — в строку "null". 3.8. Значение undefined Еще одно специальное значение, иногда используемое в JavaScript, – undefined. Оно возвращается при обращении либо к переменной, которая была объявлена, но которой никогда не присваивалось значение, либо к свойству объекта, кото рое не существует. Заметьте, что специальное значение undefined – это не то же самое, что null. Хотя значения null и undefined не эквивалентны друг другу, оператор эквива лентности == считает их равными. Рассмотрим следующее выражение: my.prop == null Это сравнение истинно, либо если свойство my.prop не существует, либо если оно существует, но содержит значение null. Поскольку значение null и undefined обо значают отсутствие значения, это равенство часто оказывается тем, что нам нужно. Однако когда действительно требуется отличить значение null от значе ния undefined, нужен оператор идентичности === или оператор typeof (подробнее об этом в главе 5). В отличие от null, значение undefined не является зарезервированным словом JavaScript. Стандарт ECMAScript v3 указывает, что всегда существует глобаль ная переменная с именем undefined, начальным значением которой является зна чение undefined. Следовательно, в реализации, соответствующей стандарту, un defined можно рассматривать как ключевое слово, если только этой глобальной переменной не присвоено другое значение. Если нельзя с уверенностью сказать, есть ли в данной реализации переменная undefined, можно просто объявить собственную переменную: var undefined; 1 Программистам на C и C++ следует обратить внимание, что null в JavaScript – это не то же самое, что 0, как в других языках. В определенных обстоятельствах null преобразуется в 0, однако эти два значения не эквивалентны. 56 Глава 3. Типы данных и значения Объявив, но не инициализировав переменную, вы гарантируете, что переменная имеет значение undefined. Оператор void (см. главу 5) предоставляет еще один способ получения значения undefined. Когда значение undefined используется в логическом контексте, оно преобразует ся в значение false. В числовом контексте – в значение NaN, а в строковом – в строку "undefined". 3.9. Объект Date В предыдущих разделах мы описали все фундаментальные типы данных, под держиваемые JavaScript. Значения даты и времени не относятся к этим фунда ментальным типам, однако в JavaScript имеется класс объектов, представляю щих дату и время, и этот класс можно использовать для работы с этим типом данных. Объект Date в JavaScript создается с помощью оператора new и конструк тора Date() (оператор new будет введен в главе 5, а в главе 7 вы больше узнаете о создании объектов): var now = new Date(); // Создание объекта, в котором хранятся текущие дата и время. // Создание объекта, в котором хранится дата Рождества. // Обратите внимание: номера месяцев начинаются с нуля, поэтому декабрь имеет номер 11! var xmas = new Date(2000, 11, 25); Методы объекта Date позволяют получать и устанавливать различные значения даты и времени и преобразовывать дату в строку с использованием либо локаль ного времени, либо времени по Гринвичу (GMT). Например: xmas.setFullYear(xmas.getFullYear() + 1); // Заменяем дату датой следующего Рождества. var weekday = xmas.getDay(); // В 2007 году Рождество выпадает на вторник. document.write("Сегодня: " + now.toLocaleString()); // Текущие дата и время. В объекте Date также определяются функции (не методы, поскольку они не вы зываются через объект Date) для преобразования даты, заданной в строковой или числовой форме, во внутреннее представление в миллисекундах, полезное для некоторых видов операций с датами. Полное описание объекта Date и его методов вы найдете в третьей части книги. 3.10. Регулярные выражения Регулярные выражения предоставляют богатый и мощный синтаксис описания текстовых шаблонов. Они применяются для поиска соответствия заданному шаблону и реализации операций поиска и замены. В JavaScript для формирова ния регулярных выражений принят синтаксис языка Perl. Регулярные выражения представляются в JavaScript объектом RegExp и могут создаваться с помощью конструктора RegExp(). Как и объект Date, объект RegExp не является одним из фундаментальных типов данных JavaScript; это лишь стандартизованный тип объектов, предоставляемый всеми соответствующими реализациями JavaScript. Однако в отличие от объекта Date, объекты RegExp имеют синтаксис литералов и могут задаваться непосредственно в коде JavaScriptпрограммы. Текст между парой символов слэша образует литерал регулярного выражения. За вторым 3.11. Объекты Error 57 символом слэша в паре могут также следовать одна или несколько букв, изменя ющих смысл шаблона. Например: /^HTML/ /[19][09]*/ /\bjavascript\b/i Грамматика регулярных выражений сложна и подробно описана в главе 11. Сей час вам важно лишь знать, как литерал регулярного выражения выглядит в Java Scriptкоде. 3.11. Объекты Error В ECMAScript v3 определяется несколько классов для представления ошибок. При возникновении ошибки времени выполнения интерпретатор JavaScript «ге нерирует» объект одного из этих типов. (Вопросы генерации и перехвата ошибок обсуждаются в главе 6 при описании инструкций throw и try.) Каждый объект ошибки имеет свойство message, которое содержит зависящее от реализации со общение об ошибке. Заранее определены следующие типы объектов ошибок – Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError и URIError. Под робнее об этих классах рассказывается в третьей части книги. 3.12. Преобразование типов Поскольку все типы данных уже обсуждались в предыдущих разделах, здесь мы рассмотрим, как значения каждого типа преобразуются в значения других ти пов. Основное правило заключается в следующем: если значение одного типа ис пользуется в контексте, требующем значение некоего другого типа, интерпрета тор JavaScript автоматически пытается преобразовать это значение. Так, напри мер, если в логическом контексте используется число, оно преобразуется в зна чение логического типа. Если в контексте строки используется объект, он преобразуется в строковое значение. Если в числовом контексте используется строка, интерпретатор JavaScript пытается преобразовать ее в число. В табл. 3.3 приводится информация о том, как производится преобразование значений, ко гда значение некоторого типа задействовано в определенном контексте. Таблица 3.3. Автоматическое преобразование типов Тип значения Контекст, в котором используется значение Строковый Числовой Логический Объектный Неопределен ное значение "undefined" NaN false Error null "null" 0 false Error Непустая строка Как есть Числовое значение строки или NaN true Объект String Пустая строка Как есть 0 false Объект String 0 "0" Как есть false Объект Number NaN "NaN" Как есть false Объект Number 58 Глава 3. Типы данных и значения Таблица 3.3 (продолжение) 3.13. Объектыобертки для элементарных типов данных Ранее в этой главе мы обсуждали строки, и тогда я обратил ваше внимание на странную особенность этого типа данных: для работы со строками используется объектная нотация.1 Например, типичная операция со строками может выгля деть следующим образом: var s = "These are the times that try people's souls."; var last_word = s.substring(s.lastIndexOf(" ")+1, s.length); Если бы вы не знали, то могли бы подумать, что s – это объект, и вы вызываете методы и читаете значения свойств этого объекта. Что же происходит? Являются ли строки объектами или это элементарный тип данных? Оператор typeof (см. главу 5) убеждает нас, что строки имеют строко вый тип данных, отличный от объектного типа. Почему же тогда для манипуля ций со строками используется объектная нотация? Дело в том, что для каждого из трех базовых типов данных определен соответст вующий класс объектов. То есть помимо поддержки числовых, строковых и ло гических типов данных JavaScript поддерживает классы Number, String и Boolean. Эти классы представляют собой «обертки» для базовых типов данных. Обертка (wrapper) содержит такое же значение базового типа, но кроме этого определяет еще свойства и методы, которые могут использоваться для манипуляций с этим значением. JavaScript может гибко преобразовывать один тип в другой. Когда мы использу ем строку в объектном контексте, т. е. когда пытаемся обратиться к свойству или методу строки, JavaScript создает внутри себя объектобертку для строкового значения. Этот объект String используется вместо базового строкового значения. Для объекта определены свойства и методы, поэтому удается задействовать зна Тип значения Контекст, в котором используется значение Строковый Числовой Логический Объектный Infinity "Infinity" Как есть true Объект Number Infinity "Infinity" Как есть true Объект Number Любое другое число Строковое пред ставление числа Как есть true Объект Number true "true" 1 Как есть Объект Boolean false "false" 0 Как есть Объект Boolean Объект toString() valueOf(), toString() или NaN true Как есть 1 Этот раздел содержит достаточно сложный материал, который при первом про чтении можно пропустить. 3.13. Объекты*обертки для элементарных типов данных 59 чение базового типа в объектном контексте. То же самое, конечно, верно и для других базовых типов и соответствующих им объектовоберток; мы просто не ра ботаем с другими типами в объектном контексте так же часто, как со строками. Следует отметить, что объект String, созданный при использовании строки в объ ектном контексте, временный – он служит для того, чтобы обеспечить доступ к свойству или методу, после чего необходимость в нем отпадает, и потому он ути лизируется системой. Предположим, что s – это строка, и мы определяем длину строки с помощью следующего предложения: var len = s.length; Здесь s остается строкой, и ее исходное значение не меняется. Создается новый временный объект String, позволяющий обращаться к свойству length, а затем этот объект удаляется, не меняя исходного значения переменной s. Если эта схе ма кажется вам одновременно и элегантной, и неестественно сложной, вы пра вы. Однако обычно реализации JavaScript выполняют внутреннее преобразова ние очень эффективно, и вам не стоит об этом беспокоиться. Чтобы явно использовать объект String в своей программе, надо создать постоян ный объект, который не будет автоматически удаляться системой. Объекты String создаются так же, как и другие объекты, – с помощью оператора new. Например: var s = "hello world"; // Значение строкового типа var S = new String("Hello World"); // Объект String Что же можно делать с созданным объектом S типа String? Ничего такого, что нельзя сделать с соответствующим значением базового типа. Если мы воспользу емся оператором typeof, он сообщит нам, что S – это объект, а не строковое значе ние, но кроме того, мы не увидим различий между базовым строковым значени ем и объектом String.1 Как мы уже видели, строки автоматически преобразуются в объекты String, когда это требуется. Оказывается, что обратное тоже верно. Ко гда мы используем объект String там, где предполагается значение базового стро кового типа, JavaScript автоматически преобразует объект String в строку. По этому, если мы используем наш объект String с оператором +, для выполнения операции конкатенации создается временное значение базового строкового типа: msg = S + '!'; Имейте в виду, все, что говорилось в этом разделе о строковых значениях и объ ектах String, также применимо к числовым и логическим значениям и соответ ствующим объектам Number и Boolean. Более подробную информацию об этих классах можно получить из соответствующих статей в третьей части книги. Наконец, следует отметить, что любые строки, числа или логические значения могут быть преобразованы в соответствующий объектобертку с помощью функ ции Object(): var number_wrapper = Object(3); 1 Однако при этом метод eval() рассматривает строковые значения и объекты String поразному, и если непреднамеренно передать ему объект String вместо значения базового строкового типа, он поведет себя не так, как вы предполагаете. 60 Глава 3. Типы данных и значения 3.14. Преобразование объектов в значения элементарных типов Обычно объекты преобразуются в значения элементарных типов достаточно прямолинейным способом, о котором рассказывалось в разделе 3.5.3. Однако на вопросах преобразования объектов следует остановиться более подробно.1 Прежде всего следует заметить, что попытка преобразования непустых объектов в логическое значение дает в результате значение true. Это справедливо для лю бых объектов (включая массивы и функции), даже для объектовоберток, кото рые представляют элементарные типы, при другом способе преобразования даю щие значение false. Например, все нижеследующие объекты преобразуются в значение true при использовании их в логическом контексте: new Boolean(false) // Внутреннее значение – false, но объект преобразуется в true new Number(0) new String("") new Array( ) В табл. 3.3 описывается порядок преобразования объектов в числовые значения, когда сначала вызывается метод объекта valueOf(). Большинство объектов по умолчанию наследуют метод valueOf() от базового объекта Object, который воз вращает сам объект. Поскольку по умолчанию метод valueOf() не возвращает значение элементарного типа, далее интерпретатор JavaScript пытается преоб разовать объект в число вызовом метода toString() с последующим преобразова нием строки в число. В случае массивов это приводит к весьма интересным результатам. Метод toString() у массивов преобразует элементы массива в строки и возвращает ре зультат операции конкатенации этих строк, разделяя отдельные элементы мас сива запятыми. Таким образом, пустой массив преобразуется в пустую строку, что в результате дает число 0! Кроме того, если массив состоит из единственного элемента, содержащего число n, весь массив преобразуется в строковое представ ление числа n, которое затем будет вновь преобразовано в число n. Если массив содержит более одного элемента или единственный элемент массива не является числом, результатом преобразования будет значение NaN. Тип преобразования зависит от контекста, в котором это преобразование произ водится. Существуют такие ситуации, когда невозможно однозначно определить контекст. Оператор + и операторы сравнения (<, <=, > и >=) могут оперировать как числами, так и строками, таким образом, когда в одной из таких операций уча ствует объект, возникает неоднозначность: в значение какого типа следует пре образовать объект – в строку или в число. В большинстве случаев интерпретатор JavaScript сначала пытается преобразовать объект с помощью метода valueOf(). Если этот метод возвращает значение элементарного типа (как правило, число), тогда используется это значение. Однако чаще всего метод valueOf() возвращает непреобразованный объект, и тогда интерпретатор JavaScript пытается преобра зовать объект в строку вызовом метода toString(). 1 Этот раздел содержит достаточно сложный материал, который при первом про чтении можно пропустить. 3.15. По значению или по ссылке 61 Однако здесь есть одно исключение из правил: когда с оператором + использует ся объект Date, преобразование сразу начинается с вызова метода toString(). Это исключение обусловлено тем обстоятельством, что объект Date обладает собст венными реализациями методов toString() и valueOf(). Однако когда объект Date указывается с оператором +, чаще всего подразумевается операция конкатена ции строк, а при выполнении операции сравнения практически всегда требуется определить, какая из двух дат является более ранней по времени. Большинство объектов либо вообще не имеют метода valueOf(), либо этот метод не возвращает значение требуемого элементарного типа. Когда объект использу ется с оператором +, обычно выполняется операция конкатенации строк вместо сложения чисел. Аналогично, когда объект участвует в операциях сравнения, обычно производится сравнение строковых значений, а не чисел. Объекты, обладающие собственной реализацией метода valueOf(), могут вести себя иначе. Если вы переопределите метод valueOf(), чтобы он возвращал число, над объектом можно будет выполнять арифметические и другие числовые опера ции, но операция сложения объекта со строкой может не дать желаемого резуль тата, поскольку метод valueOf() возвратит значение элементарного типа, и метод toString() вызван уже не будет. В результате к строке будет добавляться строко вое представление числа, возвращаемого методом valueOf(). Наконец, следует запомнить, что метод valueOf() не вызывает метод toNumber(). Строго говоря, назначение этого метода состоит в том, чтобы преобразовать объ ект в разумное значение элементарного типа; по этой причине в некоторых объ ектах методы valueOf() возвращают строку. 3.15. По значению или по ссылке В JavaScript, как и в других языках программирования, имеется возможность манипулировать данными тремя способами.1 Первый способ – это копирование данных. Например, значение можно присвоить новой переменной. Второй спо соб – передать значение функции или методу. Третий – сравнить его с другим значением, чтобы проверить, равны ли эти значения. Для понимания языка программирования совершенно необходимо разобраться, как в нем выполняют ся эти три действия. Существует два фундаментальных способа манипулирования данными: по зна' чению и по ссылке. Когда выполняется манипулирование данной величиной по значению, это означает, что в операции участвует собственно значение данной величины. В операции присваивания создается копия фактического значения, после чего эта копия сохраняется в переменной, в свойстве объекта или в эле менте массива. Копия и оригинал – это два совершенно независимых друг от друга значения, которые хранятся раздельно. Когда некоторая величина переда ется функции по значению, это означает, что функции передается копия. Если функция изменит полученное значение, эти изменения коснутся только копии и никак не затронут оригинал. Наконец, когда величина сравнивается по значе нию с другой величиной, два различных набора данных должны содержать одно 1 Этот раздел содержит достаточно сложный материал, который при первом про чтении можно пропустить. 62 Глава 3. Типы данных и значения и то же значение (это обычно подразумевает, что для выявления эквивалентно сти величин производится их побайтное сравнение). Другой способ манипулирования значением – по ссылке. В этом случае сущест вует только одна фактическая копия значения, а манипулирование производит ся посредством ссылок на это значение.1 Когда действия со значением произво дятся по ссылке, переменные хранят не само значение, а лишь ссылки на него. Именно эта ссылочная информация копируется, передается и участвует в опера циях сравнения. Таким образом, в операции присваивания по ссылке участвует сама ссылка, а не копия значения и не само значение. После присваивания пере менная будет ссылаться на то же самое значение, что и оригинальная перемен ная. Обе ссылки считаются абсолютно равноправными и в равной степени могут использоваться для манипулирования значением. Если значение изменяется с помощью одной ссылки, эти изменения будут наблюдаться с помощью другой ссылки. То же происходит, когда значение передается функции по ссылке. В функцию попадает ссылка на значение, а функция может использовать ее для изменения самого значения. Любые такие изменения становятся видимыми за пределами функции. Наконец, когда выполняется операция сравнения по ссыл ке, происходит сравнение двух ссылок, чтобы проверить, не ссылаются ли они на одно и то же значение. Ссылки на два разных значения, даже эквивалентные (т. е. состоящие из тех же самых байтов данных), не могут считаться равными. Это два абсолютно разных способа манипулирования значениями, и разобраться в них совершенно необходимо. В табл. 3.4 приводится краткое описание выше изложенного. Данная дискуссия, посвященная манипулированию данными по ссылке и по значению, носила достаточно общий характер, но она с небольшими отличиями вполне применима ко всем языкам программирования. В следую щих разделах описываются характерные отличия, свойственные языку Java Script. Там рассказывается о том, какими типами данных манипулировать по значению, а какими – по ссылке. Таблица 3.4. Передача данных по ссылке и по значению 1 Программисты на C и все те, кому знакома концепция указателей, должны пони мать основную идею ссылок в данном контексте. Тем не менее следует отметить, что JavaScript не поддерживает работу с указателями. По значению По ссылке Копирова ние Выполняется копирование самого значения – образуют ся две независимые друг от друга копии. Копируется только ссылка на значение. Если значение будет изменено с помощью вновь созданной копии ссылки, эти изме нения будут наблюдаться при использо вании оригинальной ссылки. Передача Функции передается отдель ная копия значения. Измене ние этой копии не оказывает никакого влияния на значе ние за пределами функции. Функции передается ссылка на значение. Если внутри функции значение будет из менено с помощью полученной ссылки, эти изменения будут наблюдаться и за ее пределами. 3.15. По значению или по ссылке 63 3.15.1. Элементарные и ссылочные типы Главное правило JavaScript заключается в следующем: операции над элемен' тарными типами производятся по значению, а над ссылочными типами, как и следует из их названия, – по ссылке. Числа и логические величины – это эле ментарные типы в JavaScript. Элементарные потому, что состоят из небольшого и фиксированного числа байтов, операции над которыми с легкостью выполня ются низкоуровневым интерпретатором JavaScript. Представителями ссылоч ных типов являются объекты. Массивы и функции – это специализированные типы объектов и потому также являются ссылочными типами. Эти типы данных могут состоять из произвольного числа свойств или элементов, поэтому опериро вать ими не так просто, как значениями элементарных типов, имеющими фик сированные размеры. Поскольку размеры массивов и объектов могут быть чрез вычайно велики, операции по значению над ними могут привести к неоправдан ному копированию и сравнению гигантских объемов памяти. А что можно сказать о строках? Строки могут иметь произвольную длину, поэто му их вполне можно было бы рассматривать как ссылочный тип. Тем не менее в JavaScript строки обычно рассматриваются как элементарный тип просто по тому, что они не являются объектами. В действительности строки не вписыва ются в двухполярный элементарноссылочный мир. Подробнее я остановлюсь на строках чуть позже. Лучший способ выяснить различия между данными, операции над которыми производятся по ссылке и по значению, состоит в изучении практического при мера. Тщательно разберитесь в следующем примере, обращая особое внимание на комментарии. В примере 3.1 выполняется копирование, передача и сравне ние чисел. Поскольку числа являются элементарными типами, данный пример является иллюстрацией операций, выполняемых по значению. Пример 3.1. Копирование, передача и сравнение величин по значению // Первой рассматривается операция копирования по значению var n = 1; // Переменная n хранит значение 1 var m = n; // Копирование по значению: переменная m хранит другое значение 1 // Данная функция используется для иллюстрации операции передачи величины по значению // Как вы увидите, функция работает не совсем так, как хотелось бы function add_to_total(total, x) { total = total + x; // Эта строка изменяет лишь внутреннюю копию total } // Теперь производится обращение к функции, которой передаются по значению числа, // содержащиеся в переменных n и m. Копия значения из переменной n внутри функции // доступна под именем total. Функция складывает копии значений переменных m и n, Сравнение Сравниваются два разных значения (часто побайтно), чтобы определить, равны ли они. Сравниваются две ссылки, чтобы опреде лить, ссылаются ли они на одно и то же значение. Ссылки на разные значения рассматриваются как неравные, даже ес ли сами значения совершенно идентичны. По значению По ссылке 64 Глава 3. Типы данных и значения // записывая результат в копию значения переменной n. Однако это не оказывает // никакого влияния на оригинальное значение переменной n за пределами функции. // Таким образом, в результате вызова этой функции мы не получаем никаких изменений. add_to_total(n, m); // Сейчас мы проверим операцию сравнения по значению. // В следующей строке программы литерал 1 представляет собой совершенно // независимое числовое значение, "зашитое" в текст программы. Мы сравниваем // его со значением, хранящимся в переменной n. В данном случае, чтобы // убедиться в равенстве двух чисел, выполняется их побайтовое сравнение. if (n == 1) m = 2; // n содержит то же значение, что и литерал 1; // таким образом в переменную m будет записано значение 2 Теперь рассмотрим пример 3.2. В этом примере операции копирования, переда чи и сравнения выполняются над объектами. Поскольку объекты относятся к ссылочным типам, все действия над ними производятся по ссылке. В данном примере использован объект Date, подробнее о котором можно прочитать в треть ей части книги. Пример 3.2. Копирование, передача и сравнение величин по ссылке // Здесь создается объект, который соответствует дате Рождества в 2007 году // Переменная xmas хранит ссылку на объект, а не сам объект var xmas = new Date(2007, 11, 25); // Затем выполняется копирование ссылки, получается вторая ссылка на оригинальный объект var solstice = xmas; // Обе переменные ссылаются на тот же самый объект // Здесь выполняется изменение объекта с помощью новой ссылки solstice.setDate(21); // Изменения можно наблюдать при использовании первой ссылки xmas.getDate( ); // Возвращает 21, а не первоначальное значение 25 // То же происходит при передаче объектов и массивов в функции. // Следующая функция складывает значения всех элементов массива. // Функции передается ссылка на массив, а не копия массива. // Благодаря этому функция может изменять содержимое массива, переданного // по ссылке, и эти изменения будут видны после возврата из функции. function add_to_totals(totals, x) { totals[0] = totals[0] + x; totals[1] = totals[1] + x; totals[2] = totals[2] + x; } // Наконец, далее выполняется операция сравнения по ссылке. // При сравнении двух переменных, созданных ранее, обнаруживается, // что они эквивалентны, поскольку ссылаются на один и тот же объект даже // при том, что производилось изменение даты по одной из них: (xmas == solstice) // Возвращает значение true // Две переменные, созданные позднее, ссылаются на разные объекты, // каждый из которых содержит одну и ту же дату. var xmas = new Date(2007, 11, 25); var solstice_plus_4 = new Date(2007, 11, 25); // Но согласно правилу "сравнения по ссылке" ссылки на разные объекты 3.15. По значению или по ссылке 65 // не считаются эквивалентными! (xmas != solstice_plus_4) // Возвращает значение true Прежде чем закончить обсуждение темы выполнения операций над объектами и массивами по ссылке, добавим немного ясности. Фраза «передача по ссылке» мо жет иметь несколько смыслов. Для некоторых из вас эта фраза означает такой спо соб вызова функции, который позволяет изменять эти значения внутри функции и наблюдать эти изменения за ее пределами. Однако данный термин трактуется в этой книге в несколько ином смысле. Здесь просто подразумевается, что функ ции передается ссылка на массив или объект, но не сам объект. Функция же с по мощью этой ссылки получает возможность изменять свойства объекта или элемен ты массива, и эти изменения сохраняются по выходе из функции. Те из вас, кто знаком с другими трактовками этого термина, могут заявить, что объекты и масси вы передаются по значению, правда, этим значением фактически является ссылка на объект, а не сам объект. Пример 3.3 наглядно иллюстрирует эту проблему. Пример 3.3. Ссылки передаются по значению // Здесь приводится другая версия функции add_to_totals(). Хотя она не работает, // потому что вместо изменения самого массива она изменяет ссылку на этот массив. function add_to_totals2(totals, x) { newtotals = new Array(3); newtotals[0] = totals[0] + x; newtotals[1] = totals[1] + x; newtotals[2] = totals[2] + x; totals = newtotals; // Эта строка не оказывает влияния // на массив за пределами функции } 3.15.2. Копирование и передача строк Как упоминалось ранее, строки не вписываются в двухполярный элементарно ссылочный мир. Поскольку строки не являются объектами, вполне естественно предположить, что они относятся к элементарному типу. Если строки рассмат ривать как элементарный тип данных, тогда в соответствии с описанными выше правилами операции над ними должны производиться по значению. Но по скольку строки могут иметь произвольную длину, это может приводить к непро изводительному расходованию системных ресурсов на операции копирования и побайтового сравнения. Таким образом, не менее естественно было бы предпо ложить, что строки реализованы как ссылочный тип данных. Вместо того чтобы строить предположения, можно попробовать написать не большой фрагмент на языке JavaScript и решить проблему экспериментальным путем. Если строки копируются и передаются по ссылке, должна иметься воз можность изменять их содержимое с помощью ссылки, хранящейся в другой пе ременной, или в результате передачи строки в функцию. Однако при попытке написать такой фрагмент для проведения эксперимента вы столкнетесь с серьезной проблемой: в JavaScript невозможно изменить содержи мое строки. Существует метод charAt(), который возвращает символ из заданной позиции в строке, но нет соответствующего ему метода setCharAt(), позволяюще го ввести в эту позицию другой символ. Это не упущение. Строки в JavaScript 66 Глава 3. Типы данных и значения преднамеренно неизменяемы – в JavaScript отсутствуют элементы языка, с по мощью которых можно было бы изменять символы в строке. Поскольку строки являются неизменяемыми, вопрос остается открытым, т. к. нет никакого способа проверить, как передаются строки – по ссылке или по зна чению. Можно предположить, что с целью повышения эффективности интер претатор JavaScript реализован так, чтобы строки передавались по ссылке, но это так и останется всего лишь предположением, поскольку нет возможности проверить его экспериментально. 3.15.3. Сравнение строк Несмотря на отсутствие возможности определить, как копируются строки, по ссылке или по значению, существует возможность написать такой фрагмент на JavaScript, с помощью которого можно выяснить, как именно выполняется опе рация сравнения – по ссылке или по значению. В примере 3.4 приводится фраг мент, выполняющий такую проверку. Пример 3.4. Как сравниваются строки, по ссылке или по значению? // Определить, как сравниваются строки, по ссылке или по значению, // довольно просто. Здесь сравниваются совершенно разные строки, содержащие // одинаковые последовательности символов. Если сравнение выполняется // по значению, они должны интерпретироваться как эквивалентные, если же они // сравниваются по ссылке, результат должен быть противоположным: var s1 = "hello"; var s2 = "hell" + "o"; if (s1 == s2) document.write("Строки сравниваются по значению"); Данный эксперимент доказывает, что строки сравниваются по значению. Это может оказаться сюрпризом для некоторых программистов, работающих с язы ками C, C++ и Java, где строки относятся к ссылочным типам и сравниваются по ссылке. При необходимости сравнить в этих языках фактическое содержимое строк приходится использовать специальные методы или функции. Язык Java Script относится к языкам высокого уровня и потому предполагает, что когда выполняется сравнение строк, скорее всего, имеется в виду сравнение по значе нию. Таким образом, несмотря на то, что с целью достижения более высокой эф фективности строки в JavaScript (повидимому) копируются и передаются по ссылке, тем не менее операция сравнения выполняется по значению. 3.15.4. По ссылке или по значению: подведение итогов Таблица 3.5 кратко иллюстрирует то, как выполняются операции над различ ными типами данных в JavaScript. Таблица 3.5. Операции над типами данных в JavaScript Тип Копирование Передача Сравнение Число По значению По значению По значению Логическое значение По значению По значению По значению Строка Не изменяется Не изменяется По значению Объект По ссылке По ссылке По ссылке Переменные Переменная – это имя, связанное со значением. Мы говорим, что значение хра нится, или содержится, в переменной. Переменные позволяют хранить данные в программе и работать с ними. Например, следующая строка JavaScriptкода присваивает значение 2 переменной с именем i: i = 2; А следующая добавляет 3 к значению переменной i и присваивает результат но вой переменной sum: var sum = i + 3; Это почти все, что надо знать о переменных. Но для полного понимания меха низма их работы в JavaScript следует освоить и другие понятия, и пары строк кода здесь недостаточно! В данной главе рассматриваются вопросы типизации, объявления, области видимости, содержимого и разрешения имен переменных, а также сборки мусора и двойственности понятия «переменная/свойство».1 4.1. Типизация переменных Самое важное различие между JavaScript и такими языками, как Java и C, со стоит в том, что JavaScript – это нетипизированный (untyped) язык. В частно сти, это значит, что JavaScriptпеременная может содержать значение любого типа, в отличие от Java или Cпеременной, в которой может содержаться только определенный тип данных, заданный при ее объявлении. Так, в JavaScript мож но присвоить переменной число, а затем присвоить той же переменной строку: i = 10; i = "десять"; 1 Это сложный предмет, полное понимание которого требует хорошего знакомства с материалом последующих глав книги. Начинающие могут прочесть первые два раздела и перейти к главам 5, 6 и 7, а затем вернуться к этой главе. 68 Глава 4. Переменные В Java, C, C++ и любом другом строго типизированном языке подобный код не допустим. Особенностью JavaScript, вытекающей из отсутствия типизации, является то, что язык в случае необходимости легко и автоматически преобразует значения из одного типа в другой. Например, если вы попытаетесь дописать число к стро ке, JavaScript автоматически преобразует число в соответствующую строку, ко торая может быть добавлена к имеющейся. Более подробно преобразования ти пов рассматриваются в главе 3. Нетипизированность языка JavaScript обусловливает его простоту по сравнению с типизированными языками, такими как C++ и Java, преимущество которых состоит в том, что они способствуют более строгой практике программирования, облегчая написание, поддержку и повторное использование длинных и сложных программ. В то же время многие JavaScriptпрограммы представляют собой ко роткие сценарии, поэтому такая строгость необязательна, и программисты мо гут воспользоваться преимуществами более простого синтаксиса. 4.2. Объявление переменных Прежде чем использовать переменную в JavaScript, ее необходимо объявить.1 Переменные объявляются с помощью ключевого слова var следующим образом: var i; var sum; Можно объявить несколько переменных: var i, sum; Кроме того, объявление переменной можно совмещать с ее инициализацией: var message = "hello"; var i = 0, j = 0, k = 0; Если начальное значение не задано в инструкции var, то переменная объявляет ся, но ее начальное значение остается неопределенным (undefined), пока не будет изменено программой. Обратите внимание, что инструкция var также может включаться в циклы for и for/in (о которых рассказывается в главе 6), что позволяет объявлять перемен ную цикла непосредственно в самом цикле. Например: for(var i = 0; i < 10; i++) document.write(i, "
"); for(var i = 0, j = 10; i < 10; i++, j) document.write(i*j, "
"); for(var i in o) document.write(i, "
"); Переменные, объявленные с помощью инструкции var, называются долговре' менными (permanent): попытка удалить их с помощью оператора delete приве дет к ошибке. (Оператор delete рассматривается в главе 5.) 1 Если этого не сделать, то переменная неявно будет объявлена самим интерпрета тором JavaScript. 4.3. Область видимости переменной 69 4.2.1. Повторные и опущенные объявления С помощью инструкции var можно объявить одну и ту же переменную несколько раз. Если повторное объявление содержит инициализатор, то оно действует как обычная инструкция присваивания. Если попытаться прочитать значение необъявленной переменной, JavaScript сгенерирует сообщение об ошибке. Если присвоить значение переменной, не объявленной с помощью инструкции var, JavaScript неявно объявит эту пере менную за вас. Однако переменные, объявленные таким образом, всегда созда ются как глобальные, даже если они работают только в теле функции. Чтобы не создавать глобальную переменную (или не использовать существующую), когда достаточно локальной переменной для отдельной функции, всегда помещайте инструкцию var в тело функции. Лучше всего объявлять с ключевым словом var все переменные – и глобальные, и локальные. (Различие между локальными и глобальными переменными подробнее рассматривается в следующем разделе.) 4.3. Область видимости переменной Область видимости (scope) переменной – это та часть программы, для которой эта переменная определена. Глобальная переменная имеет глобальную область видимости – она определена для всей JavaScriptпрограммы. Переменные, объ явленные внутри функции, определены только в ее теле. Они называются локаль' ными и имеют локальную область видимости. Параметры функций также счита ются локальными переменными, определенными только в теле этой функции. Внутри тела функции локальная переменная имеет преимущество перед гло бальной переменной с тем же именем. Если объявить локальную переменную или параметр функции с тем же именем, что у глобальной переменной, то фак тически глобальная переменная будет скрыта. Так, следующий код печатает слово «локальная»: var scope = "глобальная"; // Объявление глобальной переменной function checkscope() { var scope = "локальная"; // Объявление локальной переменной с тем же именем document.write(scope); // Используется локальная переменная, а не глобальная } checkscope(); // Печатается слово "локальная" Объявляя переменные с глобальной областью видимости, инструкцию var мож но опустить, но при объявлении локальных переменных она необходима. По смотрите, что получается, если этого не сделать: scope = "глобальная"; // Объявление глобальной переменной, даже без var function checkscope() { scope = "локальная"; // Ой! Мы только что изменили глобальную переменную document.write(scope); // Используется глобальная переменная myscope = "локальная"; // Здесь мы неявно объявляем новую глобальную переменную document.write(myscope); // Используется новая глобальная переменная } checkscope(); // Печатает "локальнаялокальная" document.write(scope); // Печатает "локальная" document.write(myscope); // Печатает "локальная" 70 Глава 4. Переменные Функции, как правило, не знают, какие переменные объявлены в глобальной области видимости или для чего они нужны. Поэтому функция, использующая глобальную переменную вместо локальной, рискует изменить значение, необхо димое какойлибо другой части программы. К счастью, избежать этой неприят ности легко: объявляйте все переменные с помощью инструкции var. Определения функций могут быть вложенными. Каждая функция имеет собст венную локальную область видимости, поэтому может быть несколько вложен ных уровней локальных областей видимости. Например: var scope = "глобальная область видимости"; // Глобальная переменная function checkscope() { var scope = "локальная область видимости"; // Локальная переменная function nested() { var scope = "вложенная область видимости"; // Вложенная область видимости // локальных переменных document.write(scope); // Печатает "вложенная область видимости" } nested(); } checkscope(); 4.3.1. Отсутствие блочной области видимости Обратите внимание: в отличие от C, C++ и Java, в JavaScript нет области види мости на уровне блоков. Все переменные, объявленные внутри функции, незави симо от того, где именно это сделано, определены во всей функции. В следующем фрагменте переменные i, j и k имеют одинаковые области видимости: все три оп ределены во всем теле функции. Это было бы не так, если бы код был написан на C, C++ или Java: function test(o) { var i = 0; // i определена во всей функции if (typeof o == "object") { var j = 0; // j определена везде, а не только в блоке for(var k = 0; k < 10; k++) { // k определена везде, не только в цикле document.write(k); } document.write(k); // k все еще определена: печатается 10 } document.write(j); // j определена, но может быть не инициализирована } Правило, согласно которому все переменные, объявленные в функции, опреде лены во всей функции, может иметь удивительные следствия. Например: var scope = "глобальная"; function f() { alert(scope); // Показывает "undefined", а не "глобальная". var scope = "локальная"; // Переменная инициализируется здесь, // но определена она везде в функции. alert(scope); // Показывает "локальная" } f(); Ктото может подумать, что в результате первого вызова alert() будет напечата но слово «глобальная», т. к. инструкция var, объявляющая локальную перемен 4.4. Элементарные и ссылочные типы 71 ную, еще не была выполнена. Однако согласно правилу определения областей видимости все происходит не так. Локальная переменная определена во всем те ле функции, значит, глобальная переменная с тем же именем скрыта во всем те ле функции. Хотя локальная переменная определена везде, до выполнения ин струкции var она не инициализирована. Поэтому функция f в предыдущем при мере эквивалентна следующему фрагменту: function f() { var scope; // Локальная переменная определяется в начале функции alert(scope); // Здесь она существует, но имеет значение undefined scope = "локальная"; // Здесь мы инициализируем переменную и присваиваем ей значение alert(scope); // Здесь она уже имеет значение } Этот пример показывает, почему хорошая практика программирования подра зумевает помещение всех объявлений переменных в начале функции. 4.3.2. Неопределенные и неинициализированные переменные Примеры предыдущего раздела демонстрируют тонкий момент программирова ния на JavaScript: имеется два вида неопределенных переменных. Первый – пе ременная, которая нигде не объявлена. Попытка прочитать значение необъявлен' ной переменной приведет к ошибке времени выполнения. Необъявленные пере менные не определены, потому что они просто не существуют. Как уже было ска зано, присваивание значения необъявленной переменной не приводит к ошибке – просто она при присваивании неявно объявляется как глобальная переменная. Второй вид неопределенных переменных – переменная, которая была объявлена, но значение ей нигде не присваивалось. Если прочитать значение одной из таких переменных, то будет получено ее значение по умолчанию – undefined. Такие пе ременные лучше называть неинициализированными (unassigned), чтобы отли чить их от тех переменных, которые вообще не объявлялись и не существуют. В следующем фрагменте иллюстрируются некоторые различия между неопреде ленными и неинициализированными переменными: var x; // Объявляем неинициализированную переменную. Значением ее является undefined. alert(u); // Использование необъявленной переменной приведет к ошибке. u = 3; // Присваивание значения необъявленной переменной создает эту переменную. 4.4. Элементарные и ссылочные типы Следующая тема, которую мы рассмотрим, – содержимое переменных. Мы часто говорим, что переменные содержат значения. Что же они содержат в действи тельности? Чтобы ответить на этот, казалось бы, простой вопрос, мы должны снова взглянуть на типы данных, поддерживаемые JavaScript. Эти типы можно разделить на две группы: элементарные и ссылочные. Числа, логические значения, а также значения null и undefined – это элементар ные типы. Объекты, массивы и функции – это ссылочные типы. Элементарный тип имеет фиксированный размер. Например, число занимает во семь байтов, а логическое значение может быть представлено всего одним битом. 72 Глава 4. Переменные Числовой тип – самый большой из элементарных типов. Если для каждой Java Scriptпеременной зарезервировано в памяти восемь байтов, переменная может непосредственно содержать значение любого элементарного типа.1 Однако ссылочные типы – это другое дело. Например, объекты могут быть любой длины – они не имеют фиксированного размера. То же самое относится и к мас сивам: массив может иметь любое число элементов. Аналогично функция может содержать любой объем JavaScriptкода. Поскольку данные типы не имеют фик сированного размера, их значения не могут непосредственно храниться в восьми байтах памяти, связанных с каждой переменной. Поэтому в переменной хранит ся ссылка на это значение. Обычно эта ссылка представляет собой какойлибо указатель или адрес в памяти. Ссылка – это не само значение, но она сообщает переменной, где это значение можно найти. Различие между элементарными и ссылочными типами существенно, т. к. они ведут себя поразному. Рассмотрим следующий код, оперирующий числами (элементарный тип): var a = 3.14; // Объявление и инициализация переменной var b = a; // Копирование значения переменной в новую переменную a = 4; // Модификация значения исходной переменной alert(b) // Показывает 3.14; копия не изменилась В этом фрагменте нет ничего необычного. Теперь посмотрим, что произойдет, ес ли слегка изменить код, заменив числа массивами (ссылочный тип): var a = [1,2,3]; // Инициализируем переменную ссылкой на массив var b = a; // Копируем эту ссылку в новую переменную a[0] = 99; // Изменяем массив, используя первоначальную ссылку alert(b); // Показываем измененный массив [99,2,3], используя новую ссылку Те, кого результат не удивил, уже хорошо знакомы с различием между элемен тарными и ссылочными типами. Тем же, кого он удивил, надо посмотреть вни мательнее на вторую строку. Обратите внимание, что в этом предложении вы полняется присваивание ссылки на значение типа «массив», а не присваивание самого массива. После второй строки фрагмента мы все еще имеем один объект массива; нам только удалось получить две ссылки на него. Если разница между элементарным и ссылочным типами вам внове, просто по старайтесь держать в уме содержимое переменной. Переменные содержат фак тические значения элементарных типов, но лишь ссылки на значения ссылоч ных типов. Разное поведение базовых и ссылочных типов более подробно изуча ется в разделе 3.15. Вы могли заметить, что я не указал, относятся ли строки в JavaScript к базовым или к ссылочным типам. Строки – это необычный случай. Они имеют переменную длину и потому, очевидно, не могут храниться непосредственно в переменных фик сированного размера. Исходя из соображений эффективности можно ожидать, что интерпретатор JavaScript будет копировать ссылки на строки, а не их фактическое содержимое. В то же время во многих отношениях строки ведут себя как элемен тарные типы. Вопрос о том, к какому типу принадлежат строки, элементарному 1 Это упрощение, которое не следует рассматривать как описание фактической ре ализации JavaScript. 4.5. Сборка мусора 73 или ссылочному, спорный, т. к. строки на самом деле неизменны: нет возможности избирательно изменить содержимое внутри строкового значения. Это значит, что нельзя составить пример, похожий на предыдущий, в котором массивы копирова лись бы по ссылке. В конце концов, не имеет значения, как рассматривать строки, как неизменный ссылочный тип, ведущий себя как элементарный, или как эле ментарный тип, реализованный с использованием механизма ссылочного типа. 4.5. Сборка мусора Ссылочные типы не имеют фиксированного размера; в самом деле, некоторые из них могут быть очень большими. Мы уже говорили о том, что переменные не со держат непосредственных значений ссылочного типа. Значения хранятся в ка комлибо другом месте, а в переменных находится только ссылка на это местопо ложение. А сейчас кратко остановимся на реальном хранении значений. Поскольку строки, объекты и массивы не имеют фиксированного размера, место для их хранения должно выделяться динамически, когда становится известен размер. Когда JavaScriptпрограмма создает строку, массив или объект, интерпре татор должен выделить память для хранения этой сущности. Память, выделяемая подобным образом, должна быть впоследствии освобождена, иначе интерпретатор JavaScript исчерпает всю доступную память, что приведет к отказу системы. В таких языках, как C и C++, память приходится освобождать вручную. Именно программист отвечает за отслеживание всех создаваемых объектов и, когда они больше не требуются, – за их ликвидацию (освобождение памяти). Это бывает довольно обременительно и часто приводит к ошибкам.1 В JavaScript, где не надо вручную освобождать память, реализована технология, называемая сборкой мусора (garbage collection). Интерпретатор JavaScript мо жет обнаружить, что объект никогда более не будет использоваться программой. Определив, что объект недоступен (т. е. больше нет способа получения ссылки на него), интерпретатор выясняет, что объект более не нужен, и занятая им па мять может быть освобождена.2 Рассмотрим следующие строки кода: var s = "hello"; // Выделяем память для строки var u = s.toUpperCase(); // Создаем новую строку s = u; // Переписываем ссылку на первоначальную строку После работы этого кода исходная строка "hello" больше недоступна – ни в одной из переменных программы нет ссылки на нее. Система определяет этот факт и освобождает память. 1 Это не совсем строго: для локальных (объявленных в функции) переменных, раз мещаемых в стеке, какой бы сложной структуры ни были переменные, автомати чески вызывается деструктор и производится освобождение памяти. Точно так же ведут себя контейнеры STL, или «собственные данные потока». Утверждение автора в абсолютной степени относится только к объектам, динамически распре деляемым операторами new и delete. – Примеч. науч. ред. 2 Описываемая схема сборки мусора, известная как «подсчет числа ссылок», может иметь серьезные проблемы в более сложных программах при появлении объектов с циклическими ссылками – объекты никогда не будут освобождены. Эта проблема хорошо изучена в Perl; способы борьбы см. в описании языка. – Примеч. науч. ред. 74 Глава 4. Переменные Сборка мусора выполняется автоматически и невидима для программиста. О сборке мусора он должен знать ровно столько, сколько ему требуется, чтобы доверять ее работе, – он не должен думать, куда делись все старые объекты. 4.6. Переменные как свойства Вы уже могли заметить, что в JavaScript между переменными и свойствами объ ектов много общего. Им одинаково присваиваются значения, они одинаково применяются в JavaScriptвыражениях и т. д. Есть ли какаянибудь принципи альная разница между переменной i и свойством i объекта o? Ответ: никакой. Переменные в JavaScript принципиально не отличаются от свойств объекта. 4.6.1. Глобальный объект Одно из первых действий, выполняемых интерпретатором JavaScript при запус ке перед исполнением любого кода, – это создание глобального объекта. Свойст ва этого объекта представляют собой глобальные переменные JavaScriptпро граммы. Объявляя в JavaScript глобальную переменную, фактически вы опреде ляете свойство глобального объекта. Интерпретатор JavaScript инициализирует ряд свойств глобального объекта, ссылающихся на предопределенные значения и функции. Так, свойства Infini ty, parseInt и Math ссылаются на число «бесконечность», предопределенную функцию parseInt() и предопределенный объект Math. Более подробно прочитать об этих глобальных значениях можно в третьей части книги. В коде верхнего уровня (т. е. JavaScriptкоде, который не является частью функции) сослаться на глобальный объект можно посредством ключевого слова this. Внутри функций ключевое слово this имеет другое применение, которое описано в главе 8. В клиентском языке JavaScript в качестве глобального объекта для всего Java Scriptкода, содержащегося в соответствующем ему окне броузера, служит объ ект Window. Этот глобальный объект имеет свойство window, ссылающееся на сам объект, которое можно использовать вместо ключевого слова this для ссылки на глобальный объект. Объект Window определяет базовые глобальные свойства, та кие как parseInt и Math, а также глобальные клиентские свойства, такие как navigator и screen. 4.6.2. Локальные переменные  объект вызова Если глобальные переменные – это свойства специального глобального объекта, так что же тогда такое локальные переменные? Они тоже представляют собой свойства объекта. Этот объект называется объектом вызова (call object). Когда вы полняется тело функции, аргументы и локальные переменные функции хранятся как свойства этого объекта. Использование абсолютно отдельного объекта для ло кальных переменных позволяет JavaScript не допускать переписывания локаль ными переменными значений глобальных переменных с теми же именами. 4.6.3. Контексты исполнения в JavaScript Начиная исполнять функцию, интерпретатор JavaScript создает для нее новый контекст исполнения (execution context), т. е. контекст, в котором выполняется 4.7. Еще об области видимости переменных 75 любой фрагмент JavaScriptкода. Важная часть контекста – объект, в котором определены переменные. Поэтому код JavaScriptпрограммы, не являющийся частью какойлибо функции, работает в контексте исполнения, в котором для определений переменных используется глобальный объект. А любая JavaScript функция работает в собственном уникальном контексте исполнения с собствен ным объектом вызова, в котором определены локальные переменные. Интересно отметить, что реализации JavaScript могут допускать несколько гло бальных контекстов исполнения с отдельным глобальным объектом каждый.1 (Хотя в этом случае каждый глобальный объект не является действительно гло бальным.) Очевидный пример – это клиентский JavaScript, в котором каждое отдельное окно броузера или каждый фрейм в окне определяет отдельный гло бальный контекст исполнения. Код клиентского JavaScript в каждом фрейме или окне работает в собственном контексте исполнения и имеет собственный глобальный объект. Однако эти отдельные клиентские глобальные объекты име ют свойства, связывающие их друг с другом. Другими словами, JavaScriptкод в одном фрейме может ссылаться на другой фрейм с помощью выражения pa rent.frames[1], а на глобальную переменную x в первом фрейме можно сослаться из второго фрейма с помощью выражения parent.frames[0].x. Вам не обязательно уже сейчас полностью понимать, как связываются вместе контексты исполнения отдельных окон и фреймов в клиентском JavaScript. Эту тему мы подробно рассмотрим при обсуждении интеграции JavaScript с веббро узерами в главе 13. Сейчас достаточно знать, что гибкость JavaScript позволяет одному интерпретатору JavaScript исполнять сценарии в различных глобаль ных контекстах исполнения и что этим контекстам не нужно быть совершенно раздельными – они могут ссылаться друг на друга. Последнее утверждение надо рассмотреть подробнее. Если JavaScriptкод в од ном контексте исполнения может читать и писать значения свойств и выполнять функции, определенные в другом контексте исполнения, то становятся актуаль ными вопросы безопасности. Возьмем в качестве примера клиентский Java Script. Предположим, что окно броузера A запускает сценарий или содержит ин формацию из вашей локальной сети, а окно B запускает сценарий с некоторого произвольного сайта в Интернете. Скорее всего, мы не захотим предоставлять коду в окне B доступ к свойствам в окне A. Ведь тогда этот код получит возмож ность прочитать важную корпоративную информацию и, например, украсть ее. Следовательно, безопасный запуск JavaScriptкода должен обеспечить специ альный механизм, предотвращающий доступ из одного контекста исполнения в другой, если такой доступ не разрешен. Мы вернемся к этой теме в разделе 13.8. 4.7. Еще об области видимости переменных Когда мы в первый раз обсуждали понятие области видимости переменной, я оп ределил его только на основе лексической структуры JavaScriptкода: глобаль ные переменные имеют глобальную область видимости, а переменные, объяв ленные в функции, – локальную. Если одно определение функции вложено 1 Это отход от темы; если он вам неинтересен, спокойно переходите к следующему разделу. 76 Глава 4. Переменные в другое, то переменные, объявленные в этой вложенной функции, имеют вло женную локальную область видимости. Теперь, когда мы знаем, что глобальные переменные представляют собой свойства глобального объекта, а локальные – свойства особого объекта вызова, мы можем вернуться к понятию области види мости переменной и переосмыслить его. Это даст нам хорошую возможность по новому взглянуть на существование переменных во многих контекстах и глубже понять, как работает JavaScript. В JavaScript с каждым контекстом исполнения связана цепочка областей види' мости (scope chain), представляющая собой список, или цепочку, объектов. Ко гда JavaScriptкоду требуется найти значение переменной x (этот процесс назы вается разрешением имени переменной), он начинает поиск в первом (наиболее глубоком) объекте цепочки. Если в этом объекте отыскивается свойство с име нем x, то используется значение этого свойства. Если в первом объекте не удает ся найти свойство с именем x, то JavaScript продолжает поиск в следующем объ екте цепочки. Если во втором объекте тоже не найдено свойство с именем x, по иск продолжается в следующем объекте, и т. д. В JavaScriptкоде верхнего уровня (в коде, не содержащемся ни в одном из опре делений функций), цепочка областей видимости состоит только из глобального объекта. Все переменные разыскиваются в этом объекте. Если переменная не су ществует, то ее значение равно undefined. В функции (не вложенной) цепочка об ластей видимости состоит из двух объектов. Когда функция ссылается на пере менную, в первую очередь проверяется объект вызова (локальная область види мости), во вторую очередь – глобальный объект (глобальная область видимости). Вложенная функция будет иметь три или более объектов в цепочке областей ви димости. Процесс поиска имени переменной в цепочке областей видимости функции иллюстрирует рис. 4.1. Рис. 4.1. Цепочка областей видимости и разрешения имени переменной Выражения и операторы В этой главе объясняется, как работают выражения и операторы в JavaScript. Те, кто знаком с C, C++ или Java, заметят, что в JavaScript выражения и опера торы очень похожи, и смогут ограничиться беглым просмотром этой главы. А те, кто не программирует на C, C++ или Java, в этой главе узнают все, что требуется знать о выражениях и операторах в JavaScript. 5.1. Выражения Выражение – это фраза языка JavaScript, которая может быть вычислена интер претатором для получения значения. Простейшие выражения – это литералы или имена переменных, например: 1.7 // Числовой литерал "JavaScript is fun!" // Строковый литерал true // Логический литерал null // Литерал значения null /java/ // Литерал регулярного выражения { x:2, y:2 } // Объектный литерал [2,3,5,7,11,13,17,19] // Литерал массива function(x){return x*x;} // Функциональный литерал i // Переменная i sum // Переменная sum Значение выражениялитерала – это просто значение самого литерала. Значение выраженияпеременной – это значение, содержащееся в переменной, или значе ние, на которое переменная ссылается. Эти выражения не очень интересны. Путем объединения простых выражений могут создаваться более сложные (и интересные) выражения. Например, мы ви дели, что и 1.7 – это выражение, и i – это выражение. Следующий пример тоже представляет собой выражение: i + 1.7 78 Глава 5. Выражения и операторы Значение этого выражения определяется путем сложения значений двух более простых выражений. Знак + в этом примере – это оператор, объединяющий два выражения в одно более сложное. Другим оператором является  (минус), объ единяющий выражения путем вычитания. Например: (i + 1.7)  sum В этом выражении оператор «минус» применяется для вычитания значения пе ременой sum из значения предыдущего выражения, i + 1.7. Как вы увидите в следующем разделе, JavaScript поддерживает несколько других операторов, помимо + и . 5.2. Обзор операторов Если вы программировали на C, C++ или Java, то большинство JavaScriptопе раторов должны быть уже вам знакомо. Они сведены в табл. 5.1, к которой мож но обращаться как к справочнику. Обратите внимание: большинство операторов обозначаются символами пунктуации, такими как + и =, а некоторые – ключевы ми словами, например delete и instanceof. И ключевые слова, и знаки пунктуа ции обозначают обычные операторы, просто в первом случае получается более удобочитаемый и менее лаконичный синтаксис. В этой таблице столбец, обозначенный буквой «P», содержит приоритет операто ра, а столбец, обозначенный буквой «A», – ассоциативность оператора (либо L – слева направо, либо R – справа налево). Те, кто пока не понимает, что это такое, получат разъяснения в следующих подразделах, после которых приводятся опи сания самих операторов. Таблица 5.1. JavaScript'операторы P A Оператор Типы операндов Выполняемая операция 15 L . Объект, идентификатор Обращение к свойству L [] Массив, целое число Индексация массива L () Функция, аргументы Вызов функции R new Вызов конструктора Создание нового объекта 14 R ++ Левостороннее выражение Префиксный или постфиксный инкремент (унарный) R  Левостороннее выражение Префиксный или постфиксный декремент (унарный) R  Число Унарный минус (смена знака) R + Число Унарный плюс (нет операции) R ~ Целое число Поразрядное дополнение (унарный) R ! Логическое значение Логическое дополнение (унарный) R delete Левостороннее значение Аннулирование определения свойства (унарный) R typeof Любой Возвращает тип данных (унарный) 5.2. Обзор операторов 79 5.2.1. Количество операндов Операторы могут быть разбиты на категории по количеству требуемых им опе рандов. Большинство JavaScriptоператоров, таких как оператор +, о котором мы уже говорили, двухместные. Такие операторы объединяют два выражения в одно, более сложное. Таким образом, эти операторы работают с двумя операн дами. JavaScript поддерживает также несколько унарных операторов, которые R void Любой Возвращает неопределенное значение (унарный) 13 L *, /, % Числа Умножение, деление, остаток 12 L +,  Числа Сложение, вычитание L + Строки Конкатенация строк 11 L << Целые числа Сдвиг влево L >> Целые числа Сдвиг вправо с расширением знакового разряда L >>> Целые числа Сдвиг вправо с дополнением нулями 10 L <, <= Числа или строки Меньше чем, меньше или равно L >, >= Числа или строки Больше чем, больше или равно L instanceof Объект, конструктор Проверка типа объекта L in Строка, объект Проверка наличия свойства 9 L == Любой Проверка на равенство L != Любой Проверка на неравенство L === Любой Проверка на идентичность L !== Любой Проверка на неидентичность 8 L & Целые числа Поразрядная операция И 7 L ^ Целые числа Поразрядная операция исключающего ИЛИ 6 L | Целые числа Поразрядная операция ИЛИ 5 L && Логические значения Логическое И 4 L || Логические значения Логическое ИЛИ 3 R ?: Логическое значение, любое, любое Условный трехместный оператор 2 R = Левостороннее значение, любое Присваивание R *=, /=, %=, +=, =, <<=, >>=, >>>=, &=, ^=, |= Левостороннее значение, любое Присваивание с операцией 1 L , Любой Множественное вычисление P A Оператор Типы операндов Выполняемая операция 80 Глава 5. Выражения и операторы преобразуют одно выражение в другое, более сложное. Оператор «минус» в вы ражении 3 представляет собой унарный оператор, выполняющий смену знака для операнда 3. И наконец, JavaScript поддерживает один тернарный оператор, условный оператор ?:, который объединяет в одно значение три выражения. 5.2.2. Тип операндов Создавая JavaScriptвыражения, необходимо обращать внимание на типы дан ных, передаваемых операторам, и на типы данных, которые они возвращают. Различные операторы требуют, чтобы операнды возвращали значения опреде ленного типа. Например, нельзя выполнить умножение строк, поэтому выраже ние "a" * "b" не является допустимым в JavaScript. Однако интерпретатор Java Script по мере возможности будет пытаться преобразовывать выражение в тре буемый тип, поэтому выражение "3" * "5" вполне допустимо. Его значением будет число 15, а не строка "15". Более подробно о преобразованиях типов в JavaScript рассказывалось в разделе 3.12. Некоторые операторы ведут себя поразному в зависимости от типа операндов. Самый яркий пример – оператор +, который складывает числовые операнды и выполняет конкатенацию строк. Кроме того, если ему передать одну строку и одно число, он преобразует число в строку и выполнит конкатенацию двух по лученных строк. Например, результатом выражения "1" + 0 будет строка "10". Обратите внимание, что операторы присваивания, как и некоторые другие, тре буют в качестве выражений в левой части левостороннего значения (lvalue). Ле востороннее значение – это исторический термин, обозначающий «выражение, которое может присутствовать в левой части оператора присваивания». В Java Script левосторонними значениями являются переменные, свойства объектов и элементы массивов. Спецификация ECMAScript разрешает встроенным функ циям возвращать левосторонние значения, но не определяет никаких встроен ных функций, ведущих себя подобным образом. И наконец, операторы не всегда возвращают значения того же типа, к которому принадлежат операнды. Операторы сравнения (меньше, равно, больше и т. д.) принимают в качестве аргументов различные типы, но всегда возвращают ре зультат логического типа, показывающий, истинно ли сравнение. Так, выраже ние a < 3 возвращает значение true, если значение переменной a действительно меньше, чем 3. Как мы увидим, логические значения, возвращаемые оператора ми сравнения, используются в инструкциях if, циклах while и for, управляю щих в JavaScript исполнением программы в зависимости от результатов вычис ления выражений с операторами сравнения. 5.2.3. Приоритет операторов В табл. 5.1 в столбце, помеченном буквой «P», указан приоритет каждого опе ратора. Приоритет оператора управляет порядком, в котором выполняются опе рации. Операторы с большим значением приоритета в столбце «P» выполняются раньше, чем те, для которых указаны меньшие значения приоритетов. Рассмотрим следующее выражение: w = x + y * z; 5.3. Арифметические операторы 81 Оператор умножения * имеет больший приоритет по сравнению с оператором сложения +, поэтому умножение выполняется раньше сложения. Кроме того, оператор присваивания = имеет наименьший приоритет, поэтому присваивание выполняется после завершения всех операций в правой части. Приоритет операторов может быть переопределен с помощью скобок. Для того чтобы сложение в предыдущем примере выполнялось раньше, надо написать: w = (x + y)* z; На практике, если вы не уверены в приоритетах операторов, проще всего явно задать порядок вычислений с помощью скобок. Важно знать лишь следующие правила: умножение и деление выполняются раньше сложения и вычитания, а присваивание имеет очень низкий приоритет и почти всегда выполняется по следним. 5.2.4. Ассоциативность операторов В табл. 5.1 в столбце, помеченном буквой «A», указана ассоциативность опера тора. Значение L задает ассоциативность слева направо, а значение R – ассоциа тивность справа налево. Ассоциативность оператора определяет порядок выпол нения операций с одинаковым приоритетом. Ассоциативность слева направо означает, что операции выполняются слева направо. Например, оператор сложе ния имеет ассоциативность слева направо, поэтому следующие два выражения эквивалентны: w = x + y + z; w = ((x + y) + z); Теперь обратите внимание на такие (практически бессмысленные) выражения: x = ~~y; w = x = y = z; q = a?b:c?d:e?f:g; Они эквивалентны следующим выражениям: x = ~((~y)); w = (x = (y = z)); q = a?b:(c?d:(e?f:g)); Причина в том, что унарные операторы, операторы присваивания и условные тернарные операторы имеют ассоциативность справа налево. 5.3. Арифметические операторы Рассказав о приоритетах, ассоциативности и других второстепенных вопросах, мы можем начать обсуждение самих операторов. В этом разделе приведены опи сания арифметических операторов: Сложение (+) Оператор «плюс» складывает числовые операнды или выполняет конкатена цию строк. Если одним из операндов является строка, другой операнд преобра зуется в строку и выполняется конкатенация. Операндыобъекты преобразу 82 Глава 5. Выражения и операторы ются в числа или строки, которые могут быть сложены или конкатенированы. Преобразование выполняется с помощью методов valueOf() и/или toString(). Вычитание () Когда «минус» используется в качестве двухместного оператора, он выполня ет вычитание второго операнда из первого. Если указаны нечисловые операн ды, то оператор пытается преобразовать их в числа. Умножение (*) Оператор * умножает два своих операнда. Нечисловые операнды он пытается преобразовать в числа. Деление (/) Оператор / делит первый операнд на второй. Нечисловые операнды он пыта ется преобразовать в числа. Те, кто привык к языкам программирования, различающим целые и вещественные числа, могут ожидать получения цело численного результата при делении одного целого на другое. Однако в Java Script все числа вещественные, поэтому результатом любого деления являет ся значение с плавающей точкой. Операция 5/2 дает 2.5, а не 2. Результат де ления на ноль – плюс или минус бесконечность, а 0/0 дает NaN. Деление по модулю (%) Оператор % вычисляет остаток, получаемый при целочисленном делении пер вого операнда на второй. Если заданы нечисловые операнды, то оператор пы тается преобразовать их в числа. Знак результата совпадает со знаком перво го операнда, например 5 % 2 дает 1. Оператор деления по модулю обычно при меняется к целым операндам, но работает и для вещественных значений. На пример, 4.3 % 2.1 дает результат 0.1. Унарный минус () Когда минус используется в качестве унарного оператора, он указывается пе ред одиночным операндом и выполняет унарную операцию смены знака. Другими словами, он преобразует положительное значение в отрицательное, и наоборот. Если операнд не является числом, этот оператор пытается преоб разовать его в число. Унарный плюс (+) Для симметрии с оператором «унарный минус» в JavaScript также имеется оператор «унарный плюс». При помощи этого оператора можно явно задать знак числовых литералов, если вы считаете, что это сделает текст программы более понятным: var profit = +1000000; В таком коде оператор «плюс» ничего не делает; результатом его работы яв ляется значение его аргумента. Однако нечисловые аргументы он преобразу ет в числа. Если аргумент не может быть преобразован, возвращается NaN. Инкремент (++) Этот оператор инкрементирует (т. е. увеличивает на единицу) свой единст венный операнд, который должен быть переменной, элементом массива или свойством объекта. Если значение этой переменной, элемента массива или свойства не является числом, оператор сначала пытается преобразовать его 5.4. Операторы равенства 83 в число. Точное поведение этого оператора зависит от его положения по отно шению к операнду. Если поставить его перед операндом (префиксный опера тор инкремента), то к операнду прибавляется 1, а результатом является уве личенное значение операнда. Если же он размещается после операнда (пост фиксный оператор инкремента), то к операнду прибавляется 1, однако ре зультатом является первоначальное значение операнда. Если увеличиваемое значение не является числом, оно в процессе вычисления преобразуется в число. Например, следующий код делает переменные i и j равными 2: i = 1; j = ++i; А этот устанавливает i в 2, а j в 1: i = 1; j = i++; Данный оператор в обеих своих формах чаще всего применяется для увеличе ния счетчика, управляющего циклом. Обратите внимание: нельзя вставлять перевод строки между префиксным или постфиксным оператором инкремен та и его операндом, поскольку точки с запятой в JavaScript вставляются авто матически. Если это сделать, интерпретатор JavaScript будет рассматривать операнд как полноценную инструкцию и вставит после него точку с запятой. Декремент () Этот оператор декрементирует (т. е. уменьшает на 1) свой единственный чи словой операнд, который может представлять собой переменную, элемент массива или свойство объекта. Если значение этой переменной, элемента или свойства не является числом, оператор сначала пытается преобразовать его в число. Как и для оператора ++, точное поведение оператора  зависит от его положения относительно операнда. Будучи поставленным перед операндом, он уменьшает операнд и возвращает уменьшенное значение, после операнда – уменьшает операнд, но возвращает первоначальное значение. 5.4. Операторы равенства В этом разделе описаны операторы равенства и неравенства. Это операторы, сравнивающие два значения и возвращающие логическое значение (true или false) в зависимости от результата сравнения. Как мы увидим в главе 6, чаще всего они применяются в инструкциях if и циклах for для управления ходом ис полнения программы. 5.4.1. Равенство (==) и идентичность (===) Операторы == и === проверяют две величины на совпадение, руководствуясь дву мя разными определениями совпадения. Оба оператора принимают операнды любого типа и возвращают true, если их операнды совпадают, и false, если они различны. Оператор ===, известный как оператор идентичности, проверяет два операнда на «идентичность», руководствуясь строгим определением совпаде ния. Оператор == известен как оператор равенства, он проверяет, равны ли два его операнда в соответствии с менее строгим определением совпадения, допус кающим преобразования типов. 84 Глава 5. Выражения и операторы Оператор идентичности стандартизован в ECMAScript v3 и реализован в Java Script 1.3 и более поздних версиях. С введением оператора идентичности язык JavaScript стал поддерживать операторы =, == и ===. Убедитесь, что вы понимае те разницу между операторами присваивания, равенства и идентичности. Будь те внимательны и применяйте правильные операторы при разработке своих про грамм! Хотя очень заманчиво назвать все три оператора «равно», но во избежа ние путаницы лучше читать оператор = как «получается», или «присваивается», оператор == читать как «равно», а словом «идентично» обозначать оператор ===. В JavaScript числовые, строковые и логические значения сравниваются по значе' нию. В этом случае рассматриваются две различные величины, а операторы == и === проверяют, идентичны ли эти два значения. Это значит, что две переменные равны или идентичны, только если они содержат одинаковое значение. Напри мер, две строки равны, только если обе содержат в точности одинаковые символы. В то же время объекты, массивы и функции сравниваются по ссылке. Это зна чит, что две переменные равны, только если они ссылаются на один и тот же объ ект. Два различных массива никогда не могут быть равными или идентичными, даже если они содержат равные или идентичные элементы. Две переменные, со держащие ссылки на объекты, массивы или функции, равны, только если ссы лаются на один и тот же объект, массив или функцию. Для того чтобы прове рить, содержат ли два различных объекта одинаковые свойства или содержат ли два различных массива одинаковые элементы, надо их проверить на равенство или идентичность каждого свойства или элемента. (И если какоелибо свойство или элемент само является объектом или массивом, решить, на какую глубину вложенности вы хотите выполнять сравнение.) При определении идентичности двух значений оператор === руководствуется следующими правилами: • Если два значения имеют различные типы, они не идентичны. • Два значения идентичны, только если оба они представляют собой числа, имеют одинаковые значения и не являются значением NaN (в этом, последнем случае они не идентичны). Значение NaN никогда не бывает идентичным ни какому значению, даже самому себе! Чтобы проверить, является ли значение значением NaN, следует использовать глобальную функцию isNaN(). • Если оба значения представляют собой строки и содержат одни и те же симво лы в тех же позициях, они идентичны. Если строки отличаются по длине или содержимому, они не идентичны. Обратите внимание, что в некоторых случа ях стандарт Unicode допускает несколько способов кодирования одной и той же строки. Однако для повышения эффективности сравнение строк в Java Script выполняется строго посимвольно, при этом предполагается, что все строки перед сравнением преобразованы в «нормализованную форму». Дру гой способ сравнения строк обсуждается в части III книги при описании мето да String.localeCompare(). • Если оба значения представляют собой логические значения true или false, то они идентичны. • Если оба значения ссылаются на один и тот же объект, массив или функцию, то они идентичны. Если они ссылаются на различные объекты (массивы или функции), они не идентичны, даже если оба имеют идентичные свойства или идентичные элементы. 5.4. Операторы равенства 85 • Если оба значения равны null или undefined, то они идентичны. Следующие правила применяются для определения равенства при помощи опе ратора ==: • Если два значения имеют одинаковый тип, они проверяются на идентич ность. Если значения идентичны, они равны; если они не идентичны, они не равны. • Если два значения не относятся к одному и тому же типу, они все же могут быть равными. Правила и преобразования типов при этом такие: • Если одно значение равно null, а другое – undefined, то они равны. • Если одно значение представляет собой число, а другое – строку, то строка преобразуется в число и выполняется сравнение с преобразованным значе нием. • Если какоелибо значение равно true, оно преобразуется в 1 и сравнение выполняется снова. Если какоелибо значение равно false, оно преобразу ется в 0 и сравнение выполняется снова. • Если одно из значений представляет собой объект, а другое – число или строку, объект преобразуется в элементарный тип и сравнение выполняет ся снова. Объект преобразуется в значение элементарного типа либо с по мощью своего метода toString(), либо с помощью своего метода valueOf(). Встроенные классы базового языка JavaScript сначала пытаются выпол нить преобразование valueOf(), а затем toString(), кроме класса Date, кото рый всегда выполняет преобразование toString(). Объекты, не являющие ся частью базового JavaScript, могут преобразовывать себя в значения эле ментарных типов способом, определенным в их реализации. • Любые другие комбинации значений не являются равными. В качестве примера проверки на равенство рассмотрим сравнение: "1" == true Результат этого выражения равен true, т. е. эти поразному выглядящие значе ния фактически равны. Логическое значение true преобразуется в число 1, и сравнение выполняется снова. Затем строка "1" преобразуется в число 1. По скольку оба числа теперь совпадают, оператор сравнения возвращает true. 5.4.2. Неравенство (!=) и неидентичность (!==) Операторы != и !== выполняют проверки, в точности противоположные операто рам == и ===. Оператор != возвращает false, если два значения равны друг другу, и true в противном случае. Оператор неидентичности !== возвращает false, если два значения идентичны друг другу, и true – в противном случае. Этот оператор стандартизован в ECMAScript v3 и реализован в JavaScript 1.3 и более поздних версиях. Как мы увидим позднее, оператор ! осуществляет операцию логического НЕ. Благодаря этому легче запомнить, что != обозначает «не равно», а !== – «не иден тично». Подробности определения равенства и идентичности для разных типов данных рассмотрены в предыдущем разделе. 86 Глава 5. Выражения и операторы 5.5. Операторы отношения В этом разделе описаны операторы отношения в JavaScript. Это операторы, про веряющие отношение между двумя значениями (такое как «меньше» или «яв ляется ли свойством») и возвращающие true или false в зависимости от того, как соотносятся операнды. Как мы увидим в главе 6, они чаще всего применяются в инструкциях if и циклах while для управления ходом исполнения программы. 5.5.1. Операторы сравнения Из всех типов операторов отношения чаще всего используются операторы срав нения – для определения относительного порядка двух величин. Далее приво дится список операторов сравнения: Меньше (<) Результат оператора < равен true, если первый операнд меньше, чем второй операнд; в противном случае он равен false. Больше (>) Результат оператора > равен true, если его первый операнд больше, чем вто рой операнд; в противном случае он равен false. Меньше или равно (<=) Результатом оператора <= является true, если первый операнд меньше или ра вен второму операнду; в противном случае результат равен false. Больше или равно (>=) Результат оператора >= равен true, если его первый операнд больше второго или равен ему; в противном случае он равен false. Эти операторы позволяют сравнивать операнды любого типа. Однако сравнение может выполняться только для чисел и строк, поэтому операнды, не являющие ся числами или строками, преобразуются. Сравнение и преобразование выпол няется следующим образом: • Если оба операнда являются числами или преобразуются в числа, они срав ниваются как числа. • Если оба операнда являются строками или преобразуются в строки, они срав ниваются как строки. • Если один операнд является строкой или преобразуется в строку, а другой яв ляется числом или преобразуется в число, оператор пытается преобразовать строку в число и выполнить численное сравнение. Если строка не представля ет собой число, она преобразуется в значение NaN и результатом сравнения становится false. (В JavaScript 1.1 преобразование строки в число не дает значения NaN, а приводит к ошибке.) • Если объект может быть преобразован как в число, так и в строку, интерпре татор JavaScript выполняет преобразование в число. Это значит, например, что объекты Date сравниваются как числа, т. е. можно сравнить две даты и оп ределить, какая из них более ранняя. • Если оба операнда не могут быть успешно преобразованы в числа или строки, операторы всегда возвращают false. 5.5. Операторы отношения 87 • Если один из операндов равен или преобразуется в NaN, то результатом опера тора сравнения является false. Имейте в виду, что сравнение строк выполняется строго посимвольно, для чи словых значений каждого символа из кодировки Unicode. В некоторых случаях стандарт Unicode допускает кодирование эквивалентных строк с применением различных последовательностей символов, но операторы сравнения в JavaScript не обнаруживают этих различий в кодировках; предполагается, что все строки представлены в нормализованной форме. Обратите внимание: сравнение строк производится с учетом регистра символов, т. е. в кодировке Unicode (по крайней мере, для подмножества ASCII) все прописные буквы «меньше» всех строчных букв. Это правило может приводить к непонятным результатам. Например, со гласно оператору < строка "Zoo" меньше строки "aardvark". При сравнении строк более устойчив метод String.localeCompare(), который так же учитывает национальные определения «алфавитного порядка». Для сравне ния без учета регистра необходимо сначала преобразовать строки в нижний или верхний регистр с помощью метода String.toLowerCase() или String.toUpperCase(). Операторы <= (меньше или равно) и >= (больше или равно) определяют «равенст во» двух значений не при помощи операторов равенства или идентичности. Опе ратор «меньше или равно» определяется просто как «не больше», а оператор «больше или равно» – как «не меньше». Единственное исключение имеет место, когда один из операндов представляет собой значение NaN (или преобразуется в него); в этом случае все четыре оператора сравнения возвращают false. 5.5.2. Оператор in Оператор in требует, чтобы левый операнд был строкой или мог быть преобразо ван в строку. Правым операндом должен быть объект (или массив). Результатом оператора будет true, если левое значение представляет собой имя свойства объ екта, указанного справа. Например: var point = { x:1, y:1 }; // Определяем объект var has_x_coord = "x" in point; // Равно true var has_y_coord = "y" in point; // Равно true var has_z_coord = "z" in point; // Равно false; это не трехмерная точка var ts = "toString" in point; // Унаследованное свойство; равно true 5.5.3. Оператор instanceof Оператор instanceof требует, чтобы левым операндом был объект, а правым – имя класса объектов. Результатом оператора будет true, если объект, указанный слева, представляет собой экземпляр класса, указанного справа; в противном случае результатом будет false. В главе 9 мы увидим, что в JavaScript классы объектов определяются инициализировавшей их функциейконструктором. Следовательно, правый операнд instanceof должен быть именем функцииконст руктора. Обратите внимание: все объекты представляют собой экземпляры класса Object. Например: var d = new Date(); // Создаем новый объект с помощью конструктора Date() d instanceof Date; // Равно true; объект d был создан с помощью // функции Date() 88 Глава 5. Выражения и операторы d instanceof Object; // Равно true; все объекты представляют собой экземпляры // класса Object d instanceof Number; // Равно false; d не является объектом Number var a = [1, 2, 3]; // Создаем массив с помощью литерала массива a instanceof Array; // Равно true; a – это массив a instanceof Object; // Равно true; все массивы представляют собой объекты a instanceof RegExp; // Равно false; массивы не являются регулярными выражениями Если левый операнд instanceof не является объектом или если правый операнд – это объект, не имеющий функцииконструктора, instanceof возвращает false. Но если правый операнд вообще не является объектом, возвращается ошибка времени выполнения. 5.6. Строковые операторы Как уже говорилось в предыдущих разделах, существует несколько операторов, ведущих себя особым образом, когда в качестве операндов выступают строки. Оператор + выполняет конкатенацию двух строковых операндов. Другими сло вами, создает новую строку, состоящую из первой строки, за которой следует вторая строка. Так, следующее выражение равно строке "hello there": "hello" + " " + "there" Следующие инструкции дают в результате строку "22": a = "2"; b = "2"; c = a + b; Операторы <, <=, > и >= сравнивают две строки и определяют, в каком порядке они следуют друг за другом. Сравнение основано на алфавитном порядке. Как было отмечено в разделе 5.1.1, этот алфавитный порядок базируется на исполь зуемой в JavaScript кодировке Unicode. В этой кодировке все прописные буквы латинского алфавита идут раньше, чем все строчные буквы (прописные «мень ше» строчных), что может приводить к неожиданным результатам. Операторы равенства == и неравенства != применяются не только к строкам, но, как мы видели, ко всем типам данных, и при работе со строками ничем особен ным не выделяются. Оператор + особенный, поскольку дает приоритет строковым операндам перед числовыми. Как уже отмечалось, если один из операндов оператора + представ ляет собой строку (или объект), то другой операнд преобразуется в строку (или оба операнда преобразуются в строки) и операнды конкатенируются, а не скла дываются. С другой стороны, операторы сравнения выполняют строковое срав нение, только если оба операнда представляют собой строки. Если только один операнд – строка, то интерпретатор JavaScript пытается преобразовать ее в чис ло. Далее следует иллюстрация этих правил: 1 + 2 // Сложение. Результат равен 3. "1" + "2" // Конкатенация. Результат равен "12". "1" + 2 // Конкатенация; 2 преобразуется в "2". Результат равен "12". 11 < 3 // Численное сравнение. Результат равен false. "11" < "3" // Строковое сравнение. Результат равен true. 5.7. Логические операторы 89 "11" < 3 // Численное сравнение; "11" преобразуется в 11. Результат равен false. "one" < 3 // Численное сравнение; "one" преобразуется в NaN. Результат равен false. И наконец, важно заметить, что когда оператор + применяется к строкам и чис лам, он может быть неассоциативным. Другими словами, результат может зави сеть от порядка, в котором выполняются операции. Это можно видеть на следую щих примерах: s = 1 + 2 + " слепых мышей"; // Равно "3 слепых мышей" t = "слепых мышей: " + 1 + 2; // Равно " слепых мышей: 12" Причина этой удивительной разницы в поведении заключается в том, что опера тор + работает слева направо, если только скобки не меняют этот порядок. Следо вательно, последние два примера эквивалентны следующему: s = (1 + 2) + "слепых мышей"; // Результат первой операции  число; второй  строка t = ("слепых мышей: " + 1) + 2; // Результаты обеих операций  строки 5.7. Логические операторы Логические операторы обычно используются для выполнения операций булевой алгебры. Они часто применяются в сочетании с операторами сравнения для осу ществления сложных сравнений с участием нескольких переменных в инструк циях if, while и for. 5.7.1. Логическое И (&&) При использовании с логическими операндами оператор && выполняет операцию логического И над двумя значениями: он возвращает true тогда и только тогда, когда первый и второй операнды равны true. Если один или оба операнда равны false, оператор возвращает false. Реальное поведение этого оператора несколько сложнее. Он начинает работу с вы числения левого операнда. Если получившееся значение может быть преобразо вано в false (если левый операнд равен null, 0, "" или undefined), оператор возвра щает значение левого выражения. В противном случае оператор вычисляет пра вый операнд и возвращает значение этого выражения.1 Следует отметить, что в зависимости от значения левого выражения этот опера тор либо вычисляет, либо не вычисляет правое выражение. Иногда встречается код, намеренно использующий эту особенность оператора &&. Так, следующие две строки JavaScriptкода дают эквивалентные результаты: if (a == b) stop(); (a == b) && stop(); Некоторые программисты (особенно работавшие с Perl) считают такой стиль программирования естественным и полезным, но я не рекомендую прибегать к таким приемам. Тот факт, что вычисление правой части не гарантируется, час то является источником ошибок. Рассмотрим следующий код: 1 В JavaScript 1.0 и 1.1, если в результате вычисления левого операнда получается значение false, оператор && возвращает непреобразованное значение левого опе ранда. 90 Глава 5. Выражения и операторы if ((a == null) && (b++ > 10)) stop(); Скорее всего, эта инструкция не делает того, что предполагал программист, т. к. оператор инкремента в правой части не вычисляется в тех случаях, когда левое выражение равно false. Чтобы обойти этот подводный камень, не помещайте вы ражения, имеющие побочные эффекты (присваивания, инкременты, декремен ты и вызовы функций), в правую часть оператора &&, если только не уверены аб солютно в том, что делаете. Несмотря на довольно запутанный алгоритм работы этого оператора, проще все го и абсолютно безопасно рассматривать его как оператор булевой алгебры. На самом деле он не возвращает логического значения, но то значение, которое он возвращает, всегда может быть преобразовано в логическое. 5.7.2. Логическое ИЛИ (||) При использовании с логическими операндами оператор || выполняет операцию «логическое ИЛИ» над двумя значениями: он возвращает true, если первый или второй операнд (или оба операнда) равен true. Если оба операнда равны false, он возвращает false. Хотя оператор || чаще всего применяется просто как оператор «логическое ИЛИ», он, как и оператор &&, ведет себя более сложным образом. Его работа на чинается с вычисления левого операнда. Если значение этого выражения может быть преобразовано в true, возвращается значение левого выражения. В против ном случае оператор вычисляет правый операнд и возвращает значение этого выражения.1 Как и в операторе &&, следует избегать правых операндов, имеющих побочные эффекты, если только вы умышленно не хотите воспользоваться тем обстоятель ством, что правое выражение может не вычисляться. Даже когда оператор || применяется с операндами нелогического типа, его все равно можно рассматривать как оператор «логическое ИЛИ», т. к. возвращае мое им значение независимо от типа может быть преобразовано в логическое. В то же время иногда можно встретить конструкции, где оператор || использует ся со значениями, не являющимися логическими, и где учитывается тот факт, что оператор возвращает значение, также не являющееся логическим. Суть та кого подхода основана на том, что оператор || выбирает первое значение из пред ложенных альтернатив, которое не является значением null (т. е. первое значе ние, которое преобразуется в логическое значение true). Далее приводится при мер такой конструкции: // Если переменная max_width определена, используется ее значение. // В противном случае значение извлекается из объекта preferences. // Если объект (или его свойство max_with) не определен, используется // значение константы, жестко зашитой в текст программы. var max = max_width || preferences.max_width || 500; 1 В JavaScript 1.0 и 1.1, если левый операнд может быть преобразован в true, опера тор возвращает true, а не непреобразованное значение. 5.8. Поразрядные операторы 91 5.7.3. Логическое НЕ (!) Оператор ! представляет собой унарный оператор, помещаемый перед одиноч ным операндом. Оператор инвертирует значение своего операнда. Так, если пе ременная a имеет значение true (или представляет собой значение, преобразуе мое в true), то выражение !a имеет значение false. И если выражение p && q равно false (или значению, преобразуемому в false), то выражение !(p && q) равно true. Обратите внимание, что можно преобразовать значение любого типа в логиче ское, применив этот оператор дважды: !!x. 5.8. Поразрядные операторы Несмотря на то, что все числа в JavaScript вещественные, поразрядные операторы требуют в качестве операндов целые числа. Они работают с такими операндами с помощью 32разрядного целого представления, а не эквивалентного представле ния с плавающей точкой. Четыре из этих операторов выполняют поразрядные операции булевой алгебры, аналогично описанным ранее логическим операторам, но рассматривая каждый бит операнда как отдельное логическое значение. Три других поразрядных оператора применяются для сдвига битов влево и вправо. Если операнды не являются целыми числами или слишком велики и не помеща ются в 32разрядное целое, поразрядные операторы просто «втискивают» опе ранды в 32разрядное целое, отбрасывая дробную часть операнда и любые биты старше 32го. Операторы сдвига требуют, чтобы значение правого операнда было целым числом от 0 до 31. После преобразования этого операнда в 32разрядное целое вышеописанным образом они отбрасывают любые биты старше 5го, полу чая число в соответствующем диапазоне. Те, кто не знаком с двоичными числами и двоичным представлением десятич ных целых чисел, могут пропустить операторы, рассматриваемые в этом разде ле. Они требуются для низкоуровневых манипуляций с двоичными числами и достаточно редко применяются при программировании на JavaScript. Далее приводится список поразрядных операторов: Поразрядное И (&) Оператор & выполняет операцию «логическое И» над каждым битом своих операндов. Бит результата будет равен 1, только если равны 1 соответствую щие биты обоих операндов. То есть выражение 0x1234 & 0x00FF даст в результа те число 0x0034. Поразрядное ИЛИ (|) Оператор | выполняет операцию «логическое ИЛИ» над каждым битом своих операндов. Бит результата будет равен 1, если равен 1 соответствующий бит хотя бы в одном операнде. Например, 9 | 10 равно 11. Поразрядное исключающее ИЛИ (^) Оператор ^ выполняет логическую операцию «исключающее ИЛИ» над каж дым битом своих операндов. Исключающее ИЛИ обозначает, что должен быть истинен либо первый операнд, либо второй, но не оба сразу. Бит резуль тата устанавливается, если соответствующий бит установлен в одном (но не в обоих) из двух операндов. Например, 9 ^ 10 равно 3. 92 Глава 5. Выражения и операторы Поразрядное НЕ (~) Оператор ~ представляет собой унарный оператор, указываемый перед своим единственным целым аргументом. Он выполняет инверсию всех битов опе ранда. Изза способа представления целых со знаком в JavaScript применение оператора ~ к значению эквивалентно изменению его знака и вычитанию 1. Например, ~0x0f равно 0xfffffff0, или 16. Сдвиг влево (<<) Оператор << сдвигает все биты в первом операнде влево на количество пози ций, указанное во втором операнде, который должен быть целым числом в диапазоне от 0 до 31. Например, в операции a << 1 первый бит в a становится вторым битом, второй бит становится третьим и т. д. Новым первым битом становится ноль, значение 32го бита теряется. Сдвиг значения влево на одну позицию эквивалентен умножению на 2, на две позиции – умножению на 4, и т. д. Например, 7 << 1 равно 14. Сдвиг вправо с сохранением знака (>>) Оператор >> перемещает все биты своего первого операнда вправо на количе ство позиций, указанное во втором операнде (целое между 0 и 31). Биты, сдвинутые за правый край, теряются. Самый старший бит (32й) не меняется, чтобы сохранить знак результата. Если первый операнд положителен, стар шие биты результата заполняются нулями; если первый операнд отрицате лен, старшие биты результата заполняются единицами. Сдвиг значения вправо на одну позицию эквивалентен делению на 2 (с отбрасыванием остат ка), а сдвиг вправо на две позиции эквивалентен делению на 4 и т. д. Напри мер, 7 >> 1 равно 3, а 7 >> 1 равно 4. Сдвиг вправо с заполнением нулями (>>>) Оператор >>> аналогичен оператору >> за исключением того, что при сдвиге старшие разряды заполняются нулями независимо от знака первого операн да. Например, 1 >> 4 равно 1, а 1 >>> 4 равно 268435455 (0x0fffffff). 5.9. Операторы присваивания Как мы видели при обсуждении переменных в главе 4, для присваивания значе ния переменной в JavaScript используется символ =. Например: i = 0 В JavaScript можно не рассматривать такую строку как выражение, которое имеет результат, но это действительно выражение и формально знак = представ ляет собой оператор. Левым операндом оператора = должна быть переменная, элемент массива или свойство объекта. Правым операндом может быть любое значение любого типа. Значением оператора присваивания является значение правого операнда. По бочный эффект оператора = заключается в присваивании значения правого опе ранда переменной, элементу массива или свойству, указанному слева, так что при последующих обращениях к переменной, элементу массива или свойству будет получено это значение. 5.9. Операторы присваивания 93 Поскольку = представляет собой оператор, его можно включать в более сложные выражения. Так, в одном выражении можно совместить операции присваива ния и проверки значения: (a = b) == 0 При этом следует отчетливо понимать, что между операторами = и == есть разница! Если в выражении присутствует несколько операторов присваивания, они вы числяются справа налево. Поэтому можно написать код, присваивающий одно значение нескольким переменным, например: i = j = k = 0; Помните, что каждое выражение присваивания имеет значение, равное значению правой части. Поэтому в приведенном коде значение первого присваивания (са мого правого) становится правой частью второго присваивания (среднего), а это значение становится правой частью последнего (самого левого) присваивания. 5.9.1. Присваивание с операцией Помимо обычного оператора присваивания (=) JavaScript поддерживает несколь ко других операторовсокращений, объединяющих присваивание с некоторой другой операцией. Например, оператор += выполняет сложение и присваивание. Следующие выражения эквивалентны: total += sales_tax total = total + sales_tax Как можно было ожидать, оператор += работает и с числами, и со строками. Если операнды числовые, он выполняет сложение и присваивание, а если строковые – конкатенацию и присваивание. Из подобных ему операторов можно назвать =, *=, &= и др. Все операторы при сваивания с операцией перечислены в табл. 5.2. Таблица 5.2. Операторы присваивания Оператор Пример Эквивалент += a += b a = a + b = a = b a = a – b *= a *= b a = a * b /= a /= b a = a / b %= a %= b a = a % b <<= a <<= b a = a << b >>= a >>= b a = a >> b >>>= a >>>= b a = a >>> b &= a &= b a = a & b |= a |= b a = a | b ^= a ^= b a = a ^ b 94 Глава 5. Выражения и операторы В большинстве случаев следующие выражения эквивалентны (здесь op означает оператор): a op= b a = a op b Эти выражения отличаются, только если a содержит операции, имеющие побоч ные эффекты, такие как вызов функции или оператор инкремента. 5.10. Прочие операторы JavaScript поддерживает еще несколько операторов, которые описываются в сле дующих разделах. 5.10.1. Условный оператор (?:) Условный оператор – это единственный тернарный оператор (с тремя операнда ми) в JavaScript и иногда он так и называется – «тернарный оператор». Этот опе ратор обычно записывается как ?:, хотя в текстах программ он выглядит подру гому. Он имеет три операнда, первый идет перед ?, второй – между ? и :, третий – после :. Используется он следующим образом: x > 0 ? x*y : x*y Первый операнд условного оператора должен быть логическим значением (или преобразовываться в логическое значение) – обычно это результат выражения сравнения. Второй и третий операнды могут быть любыми значениями. Значе ние, возвращаемое условным оператором, зависит от логического значения пер вого операнда. Если этот операнд равен true, то условное выражение принимает значение второго операнда. Если первый операнд равен false, то условное выра жение принимает значение третьего операнда. Тот же результат можно получить с помощью инструкции if, но оператор ?: час то оказывается удобным сокращением. Вот типичный пример, в котором прове ряется, определена ли переменная, и если да, то берется ее значение, а если нет, берется значение по умолчанию: greeting = "hello " + (username != null ? username : "there"); Эта запись эквивалентна следующей конструкции if, но более компактна: greeting = "hello "; if (username != null) greeting += username; else greeting += "there"; 5.10.2. Оператор typeof Унарный оператор typeof помещается перед единственным операндом, который может иметь любой тип. Его значение представляет собой строку, указывающую тип данных операнда. Результатом оператора typeof будет строка "number", "string" или "boolean", если его операндом является число, строка или логическое значение соответственно. 5.10. Прочие операторы 95 Для объектов, массивов и (как ни странно) значения null результатом будет строка "object". Для операндовфункций результатом будет строка "function", а для неопределенного операнда – строка "undefined". Значение оператора typeof равно "object", когда операнд представляет собой объ ектобертку Number, String или Boolean. Оно также равно "object" для объектов Date и RegExp. Для объектов, не являющихся частью базового языка JavaScript, а предоставляемых контекстом, в который встроен JavaScript, возвращаемое оператором typeof значение зависит от реализации. Однако в клиентском языке JavaScript значение оператора typeof обычно равно "object" для всех клиентских объектов – так же, как и для всех базовых объектов. Оператор typeof может применяться, например, в таких выражениях: typeof i (typeof value == "string") ? "'" + value + "'" : value Операнд typeof можно заключить в скобки, благодаря чему ключевое слово ty peof выглядит как имя функции, а не как ключевое слово или оператор: typeof(i) Для всех объектных типов и типов массивов результатом оператора typeof явля ется строка "object", поэтому он может быть полезен только для того, чтобы от личить объекты от базовых типов. Для того чтобы отличить один объектный тип от другого, следует обратиться к другим приемам, таким как использование опе ратора instanceof или свойства constructor (подробности вы найдете в описании свойства Object.constructor, в третьей части книги). Оператор typeof определен в спецификации ECMAScript v1 и реализован в Java Script 1.1 и более поздних версиях. 5.10.3. Оператор создания объекта (new) Оператор new создает новый объект и вызывает функциюконструктор для его инициализации. Это унарный оператор, указываемый перед вызовом конструк тора и имеющий следующий синтаксис: new конструктор(аргументы) Здесь конструктор – это выражение, результатом которого является функция конструктор, и за ним должны следовать ноль или более аргументов, разделен ных запятыми и заключенных в круглые скобки. Как особый случай и только для оператора new JavaScript упрощает грамматику, допуская отсутствие ско бок, если у функции нет аргументов. Вот несколько примеров использования оператора new: o = new Object; // Здесь необязательные скобки опущены d = new Date(); // Возвращает объект Date, содержащий текущее время c = new Rectangle(3.0, 4.0, 1.5, 2.75); // Создает объект класса Rectangle obj[i] = new constructors[i](); Оператор new сначала создает новый объект с неопределенными свойствами, а за тем вызывает заданную функциюконструктор, передавая ей указанные аргу менты, а также только что созданный объект в качестве значения ключевого слова this. С помощью этого слова функцияконструктор может инициализиро 96 Глава 5. Выражения и операторы вать новый объект любым необходимым образом. В главе 7 оператор new, ключе вое слово this и функцииконструкторы рассмотрены более подробно. Оператор new может также применяться для создания массивов с помощью син таксиса new Array(). Подробнее о создании объектов и массивов и работе с ними мы поговорим в главе 7. 5.10.4. Оператор delete Унарный оператор delete выполняет попытку удалить свойство объекта, эле мент массива или переменную, указанную в его операнде.1 Он возвращает true, если удаление прошло успешно, и false в противном случае. Не все переменные и свойства могут быть удалены – некоторые встроенные свойства из базового и клиентского языков JavaScript устойчивы к операции удаления. Кроме того, не могут быть удалены переменные, определенные пользователем с помощью инструкции var. Если оператор delete вызывается для несуществующего свойст ва, он возвращает true. (Как ни странно, стандарт ECMAScript определяет, что оператор delete также возвращает true, если его операнд не является свойством, элементом массива или переменной.) Далее приводится несколько примеров применения этого оператора: var o = {x:1, y:2}; // Определяем переменную; инициализируем ее объектом delete o.x; // Удаляем одно из свойств объекта; возвращает true typeof o.x; // Свойство не существует; возвращает "undefined" delete o.x; // Удаляем несуществующее свойство; возвращает true delete o; // Объявленную переменную удалить нельзя; возвращает false delete 1; // Нельзя удалить целое; возвращает true x = 1; // Неявно объявляем переменную без ключевого слова var delete x; // Этот вид переменных можно удалять; возвращает true x; // Ошибка времени выполнения: x не определено Обратите внимание: удаленное свойство, переменная или элемент массива не просто устанавливается в undefined. Когда свойство удалено, оно прекращает су ществование. Эта тема обсуждалась в разделе 4.3.2. Важно понимать, что оператор delete влияет только на свойства, но не на объек ты, на которые эти свойства ссылаются. Взгляните на следующий фрагмент: var my = new Object(); // Создаем объект по имени "my" my.hire = new Date(); // my.hire ссылается на объект Date my.fire = my.hire; // my.fire ссылается на тот же объект delete my.hire; // свойство hire удалено; возвращает true document.write(my.fire); // Но my.fire продолжает ссылаться на объект Date 5.10.5. Оператор void Унарный оператор void указывается перед своим единственным операндом, тип которого может быть любым. Действие этого оператора необычно: он отбрасыва 1 Тем, кто программировал на C++, следует обратить внимание, что оператор delete в JavaScript совершенно не похож на оператор delete в C++. В JavaScript освобож дение памяти выполняется сборщиком мусора автоматически и беспокоиться о явном освобождении памяти не надо. Поэтому в операторе delete в стиле C++, удаляющем объекты без остатка, нет необходимости. 5.10. Прочие операторы 97 ет значение операнда и возвращает undefined. Чаще всего этот оператор применя ется на стороне клиента в URLадресе с признаком псевдопротокола javascript:, где позволяет вычислять выражение ради его побочных действий, не отображая в броузере вычисленное значение. Например, можно использовать оператор void в HTMLтеге: Открыть новое окно Другое применение void – это намеренная генерация значения undefined. Опера тор void определяется в ECMAScript v1 и реализуется в JavaScript 1.1. В ECMA Script v3 определяется глобальное свойство undefined, реализованное в Java Script 1.5. Однако для сохранения обратной совместимости лучше обращаться к выражению вроде void 0, а не к свойству undefined. 5.10.6. Оператор «запятая» Оператор «запятая» (,) очень прост. Он вычисляет свой левый операнд, вычисля ет свой правый операнд и возвращает значение правого операнда, т. е. следую щая строка i=0, j=1, k=2; возвращает значение 2 и практически эквивалентна записи: i = 0; j = 1; k = 2; Этот странный оператор полезен лишь в ограниченных случаях; в основном то гда, когда требуется вычислить несколько независимых выражений с побочны ми эффектами там, где допускается только одно выражение. На практике опера тор «запятая» фактически используется только в сочетании с инструкцией for, которую мы рассмотрим в главе 6. 5.10.7. Операторы доступа к массивам и объектам Как отмечалось в главе 3, можно обращаться к элементам массива посредством квадратных скобок ([]), а к элементам объекта – посредством точки (.). И квад ратные скобки, и точка рассматриваются в JavaScript как операторы. Оператору «точка» в качестве левого операнда требуется объект, а в качестве правого – идентификатор (имя свойства). Правый операнд не может быть стро кой или переменной, содержащей строку; он должен быть точным именем свой ства или метода без какихлибо кавычек. Вот несколько примеров: document.lastModified navigator.appName frames[0].length document.write("hello world") Если указанное свойство в объекте отсутствует, интерпретатор JavaScript не ге нерирует ошибку, а возвращает в качестве значения выражения undefined. Большинство операторов допускают произвольные выражения для всех своих операндов, если только тип операнда в данном случае допустим. Оператор «точ 98 Глава 5. Выражения и операторы ка» представляет собой исключение: правый операнд должен быть идентифика тором. Ничего другого не допускается. Оператор [] обеспечивает доступ к элементам массива. Он также обеспечивает доступ к свойствам объекта без ограничений, накладываемых на правый опе ранд оператора «точка». Если первый операнд (указанный перед левой скобкой) ссылается на массив, то второй операнд (указанный между скобками) должен быть выражением, имеющим целое значение. Например: frames[1] document.forms[i + j] document.forms[i].elements[j++] Если первый операнд оператора [] представляет собой ссылку на объект, то вто рой должен быть выражением, результатом которого является строка, соответ ствующая имени свойства объекта. Обратите внимание: в этом случае второй операнд представляет собой строку, а не идентификатор. Она может быть либо константой, заключенной в кавычки, либо переменной или выражением, ссы лающимся на строку. Например: document["lastModified"] frames[0]['length'] data["val" + i] Оператор [] обычно применяется для обращения к элементам массива. Для дос тупа к свойствам объекта он менее удобен, чем оператор «точка», т. к. требует заключения имени свойства в кавычки. Однако когда объект выступает в роли ассоциативного массива, а имена свойств генерируются динамически, оператор «точка» использоваться не может и следует применять оператор []. Чаще всего такая ситуация возникает в случае применения цикла for/in, рассмотренного в главе 6. Например, в следующем фрагменте для вывода имен и значений всех свойств объекта o используются цикл for/in и оператор []: for (f in o) { document.write('o.' + f + ' = ' + o[f]); document.write('
'); } 5.10.8. Оператор вызова функции Оператор () предназначен в JavaScript для вызова функций. Этот оператор не обычен в том отношении, что не имеет фиксированного количества операндов. Первый операнд – это всегда имя функции или выражение, ссылающееся на функцию. За ним следует левая скобка и любое количество дополнительных опе рандов, которые могут быть произвольными выражениями, отделенными друг от друга запятыми. За последним операндом следует правая скобка. Оператор () вычисляет все свои операнды и затем вызывает функцию, заданную первым опе рандом, используя в качестве аргументов оставшиеся операнды. Например: document.close() Math.sin(x) alert("Welcome " + name) Date.UTC(2000, 11, 31, 23, 59, 59) funcs[i].f(funcs[i].args[0], funcs[i].args[1]) Инструкции Как мы видели в предыдущей главе, выражения – это «фразы» на языке Java Script, а в результате вычисления выражений получаются значения. Входящие в выражения операторы могут иметь побочные эффекты, но обычно сами выра жения ничего не делают. Чтобы чтото произошло, необходимо использовать ин' струкции JavaScript, которые похожи на полноценные предложения обычного языка или команды. В данной главе описано назначение и синтаксис различных JavaScriptинструкций. Программа на JavaScript представляет собой набор ин струкций, и как только вы познакомитесь с этими инструкциями, вы сможете приступить к написанию программ. Прежде чем начать разговор об JavaScriptинструкциях, вспомним, что в разде ле 2.4 говорилось, что в JavaScript инструкции отделяются друг от друга точка ми с запятой. Однако если каждая инструкция помещается на отдельной строке, интерпретатор JavaScript допускает их отсутствие. Тем не менее желательно вы работать привычку всегда ставить точки с запятой. 6.1. Инструкциивыражения Простейший вид инструкций в JavaScript – это выражения, имеющие побочные эффекты. Мы встречали их в главе 5. Основная категория инструкцийвыраже ний – это инструкции присваивания. Например: s = "Привет " + name; i *= 3; Операторы инкремента и декремента, ++ и , родственны операторам присваи вания. Их побочным эффектом является изменение значения переменной, как при выполнении присваивания: counter++; Оператор delete имеет важный побочный эффект – удаление свойства объекта. Поэтому он почти всегда применяется как инструкция, а не как часть более сложного выражения: 100 Глава 6. Инструкции delete o.x; Вызовы функций – еще одна большая категория инструкцийвыражений. На пример: alert("Добро пожаловать, " + name); window.close(); Эти вызовы клиентских функций представляют собой выражения, однако они влияют на вебброузер, поэтому являются также и инструкциями. Если функция не имеет какихлибо побочных эффектов, нет смысла вызывать ее, если только она не является частью инструкции присваивания. Например, никто не станет просто вычислять косинус и отбрасывать результат: Math.cos(x); Наоборот, надо вычислить значение и присвоить его переменной для дальнейше го использования: cx = Math.cos(x); Опять же обратите внимание: каждая строка этих примерах завершается точкой с запятой. 6.2. Составные инструкции В главе 5 мы видели, что объединить несколько выражений в одно можно по средством оператора «запятая». В JavaScript имеется также способ объединения нескольких инструкций в одну инструкцию или в блок инструкций. Это делает ся простым заключением любого количества инструкций в фигурные скобки. Таким образом, следующие строки рассматриваются как одна инструкция и мо гут использоваться везде, где интерпретатор JavaScript требует наличия единст венной инструкции: { x = Math.PI; cx = Math.cos(x); alert("cos(" + x + ") = " + cx); } Обратите внимание, что хотя блок инструкций действует как одна инструкция, он не завершается точкой с запятой. Отдельные инструкции внутри блока завер шаются точками с запятой, однако сам блок – нет. Если объединение выражений с помощью оператора «запятая» редко использу ется, то объединение инструкций в блоки кода распространено повсеместно. Как мы увидим в последующих разделах, некоторые JavaScriptинструкции сами со держат другие инструкции (так же как выражения могут содержать другие вы ражения); такие инструкции называются составными. Формальный синтаксис JavaScript определяет, что каждая из этих составных инструкций содержит оди ночную подынструкцию. Блоки инструкций позволяют помещать любое коли чество инструкций там, где требуется наличие одной подынструкции. Исполняя составную инструкцию, интерпретатор JavaScript просто исполняет одну за другой составляющие ее инструкции в том порядке, в котором они запи 6.3. Инструкция if 101 саны. Обычно интерпретатор исполняет все инструкции, однако в некоторых слу чаях выполнение составной инструкции может быть внезапно прервано. Это про исходит, если в составной инструкции содержится инструкция break, continue, re turn или throw и если при выполнении возникает ошибка либо вызов функции приводит к ошибке или генерации необрабатываемого исключения. Об этих вне запных прерываниях работы мы узнаем больше в последующих разделах. 6.3. Инструкция if Инструкция if – это базовая управляющая инструкция, позволяющая интер претатору JavaScript принимать решения или, точнее, исполнять инструкции в зависимости от условий. Инструкция имеет две формы. Первая: if (выражение) инструкция В этой форме инструкции if сначала вычисляется выражение. Если полученный результат равен true или может быть преобразован в true, то исполняется инструк ция. Если выражение равно false или преобразуется в false, то инструкция не ис полняется. Например: if (username == null) // Если переменная username равна null или undefined, username = "John Doe"; // определяем ее Аналогично: // Если переменная username равна null, undefined, 0, "" или NaN, она // преобразуется в false, и эта инструкция присвоит переменной новое значение. if (!username) username = "John Doe"; Несмотря на кажущуюся избыточность, скобки вокруг выражения являются обязательной частью синтаксиса инструкции if. Как было упомянуто в преды дущем разделе, мы всегда можем заменить одиночную инструкцию блоком ин струкций. Поэтому инструкция if может выглядеть так: if ((address == null) || (address == "")) { address = "undefined"; alert("Пожалуйста, укажите почтовый адрес."); } Отступы, присутствующие в этих примерах, не обязательны. Дополнительные пробелы и табуляции игнорируются в JavaScript, и поскольку мы ставили после каждой отдельной инструкции точку с запятой, эти примеры могли быть запи саны в одну строку. Оформление текста с использованием символов перевода строки и отступов, как это показано здесь, облегчает чтение и понимание кода. Вторая форма инструкции if вводит конструкцию else, исполняемую в тех слу чаях, когда выражение равно false. Ее синтаксис: if (выражение) инструкция1 else инструкция2 В этой форме инструкции сначала вычисляется выражение, и если оно равно true, то исполняется инструкция1, в противном случае исполняется инструкция2. Например: 102 Глава 6. Инструкции if (username != null) alert("Привет " + username + "\nДобро пожаловать на мою домашнюю страницу."); else { username = prompt("Добро пожаловать!\n Как вас зовут?"); alert("Привет " + username); } При наличии вложенных инструкций if с блоками else требуется некоторая ос торожность – необходимо гарантировать, что else относится к соответствующей инструкции if. Рассмотрим следующие строки: i = j = 1; k = 2; if (i == j) if (j == k) document.write("i равно k"); else document.write("i не равно j"); // НЕПРАВИЛЬНО!! В этом примере внутренняя инструкция if является единственной инструкцией внешней инструкции if. К сожалению, не ясно (если исключить подсказку, ко торую дают отступы), к какой инструкции if относится блок else. А отступы в этом примере выставлены неправильно, ведь интерпретатор JavaScript реаль но интерпретирует предыдущий пример так: if (i == j) { if (j == k) document.write("i равно k"); else document.write("i не равно j"); // OOPS! } Правило JavaScript (и большинства других языков программирования): конст рукция else является частью ближайшей к ней инструкции if. Чтобы сделать этот пример менее двусмысленным и более легким для чтения, понимания, со провождения и отладки, надо поставить фигурные скобки: if (i == j) { if (j == k) { document.write("i равно k"); } } else { // Вот какая разница возникает изза местоположения фигурных скобок! document.write("i не равно j"); } Многие программисты заключают тело инструкций if и else (а также других со ставных инструкций, таких как циклы while) в фигурные скобки, даже когда те ло состоит только из одной инструкции. Последовательное применение этого правила поможет избежать неприятностей, подобных только что описанной. 6.4. Инструкция else if Мы видели, что инструкция if/else используется для проверки условия и вы полнения одного из двух фрагментов кода в зависимости от результата провер 6.5. Инструкция switch 103 ки. Но что если требуется выполнить один из многих фрагментов кода? Возмож ный способ сделать это состоит в применении инструкции else if. Формально это не JavaScriptинструкция, а лишь распространенный стиль программирования, состоящий в применении повторяющихся инструкций if/else: if (n == 1) { // Исполняем блок кода 1 } else if (n == 2) { // Исполняем блок кода 2 } else if (n == 3) { // Исполняем блок кода 3 } else { // Если все остальные условия else не выполняются, исполняем блок 4 } В этом фрагменте нет ничего особенного. Это просто последовательность инструк ций if, где каждая инструкция if является частью конструкции else предыдущей инструкции. Стиль else if предпочтительнее и понятнее записи в синтаксически эквивалентной форме, полностью показывающей вложенность инструкций: if (n == 1) { // Исполняем блок кода 1 } else { if (n == 2) { // Исполняем блок кода 2 } else { if (n == 3) { // Исполняем блок кода 3 } else { // Если все остальные условия else не выполняются, исполняем блок кода 4 } } } 6.5. Инструкция switch Инструкция if создает ветвление в потоке исполнения программы. Многопози ционное ветвление можно реализовать посредством нескольких инструкций if, как показано в предыдущем разделе. Однако это не всегда наилучшее решение, особенно если все ветви зависят от значения одной переменной. В этом случае расточительно повторно проверять значение одной и той же переменной в не скольких инструкциях if. Инструкция switch работает именно в такой ситуации и делает это более эффек тивно, чем повторяющиеся инструкции if. Инструкция switch в JavaScript очень похожа на инструкцию switch в Java или C. За инструкцией switch следует выражение и блок кода – почти так же, как в инструкции if: 104 Глава 6. Инструкции switch(выражение) { инструкции } Однако полный синтаксис инструкции switch более сложен, чем показано здесь. Различные места в блоке кода помечены ключевым словом case, за которым следу ет значение и символ двоеточия. Когда выполняется инструкция switch, она вы числяет значение выражения, а затем ищет метку case, соответствующую этому значению. Если метка найдена, исполняется блок кода, начиная с первой инст рукции, следующей за меткой case. Если метка case с соответствующим значением не найдена, исполнение начинается с первой инструкции, следующей за специаль ной меткой default:. Если метки default: нет, блок кода пропускается целиком. Работу инструкции switch сложно объяснить на словах, поэтому приведем при мер. Следующая инструкция switch эквивалентна повторяющимся инструкциям if/else, показанным в предыдущем разделе: switch(n) { case 1: // Выполняется, если n == 1 // Исполняем блок кода 1. break; // Здесь останавливаемся case 2: // Выполняется, если n == 2 // Исполняем блок кода 2. break; // Здесь останавливаемся case 3: // Выполняется, если n == 3 // Исполняем блок кода 3. break; // Здесь останавливаемся default: // Если все остальное не подходит... // Исполняем блок кода 4. break; // Здесь останавливаемся } Обратите внимание на ключевое слово break в конце каждого блока case. Инст рукция break, описываемая далее в этой главе, приводит к передаче управления в конец инструкции switch или цикла. Конструкции case в инструкции switch за дают только начальную точку исполняемого кода, но не задают никаких конеч ных точек. В случае отсутствия инструкций break инструкция switch начинает исполнение блока кода с метки case, соответствующей значению выражения, и продолжает исполнение до тех пор, пока не дойдет до конца блока. В редких случаях это полезно для написания кода, который переходит от одной метки case к следующей, но в 99 % случаев следует аккуратно завершать каждый блок case инструкцией break. (При использовании switch внутри функции можно помещать вместо break инструкцию return. Обе эти инструкции служат для завершения ра боты инструкции switch и предотвращения перехода к следующей метке case.) Ниже приводится более реальный пример использования инструкции switch; он преобразует значение в строку способом, зависящим от типа значения: function convert(x) { switch(typeof x) { case 'number': // Преобразуем число в шестнадцатеричное целое return x.toString(16); case 'string': // Возвращаем строку, заключенную в кавычки return '"' + x + '"'; case 'boolean': // Преобразуем в TRUE или FALSE, в верхнем регистре 6.6. Инструкция while 105 return x.toString().toUpperCase(); default: // Любой другой тип преобразуем обычным способом return x.toString() } } Обратите внимание: в двух предыдущих примерах за ключевыми словами case следовали числа или строковые литералы. Именно так инструкция switch чаще всего используется на практике, но стандарт ECMAScript v3 допускает указание после case произвольного выражения.1 Например: case 60*60*24: case Math.PI: case n+1: case a[0]: Инструкция switch сначала вычисляет выражение после ключевого слова switch, а затем выражения case в том порядке, в котором они указаны, пока не будет найдено совпадающее значение.2 Факт совпадения определяется в соответствии с оператором идентичности ===, а не оператором равенства ==, поэтому выраже ния должны совпадать без какоголибо преобразования типов. Обратите внимание: использование выражений case, имеющих побочные эффек ты, такие как вызовы функций и присваивания, не является хорошей практи кой программирования, т. к. при каждом исполнении инструкции switch вычис ляются не все выражения case. Когда побочные эффекты возникают лишь в не которых случаях, трудно понять и предсказать поведение программы. Безопас нее всего ограничиваться в выражениях case константными выражениями. Как объяснялось ранее, если ни одно из выражений case не соответствует выра жению switch, инструкция switch начинает выполнение с инструкции с меткой default:. Если метка default: отсутствует, инструкция switch полностью пропус кается. Обратите внимание, что в предыдущих примерах метка default: указана в конце тела инструкции switch после всех меток case. Это логичное и обычное место для нее, но на самом деле она может располагаться в любом месте внутри инструкции switch. 6.6. Инструкция while Так же как инструкция if является базовой управляющей инструкцией, позво ляющей интерпретатору JavaScript принимать решения, инструкция while – это 1 Это существенное отличие инструкции switch в JavaScript от инструкции switch в C, C++ и Java. В этих языках выражения case должны быть константами, вы числяемыми на этапе компиляции, иметь тип integer или другой перечислимый тип, причем один и тот же тип для всех констант. 2 Это значит, что инструкция switch в JavaScript менее эффективна, чем в C, C+ и Java. Выражения case в этих языках представляют собой константы, вычисляе мые на этапе компиляции, а не во время выполнения, как в JavaScript. Кроме то го, поскольку выражения case являются в C, C++ и Java перечислимыми, ин струкция switch часто может быть реализована с использованием высокоэффек тивной таблицы переходов. 106 Глава 6. Инструкции базовая инструкция, позволяющая JavaScript выполнять повторяющиеся дей ствия. Она имеет следующий синтаксис: while (выражение) инструкция Инструкция while начинает работу с вычисления выражения. Если оно равно false, интерпретатор JavaScript переходит к следующей инструкции програм мы, а если true, то исполняется инструкция, образующая тело цикла, и выраже ние вычисляется снова. И опять, если значение равно false, интерпретатор Java Script переходит к следующей инструкции программы, в противном случае он исполняет инструкцию снова. Цикл продолжается, пока выражение не станет равно false, тогда инструкция while завершит работу и JavaScript пойдет даль ше. С помощью синтаксиса while(true) можно записать бесконечный цикл. Обычно не требуется, чтобы интерпретатор JavaScript снова и снова выполнял одну и ту же операцию. Почти в каждом цикле с каждой итерацией цикла одна или несколько переменных изменяют свои значения. Поскольку переменная ме няется, действия, которые выполняет инструкция, при каждом проходе тела цик ла могут отличаться. Кроме того, если изменяемая переменная (или перемен ные) присутствует в выражении, значение выражения может меняться при каж дом проходе цикла. Это важно, т. к. в противном случае выражение, значение которого было равно true, никогда не изменится и цикл никогда не завершится! Пример цикла while: var count = 0; while (count < 10) { document.write(count + "
"); count++; } Как видите, в начале примера переменной count присваивается значение 0, а за тем ее значение увеличивается каждый раз, когда выполняется тело цикла. По сле того как цикл будет выполнен 10 раз, выражение становится равным false (т. е. переменная count уже не меньше 10), инструкция while завершается и Java Script может перейти к следующей инструкции программы. Большинство цик лов имеют переменныесчетчики, аналогичные count. Чаще всего в качестве счетчиков цикла выступают переменные с именами i, j и k, хотя для того чтобы сделать код более понятным, следует давать счетчикам более наглядные имена. 6.7. Цикл do/while Цикл do/while во многом похож на цикл while, за исключением того, что выраже ние цикла проверяется в конце, а не в начале цикла. Это значит, что тело цикла всегда исполняется хотя бы один раз. Синтаксис этого предложения таков: do инструкция while (выражение); Цикл do/while используется реже, чем родственный ему цикл while. Дело в том, что на практике ситуация, когда требуется хотя бы однократное исполнение цикла, несколько необычна. Например: 6.8. Инструкция for 107 function printArray(a) { if (a.length == 0) document.write("Пустой массив"); else { var i = 0; do { document.write(a[i] + "
"); } while (++i < a.length); } } Между циклом do/while и обычным циклом while есть два отличия. Вопервых, цикл do требует как ключевого слова do (для отметки начала цикла), так и клю чевого слова while (для отметки конца цикла и указания условия). Вовторых, в отличие от цикла while, цикл do завершается точкой с запятой. Причина в том, что цикл do завершается условием цикла, а не просто фигурной скобкой, отме чающей конец тела цикла. 6.8. Инструкция for Цикл, начинающийся с инструкции for, часто оказывается более удобным, чем while. Инструкция for использует шаблон, общий для большинства циклов (в том числе приведенного ранее примера цикла while). Большинство циклов имеют не которую переменнуюсчетчик. Эта переменная инициализируется перед нача лом цикла и проверяется в выражении, вычисляемом перед каждой итерацией цикла. И наконец, переменнаясчетчик инкрементируется или изменяется ка кимлибо другим образом в конце тела цикла, непосредственно перед повторным вычислением выражения. Инициализация, проверка и обновление – это три ключевых операции, выпол няемых с переменной цикла; инструкция for делает эти три шага явной частью синтаксиса цикла. Это особенно облегчает понимание действий, выполняемых циклом for, и предотвращает такие ошибки, как пропуск инициализации или инкрементирования переменной цикла. Синтаксис цикла for: for(инициализация; проверка; инкремент) инструкция Проще всего объяснить работу цикла for, показав эквивалентный ему цикл while:1 инициализация; while(проверка) { инструкция инкремент; } Другими словами, выражение инициализация вычисляется один раз перед нача лом цикла. Это выражение, как правило, является выражением с побочными эф фектами (обычно присваиванием), т. к. от него должна быть какаято польза. 1 Как мы увидим при рассмотрении инструкции continue, этот цикл while не являет ся точным эквивалентом цикла for. 108 Глава 6. Инструкции JavaScript также допускает, чтобы выражение инициализация было инструкцией объявления переменной var, поэтому можно одновременно объявить и проини циализировать счетчик цикла. Выражение проверка вычисляется перед каждой итерацией и определяет, будет ли выполняться тело цикла. Если результат про верки равен true, выполняется инструкция, являющаяся телом цикла. В конце цикла вычисляется выражение инкремент. И это выражение, чтобы приносить пользу, должно быть выражением с побочными эффектами. Обычно это либо вы ражение присваивания, либо выражение, использующее оператор ++ или . Пример цикла while из предыдущего раздела, выводящий числа от 0 до 9, может быть переписан в виде следующего цикла for: for(var count = 0; count < 10; count++) document.write(count + "
"); Обратите внимание, что этот синтаксис помещает всю важную информацию о пе ременной цикла в одну строку, делая работу цикла более понятной. Кроме того, помещение выражения инкремент в инструкцию for само по себе упрощает тело цикла до одной инструкции; нам даже не потребовалось ставить фигурные скоб ки для формирования блока инструкций. Конечно, циклы могут быть значительно более сложными, чем в этих простых примерах, и иногда на каждой итерации цикла изменяется несколько перемен ных. Эта ситуация – единственный случай в JavaScript, когда часто применяет ся оператор «запятая» – он позволяет объединить несколько выражений ини циализации и инкрементирования в одно выражение, подходящее для исполь зования в цикле for. Например: for(i = 0, j = 10; i < 10; i++, j) sum += i * j; 6.9. Инструкция for/in Ключевое слово for в JavaScript существует в двух ипостасях. Мы только что ви дели его в цикле for. Оно также используется в инструкции for/in. Эта инструк ция – несколько иной вид цикла, имеющего следующий синтаксис: for (переменная in объект) инструкция Здесь переменная должна быть либо именем переменной, либо инструкцией var, объявляющей переменную, либо элементом массива, либо свойством объекта (т. е. должна быть чемто, что может находиться левой части выражения при сваивания). Параметр объект – это имя объекта или выражение, результатом ко торого является объект. И как обычно, инструкция – это инструкция или блок ин струкций, образующих тело цикла. Элементы массива можно перебирать простым увеличением индексной перемен ной при каждом исполнении тела цикла while или for. Инструкция for/in пре доставляет средство перебора всех свойств объекта. Тело цикла for/in исполня ется единожды для каждого свойства объекта. Перед исполнением тела цикла имя одного из свойств объекта присваивается переменной в виде строки. В теле цикла эту переменную можно использовать для получения значения свойства 6.10. Метки 109 объекта с помощью оператора []. Например, следующий цикл for/in печатает имена и значения всех свойств объекта: for (var prop in my_object) { document.write("имя: " + prop + "; значение: " + my_object[prop], "
"); } Обратите внимание: переменной в цикле for/in может быть любое выражение, если только результатом его является нечто, подходящее для левой части при сваивания. Это выражение вычисляется при каждом вызове тела цикла, т. е. ка ждый раз оно может быть разным. Так, скопировать имена всех свойств объекта в массив можно следующим образом: var o = {x:1, y:2, z:3}; var a = new Array(); var i = 0; for(a[i++] in o) /* пустое тело цикла */; Массивы в JavaScript – это просто специальный тип объектов. Следовательно, цикл for/in может использоваться для перебора элементов массива так же, как свойств объекта. Например, предыдущий блок кода при замене строки на приве денную ниже перечисляет «свойства» 0, 1 и 2 массива: for(i in a) alert(i); Цикл for/in не задает порядка, в котором свойства объекта присваиваются пере менной. Нельзя заранее узнать, каким будет этот порядок, и в различных реали зациях и версиях JavaScript поведение может быть разным. Если тело цикла for/in удалит свойство, которое еще не было перечислено, это свойство перечис лено не будет. Если тело цикла определяет новые свойства, то будут или нет пе речислены эти свойства, зависит от реализации. Цикл for/in на самом деле не перебирает все свойства всех объектов. Так же как некоторые свойства объектов помечаются как доступные только для чтения или постоянные (не удаляемые), свойства могут помечаться как неперечислимые. Такие свойства не перечисляются циклом for/in. Если все свойства, определен ные пользователем, перечисляются, то многие встроенные свойства, включая все встроенные методы, не перечисляются. Как мы увидим в главе 7, объекты могут наследовать свойства от других объектов. Унаследованные свойства, кото рые определены пользователем, также перечисляются циклом for/in. 6.10. Метки Метки case и default: в сочетании с инструкцией switch – это особый вариант бо лее общего случая. Любая инструкция может быть помечена указанным перед ней именем идентификатора и двоеточием: идентификатор: инструкция Здесь идентификатор может быть любым допустимым в JavaScript идентификато ром, не являющимся зарезервированным словом. Имена меток отделены от имен переменных и функций, поэтому программист не должен беспокоиться о кон фликте имен, если имя метки совпадает с именем переменной или функции. Пример инструкции while с меткой: 110 Глава 6. Инструкции parser: while(token != null) { // здесь код опущен } Пометив инструкцию, мы даем ей имя, по которому на нее можно ссылаться из любого места программы. Пометить можно любую инструкцию, хотя обычно по мечаются только циклы while, do/while, for и for/in. Дав циклу имя, можно по средством инструкций break и continue выходить из цикла или из отдельной ите рации цикла. 6.11. Инструкция break Инструкция break приводит к немедленному выходу из самого внутреннего цик ла или инструкции switch. Синтаксис ее прост: break; Инструкция break приводит к выходу из цикла или инструкции switch, поэтому такая форма break допустима только внутри этих инструкций. JavaScript допускает указание имени метки за ключевым словом break: break: имя_метки; Обратите внимание: имя_метки – это просто идентификатор; за ним не указывает ся двоеточие, как в случае определения метки инструкции. Когда break используется с меткой, происходит переход в конец именованной инструкции или прекращение ее выполнения; именованной инструкцией может быть любая инструкция, внешняя по отношению к break. Именованная инструк ция не обязана быть циклом или инструкцией switch; инструкция break, исполь зованная с меткой, даже не обязана находиться внутри цикла или инструкции switch. Единственное ограничение на метку, указанную в инструкции break, – она должна быть именем внешней по отношению к break инструкции. Метка мо жет быть, например, именем инструкции if или даже блока инструкций, заклю ченных в фигурные скобки только для присвоения метки этому блоку. Как обсуждалось в главе 2, между ключевым словом break и именем метки пере вод строки не допускается. Дело в том, что интерпретатор JavaScript автомати чески вставляет пропущенные точки с запятой. Если разбить строку кода между ключевым словом break и следующей за ним меткой, интерпретатор предполо жит, что имелась в виду простая форма этой инструкции без метки, и добавит точку с запятой. Ранее уже демонстрировались примеры инструкции break, помещенной в инст рукцию switch. В циклах она обычно используется для преждевременного выхо да в тех случаях, когда по какойлибо причине отпала необходимость доводить выполнение цикла до конца. Когда в цикле имеются сложные условия выхода, часто проще реализовать некоторые из этих условий с помощью инструкции break, а не пытаться включить их все в одно выражение цикла. Следующий фрагмент выполняет поиск определенного значения среди элемен тов массива. Цикл прерывается естественным образом, когда доходит до конца 6.12. Инструкция continue 111 массива; если искомое значение найдено, он прерывается с помощью инструк ции break: for(i = 0; i < a.length; i++) { if (a[i] == target) break; } Форма инструкции break с меткой требуется только во вложенных циклах или в инструкции switch при необходимости выйти из инструкции, не являющейся самой внутренней. Следующий пример показывает помеченные циклы for и инструкции break с метками. Проверьте, удастся ли вам понять, каким будет результат работы это го фрагмента: outerloop: for(var i = 0; i < 10; i++) { innerloop: for(var j = 0; j < 10; j++) { if (j > 3) break; // Выход из самого внутреннего цикла if (i == 2) break innerloop; // То же самое if (i == 4) break outerloop; // Выход из внешнего цикла document.write("i = " + i + " j = " + j + "
"); } } document.write("FINAL i = " + i + " j = " + j + "
"); 6.12. Инструкция continue Инструкция continue схожа с инструкцией break. Однако вместо выхода из цикла continue запускает новую итерацию цикла. Синтаксис инструкции continue столь же прост, как и у инструкции break: continue; Инструкция continue может также использоваться с меткой: continue имя_метки; Инструкция continue как в форме без метки, так и с меткой может использовать ся только в теле циклов while, do/while, for и for/in. Использование ее в любых других местах приводит к синтаксической ошибке. Когда выполняется инструкция continue, текущая итерация цикла прерывается и начинается следующая. Для разных типов циклов это означает разное: • В цикле while указанное в начале цикла выражение проверяется снова, и если оно равно true, тело цикла выполняется сначала. • В цикле do/while исполнение переходит в конец цикла, где перед повторным исполнением цикла снова проверяется условие. • В цикле for вычисляется выражение инкремента и снова проверяется выра жение проверки, чтобы определить, следует ли выполнять следующую итера цию. 112 Глава 6. Инструкции • В цикле for/in цикл начинается заново с присвоением указанной переменной имени следующего свойства. Обратите внимание на различия в поведении инструкции continue в циклах while и for – цикл while возвращается непосредственно к своему условию, а цикл for сначала вычисляет выражение инкремента, а затем возвращается к условию. Ранее при обсуждении цикла for я объяснял поведение цикла for в терминах эк вивалентного цикла while. Поскольку инструкция continue ведет себя в этих двух циклах поразному, точно имитировать цикл for с помощью цикла while невоз можно. В следующем примере показано использование инструкции continue без метки для выхода из текущей итерации цикла в случае ошибки: for(i = 0; i < data.length; i++) { if (data[i] == null) continue; // Продолжение с неопределенными данными невозможно total += data[i]; } Инструкция continue, как и break, может применяться во вложенных циклах в форме, включающей метку, и тогда заново запускаемый цикл – это не обяза тельно цикл, непосредственно содержащий инструкцию continue. Кроме того, как и для инструкции break, переводы строк между ключевым словом continue и именем метки не допускаются. 6.13. Инструкция var Инструкция var позволяет явно объявить одну или несколько переменных. Ин струкция имеет следующий синтаксис: var имя_1 [ = значение_1] [ ,..., имя_n [= значение_n]] За ключевым словом var следует список объявляемых переменных через запя тую; каждая переменная в списке может иметь специальное выражениеини циализатор, указывающее ее начальное значение. Например: var i; var j = 0; var p, q; var greeting = "hello" + name; var x = 2.34, y = Math.cos(0.75), r, theta; Инструкция var определяет каждую из перечисленных переменных путем созда ния свойства с этим именем в объекте вызова функции, в которой она находится, или в глобальном объекте, если объявление находится не в теле функции. Свой ство или свойства, создаваемые с помощью инструкции var, не могут быть удале ны оператором delete. Обратите внимание: помещение инструкции var внутрь инструкции with (см. раздел 6.18) не изменяет ее поведения. Если в инструкции var начальное значение переменной не указано, то перемен ная определяется, однако ее начальное значение остается неопределенным (unde fined). 6.14. Инструкция function 113 Кроме того, инструкция var может являться частью циклов for и for/in. Напри мер: for(var i = 0; i < 10; i++) document.write(i, "
"); for(var i = 0, j=10; i < 10; i++,j) document.write(i*j, "
"); for(var i in o) document.write(i, "
"); Значительно больше информации о переменных и их объявлении в JavaScript содержится в главе 4. 6.14. Инструкция function Инструкция function в JavaScript определяет функцию. Она имеет следующий синтаксис: function имя_функции([арг1 [,арг2 [..., аргn]]]) { инструкции } Здесь имя_функции – это имя определяемой функции. Оно должно быть идентифи катором, а не строкой или выражением. За именем функции следует заключен ный в скобки список имен аргументов, разделенных запятыми. Эти идентифи каторы могут использоваться в теле функции для ссылки на значения аргумен тов, переданных при вызове функции. Тело функции состоит из произвольного числа JavaScriptинструкций, заклю ченных в фигурные скобки. Эти инструкции не исполняются при определении функции. Они компилируются и связываются с новым объектом функции для исполнения при ее вызове с помощью оператора вызова (). Обратите внимание, что фигурные скобки – это обязательная часть инструкции function. В отличие от блоков инструкций в циклах while и других конструкциях, тело функции тре бует фигурных скобок, даже если оно состоит только из одной инструкции. Определение функции создает новый объект функции и сохраняет объект в толь ко что созданном свойстве с именем имя_функции. Вот несколько примеров опреде лений функций: function welcome() { alert("Добро пожаловать на мою домашнюю страницу!"); } function print(msg) { document.write(msg, "
"); } function hypotenuse(x, y) { return Math.sqrt(x*x + y*y); // Инструкция return описана далее } function factorial(n) { // Рекурсивная функция if (n <= 1) return 1; return n * factorial(n  1); } Определения функций обычно находятся в JavaScriptкоде верхнего уровня. Они также могут быть вложенными в определения других функций, но только на «верхнем уровне», т. е. определения функции не могут находиться внутри инструкций if, циклов while или любых других конструкций. 114 Глава 6. Инструкции Формально function не является инструкцией. Инструкции приводят к некото рым динамическим действиям в JavaScriptпрограмме, а определения функций описывают статическую структуру программы. Инструкции исполняются во время исполнения программы, а функции определяются во время анализа или компиляции JavaScriptкода, т. е. до их фактического исполнения. Когда син таксический анализатор JavaScript встречает определение функции, он анали зирует и сохраняет (без исполнения) составляющие тело функции инструкции. Затем он определяет свойство (в объекте вызова, если определение функции вло жено в другую функцию; в противном случае – в глобальном объекте) с именем, которое было указано в определении функции. Тот факт, что функции определяются на этапе синтаксического анализа, а не во время исполнения, приводит к некоторым интересным эффектам. Рассмотрим следующий фрагмент: alert(f(4)); // Показывает 16. Функция f() может быть вызвана до того, // как она определена. var f = 0; // Эта инструкция переписывает содержимое свойства f. function f(x) { // Эта "инструкция" определяет функцию f до того, return x*x; // как будут выполнены приведенные ранее строки. } alert(f); // Показывает 0. Функция f() перекрыта переменной f. Такие необычные результаты возникают изза того, что функция определяется не в то время, в которое определяется переменная. К счастью, подобные ситуа ции возникают не очень часто. В главе 8 мы узнаем о функциях больше. 6.15. Инструкция return Как вы помните, вызов функции с помощью оператора () представляет собой вы ражение. Все выражения имеют значения, и инструкция return служит для опре деления значения, возвращаемого функцией. Это значение становится значени ем выражения вызова функции. Инструкция return имеет следующий синтаксис: return выражение; Инструкция return может располагаться только в теле функции. Присутствие ее в любом другом месте является синтаксической ошибкой. Когда выполняется инструкция return, вычисляется выражение и его значение возвращается в каче стве значения функции. Инструкция return прекращает исполнение функции, даже если в теле функции остались другие инструкции. Инструкция return ис пользуется для возвращения значения следующим образом: function square(x) { return x*x; } Инструкция return может также использоваться без выражения, тогда она про сто прерывает исполнение функции, не возвращая значение. Например: function display_object(obj) { // Сначала убедимся в корректности нашего аргумента // В случае некорректности пропускаем остаток функции if (obj == null) return; 6.16. Инструкция throw 115 // Здесь находится оставшаяся часть функции... } Если в функции выполняется инструкция return без выражения или если выпол нение функции прекращается по причине достижения конца тела функции, зна чение выражения вызова функции оказывается неопределенным (undefined). JavaScript вставляет точку с запятой автоматически, поэтому нельзя разделять переводом строки инструкцию return и следующее за ней выражение. 6.16. Инструкция throw Исключение – это сигнал, указывающий на возникновение какойлибо исклю чительной ситуации или ошибки. Генерация исключения (throw) – это способ просигнализировать о такой ошибке или исключительной ситуации. Перехва' тить исключение (catch), значит, обработать его, т. е. предпринять действия, необходимые или подходящие для восстановления после исключения. В Java Script исключения генерируются в тех случаях, когда возникает ошибка време ни выполнения, тогда программа явно генерирует его с помощью инструкции throw. Исключения перехватываются с помощью инструкции try/catch/finally, которая описана в следующем разделе.1 Инструкция throw имеет следующий синтаксис: throw выражение; Результатом выражения может быть значение любого типа. Однако обычно это объект Error или экземпляр одного из подклассов Error. Также бывает удобно ис пользовать в качестве выражения строку, содержащую сообщение об ошибке или числовое значение, обозначающее некоторый код ошибки. Вот пример кода, в котором инструкция throw применяется для генерации исключения: function factorial(x) { // Если входной аргумент не является допустимым, // генерируем исключение! if (x < 0) throw new Error("x не может быть отрицательным"); // В противном случае вычисляем значение и нормальным образом выходим из функции for(var f = 1; x > 1; f *= x, x) /* пустое тело цикла */ ; return f; } Когда генерируется исключение, интерпретатор JavaScript немедленно преры вает нормальное исполнение программы и переходит к ближайшему2 обработчи ку исключений. В обработчиках исключений используется конструкция catch инструкции try/catch/finally, описание которой приведено в следующем разде ле. Если блок кода, в котором возникло исключение, не имеет соответствующей конструкции catch, интерпретатор анализирует следующий внешний блок кода 1 Инструкции throw и try/catch/finally в JavaScript напоминают соответствующие инструкции в C++ и Java. 2 К самому внутреннему по вложенности охватывающему обработчику исключе ний. – Примеч. науч. ред. 116 Глава 6. Инструкции и проверяет, связан ли с ним обработчик исключений. Это продолжается до тех пор, пока обработчик не будет найден. Если исключение генерируется в функ ции, не содержащей инструкции try/catch/finally, предназначенной для его об работки, то исключение распространяется на код, вызвавший функцию. Так ис ключения распространяются по лексической структуре методов JavaScript вверх по стеку вызовов. Если обработчик исключения так и не будет найден, ис ключение рассматривается как ошибка и о ней сообщается пользователю. Инструкция throw стандартизована в ECMAScript v3 и реализована в Java Script 1.4. Класс Error и его подклассы также являются частью стандарта ECMA Script v3, но они не были реализованы до JavaScript 1.5. 6.17. Инструкция try/catch/finally Инструкция try/catch/finally реализует механизм обработки исключений в Java Script. Конструкция try в этой инструкции просто определяет блок кода, в кото ром обрабатываются исключения. За блоком try следует конструкция catch с блоком инструкций, вызываемых, когда гделибо в блоке try возникает исклю чение. За конструкцией catch следует блок finally, содержащий код зачистки, который гарантированно выполняется независимо от того, что происходит в бло ке try. И блок catch, и блок finally не являются обязательными, однако после блока try должен обязательно присутствовать хотя бы один из них. Блоки try, catch и finally начинаются и заканчиваются фигурными скобками. Это обяза тельная часть синтаксиса и она не может быть опущена, даже если между ними содержится только одна инструкция. Как и инструкция throw, инструкция try/ catch/finally стандартизована в ECMAScript v3 и реализована в JavaScript 1.4. Следующий фрагмент иллюстрирует синтаксис и суть инструкции try/catch/fi nally. Обратите внимание, в частности, на то, что за ключевым словом catch сле дует идентификатор в скобках. Этот идентификатор похож на аргумент функ ции. Он присваивает имя локальной переменной, существующей только в теле блока catch. JavaScript присваивает этой переменной объект исключения или значение, указанное при генерации исключения: try { // Обычно этот код без сбоев работает от начала до конца. // Но в какойто момент в нем может генерироваться исключение // либо непосредственно с помощью инструкции throw, либо косвенно // вызовом метода, генерирующего исключение. } catch (e) { // Инструкции в этом блоке выполняются тогда и только тогда, когда // в блоке try генерируется исключение. Эти инструкции могут // использовать локальную переменную e, ссылающуюся на объект Error // или на другое значение, указанное в инструкции throw. Этот блок может // либо какимлибо образом обработать исключение, либо проигнорировать // его, делая чтото другое, либо заново сгенерировать исключение // с помощью инструкции throw. } finally { // Этот блок содержит инструкции, которые выполняются всегда, независимо от того, // что произошло в блоке try. Они выполняются, если блок try прерван: 6.17. Инструкция try/catch/finally 117 // 1) нормальным образом, достигнув конца блока // 2) изза инструкции break, continue или return // 3) с исключением, обработанным приведенным ранее блоком catch // 4) с неперехваченным исключением, которое продолжает свое // распространение на более высокие уровни } Далее приводится более реалистичный пример инструкции try/catch. В нем вы зываются метод factorial(), определенный в предыдущем разделе, и методы prompt() и alert() клиентского языка JavaScript для организации ввода и вывода: try { // Просим пользователя ввести число var n = prompt("Введите положительное число", ""); // Вычисляем факториал числа, предполагая, что входные данные корректны var f = factorial(n); // Показываем результат alert(n + "! = " + f); } catch (ex) { // Если введенные данные некорректны, мы попадем сюда // Сообщаем пользователю об ошибке alert(ex); } Это пример инструкции try/catch без конструкции finally. Хотя finally исполь зуется не так часто, как catch, тем не менее иногда эта конструкция оказывается полезной. Однако ее поведение требует дополнительных объяснений. Блок fi nally гарантированно исполняется, если исполнялась хотя бы какаято часть блока try, независимо от того каким образом завершился код в блоке try. Эта возможность обычно используется для зачистки после выполнения кода в пред ложении try. В обычной ситуации управление доходит до конца блока try, а затем переходит к блоку finally, который выполняет всю необходимую зачистку. Если управле ние вышло из блока try изза инструкций return, continue или break, перед пере дачей управления в другое место кода исполняется блок finally. Если в блоке try возникает исключение и имеется соответствующий блок catch для его обработки, управление сначала передается в блок catch, а затем – в блок finally. Если отсутствует локальный блок catch, то управление сначала переда ется в блок finally, а затем переходит на ближайший внешний блок catch, кото рый может обработать исключение. Если сам блок finally передает управление с помощью инструкции return, con tinue, break или throw или путем вызова метода, генерирующего исключение, не законченная команда на передачу управления отменяется и выполняется новая. Например, если блок finally генерирует исключение, это исключение заменяет любое сгенерированное исключение. Если в блоке finally имеется инструкция return, происходит нормальный выход из метода, даже если генерировалось ис ключение, которое не было обработано. Инструкции try и finally могут использоваться вместе без конструкции catch. В этом случае блок finally – это просто код зачистки, который будет гарантиро ванно исполнен независимо от наличия в блоке try инструкции break, continue 118 Глава 6. Инструкции или return. Например, в следующем коде используется инструкция try/finally, гарантирующая, что счетчик цикла будет инкрементирован в конце каждой ите рации, даже если итерация внезапно прервется инструкцией continue: var i = 0, total = 0; while(i < a.length) { try { if ((typeof a[i] != "number") || isNaN(a[i])) // Если это не число, continue; // переходим к следующей итерации цикла. total += a[i]; // В противном случае добавляем число к общей сумме. } finally { i++; // Всегда увеличиваем i, даже если ранее была инструкция continue. } } 6.18. Инструкция with В главе 4 мы обсуждали область видимости переменных и цепочку областей ви димости – список объектов, в которых выполняется поиск при разрешении име ни переменной. Инструкция with используется для временного изменения це почки областей видимости. Она имеет следующий синтаксис: with (объект) инструкция Эта инструкция добавляет объект в начало цепочки областей видимости, испол няет инструкцию, а затем восстанавливает цепочку в ее первоначальном состоянии. На практике инструкция with помогает значительно сократить объем набирае мого текста. В клиентском языке JavaScript часто работают с глубоко вложен ными иерархиями объектов. Например, для доступа к элементам HTMLформы вам, возможно, придется пользоваться такими выражениями: frames[1].document.forms[0].address.value Если надо обратиться к этой форме несколько раз, можно воспользоваться инст рукцией with для добавления формы в цепочку областей видимости: with(frames[1].document.forms[0]) { // Здесь обращаемся к элементам формы непосредственно, например: name.value = ""; address.value = ""; email.value = ""; } Это сокращает объем текста программы – больше не надо указывать фрагмент frames[1].document.forms[0] перед каждым именем свойства. Этот объект пред ставляет собой временную часть цепочки областей видимости и автоматически участвует в поиске, когда JavaScript требуется разрешить такой идентифика тор, как address. Несмотря на удобство этой конструкции в некоторых случаях, ее использование не приветствуется. JavaScriptкод с инструкцией with сложен в оптимизации и поэтому может работать медленнее, чем эквивалентный код, написанный без 6.19. Пустая инструкция 119 нее. Кроме того, определения функций и инициализация переменных в теле ин струкции with могут приводить к странным и трудным для понимания результа там.1 По этим причинам использовать инструкцию with не рекомендуется. К тому же существуют и другие абсолютно законные способы уменьшения объема набираемого текста. Так, предыдущий пример можно переписать следующим образом: var form = frames[1].document.forms[0]; form.name.value = ""; form.address.value = ""; form.email.value = ""; 6.19. Пустая инструкция И наконец, последняя из допустимых в JavaScript инструкций – пустая инст рукция. Она выглядит следующим образом: ; Выполнение пустой инструкции, очевидно, не имеет никакого эффекта и не про изводит никаких действий. Можно подумать, что особых причин для ее приме нения нет, однако изредка пустая инструкция может быть полезна, когда требу ется создать цикл, имеющий пустое тело. Например: // Инициализация массива a for(i=0; i < a.length; a[i++] = 0); Обратите внимание, что случайное указание точки с запятой после правой круг лой скобки в циклах for и while или в инструкции if может привести к неприят ным ошибкам, которые сложно обнаружить. Например, следующий фрагмент вряд ли делает то, что предполагал его автор: if ((a == 0) || (b == 0)); // Ой! Эта строка ничего не делает... o = null; // а эта строка выполняется всегда. Когда пустая инструкция применяется специально, код желательно снабжать исчерпывающими комментариями. Например: for(i=0; i < a.length; a[i++] = 0) /* Пустое тело цикла */ ; 6.20. Итоговая таблица JavaScriptинструкций В этой главе мы представили все инструкции языка JavaScript. В табл. 6.1 со держится перечень этих инструкций с указанием синтаксиса и назначения каж дой из них. 1 Эти результаты и их причины слишком сложны, чтобы объяснять их здесь. 120 Глава 6. Инструкции Таблица 6.1. Синтаксис JavaScript'инструкций Инструкция Синтаксис Назначение break break; break имя_метки; Выход из самого внутрен него цикла инструкции switch или инструкции с име нем имя_метки case case выражение: Метка для инструкции внут ри конструкции switch continue continue; continue имя_метки; Перезапуск самого внутрен него цикла или цикла, поме ченного меткой имя_метки default default: Отметка инструкции по умолчанию внутри инструк ции switch do/while do инструкция while (выражение); Альтернатива циклу while Пустая ин струкция ; Ничего не делает for for (инициализация; проверка; инкремент) инструкция Простой в использовании цикл for/in for (переменная in объект) инструкция Цикл по свойствам объекта function function имя_функции([арг1[..., аргn]]) { инструкции } Объявление функции if/else if (выражение) инструкция1 [else инструкция2] Условное исполнение фраг мента программы Метка идентификатор: инструкция Присваивание инструкции имени идентификатор return return [выражение]; Возврат из функции или за дание возвращаемого функ цией значения, равным выра жению switch switch (выражение) { инструкции } Многопозиционное ветвле ние для инструкций, поме ченных метками case и de fault throw throw выражение; Генерация исключения 6.20. Итоговая таблица JavaScript*инструкций 121 try try { инструкции } catch (идентификатор) { инструкции } finally { инструкции } Перехват исключения var var имя_1 [ = значение_1] [ ..., имя_n [ = значение_n]]; Объявление и инициализа ция переменных while while (выражение) инструкция Базовая конструкция для цикла with with (объект) инструкция Расширение цепочки облас тей видимости (не рекомен дуется к применению) Инструкция Синтаксис Назначение Объекты и массивы В главе 3 говорилось, что объекты и массивы – это два фундаментальных и наибо лее важных типа данных в JavaScript. Объекты и массивы отличаются от элемен тарных типов данных, таких как строки или числа, тем, что они представляют не единственное значение, а целые их наборы. Объекты являются коллекциями име нованных свойств, а массивы представляют собой специализированные объекты, которые ведут себя как упорядоченные коллекции пронумерованных значений. В этой главе мы детально рассмотрим объекты и массивы языка JavaScript. 7.1. Создание объектов Объекты – это составной тип данных, они объединяют множество значений в еди ный модуль и позволяют сохранять и извлекать значения по их именам. Говоря другими словами, объекты – это неупорядоченные коллекции свойств, каждое из которых имеет свои имя и значение. Именованные значения, хранящиеся в объекте, могут быть данными элементарных типов, такими как числа или строки, или сами могут быть объектами. Самый простой способ создания объектов заключается во включении в програм му литерала объекта. Литерал объекта – это заключенный в фигурные скобки список свойств (пар «имя–значение»), разделенных запятыми. Имя каждого свойства может быть JavaScriptидентификатором или строкой, а значением любого свойства может быть константа или JavaScriptвыражение. Несколько примеров создания объектов: var empty = {}; // Объект без свойств var point = { x:0, y:0 }; var circle = { x:point.x, y:point.y+1, radius:2 }; var homer = { "name": "Homer Simpson", "age": 34, "married": true, "occupation": "plant operator", 7.2. Свойства объектов 123 'email': "homer@example.com" }; Литерал объекта – это выражение, которое создает и инициализирует новый объект всякий раз, когда производится вычисление этого выражения. Таким об разом, с помощью единственного литерала объекта можно создать множество новых объектов, если этот литерал поместить в тело цикла или функции, кото рая будет вызываться многократно. С помощью оператора new можно создать другую разновидность объектов. За этим оператором должно быть указано имя функцииконструктора, выполняю щей инициализацию свойств объекта. Например: var a = new Array( ); // Создать пустой массив var d = new Date( ); // Создать объект с текущими временем и датой var r = new RegExp("javascript", "i"); // Создать объект регулярного выражения Продемонстрированные здесь функции Array(), Date() и RegExp() являются встро енными конструкторами базового языка JavaScript. (Конструктор Array() опи сан далее в этой главе, описание других конструкторов можно найти в третьей части книги.) Конструктор Object() создает пустой объект, как если бы исполь зовался литерал {}. Существует возможность определять собственные конструкторы для инициали зации вновь создаваемых объектов необходимым вам способом. Как это делает ся, показано в главе 9. 7.2. Свойства объектов Обычно для доступа к значениям свойств объекта используется оператор . (точ ка). Значение в левой части оператора должно быть ссылкой на объект, к свойст вам которого требуется получить доступ. Обычно это просто имя переменной, со держащей ссылку на объект, но это может быть любое допустимое в JavaScript выражение, являющееся объектом. Значение в правой части оператора должно быть именем свойства. Это должен быть идентификатор, а не строка или выра жение. Так, обратиться к свойству p объекта o можно посредством выражения o.p, а к свойству radius объекта circle – посредством выражения circle.radius. Свойства объекта работают как переменные: в них можно сохранять значения и считывать их. Например: // Создаем объект. Сохраняем ссылку на него в переменной. var book = new Object(); // Устанавливаем свойство в объекте. book.title = "JavaScript: полное руководство" // Устанавливаем другие свойства. Обратите внимание на вложенные объекты. book.chapter1 = new Object(); book.chapter1.title = "Введение в JavaScript"; book.chapter1.pages = 11; book.chapter2 = { title: "Лексическая структура", pages: 6 }; // Читаем значения некоторых свойств из объекта. alert("Заголовок: " + book.title + "\n\t" + 124 Глава 7. Объекты и массивы "Глава 1 " + book.chapter1.title + "\n\t" + "Глава 2 " + book.chapter2.title); Важно обратить внимание на один момент в этом примере – новое свойство объ екта можно добавить, просто присвоив этому свойству значение. Если перемен ные должны объявляться с помощью ключевого слова var, то для свойств объек та такой необходимости (и возможности) нет. К тому же после создания свойства объекта (в результате присваивания) значение свойства можно будет изменить в любой момент простым присваиванием ему нового значения: book.title = "JavaScript: Книга с носорогом" 7.2.1. Перечисление свойств Цикл for/in, который обсуждался в главе 6, предоставляет средство, позволяющее перебрать, или перечислить, свойства объекта. Это обстоятельство можно исполь зовать при отладке сценариев или при работе с объектами, которые могут иметь произвольные свойства с заранее неизвестными именами. В следующем фрагмен те демонстрируется функция, которая выводит список имен свойств объекта: function DisplayPropertyNames(obj) { var names = ""; for(var name in obj) names += name + "\n"; alert(names); } Обратите внимание, что цикл for/in не перечисляет свойства в какомлибо за данном порядке, и хотя он перечисляет все свойства, определенные пользовате лем, некоторые предопределенные свойства и методы он не перечисляет. 7.2.2. Проверка существования свойств Для проверки факта наличия того или иного свойства у объекта может использо ваться оператор in (см. главу 5). С левой стороны от оператора помещается имя свойства в виде строки, с правой стороны – проверяемый объект. Например: // Если объект o имеет свойство с именем "x", установить его if ("x" in o) o.x = 1; Однако потребность в операторе in возникает не так часто, потому что при обра щении к несуществующему свойству возвращается значение undefined. Таким образом, указанный фрагмент обычно записывается следующим образом: // Если свойство x существует и его значение // не равно undefined, установить его. if (o.x !== undefined) o.x = 1; Обратите внимание: есть вероятность, что свойство фактически существует, но еще не определено. Например, если записать такую строку: o.x = undefined то свойство x будет существовать, но не будет иметь значения. В этом случае в первом из показанных фрагментов в свойство x будет записано значение 1, во втором – нет. 7.3. Объекты как ассоциативные массивы 125 Кроме того, обратите внимание, что вместо обычного оператора != был использо ван оператор !==. Операторы !== и === различают значения undefined и null, хотя иногда в этом нет необходимости: // Если свойство doSomething существует и не содержит значение null // или undefined, тогда предположить, что это функция и ее следует вызвать! if (o.doSomething) o.doSomething(); 7.2.3. Удаление свойств Для удаления свойства объекта предназначен оператор delete: delete book.chapter2; Обратите внимание, что при удалении свойства его значение не просто устанав ливается в значение undefined; оператор delete действительно удаляет свойство из объекта. Цикл for/in демонстрирует это отличие: он перечисляет свойства, кото рым было присвоено значение undefined, но не перечисляет удаленные свойства. 7.3. Объекты как ассоциативные массивы Как мы знаем, доступ к свойствам объекта осуществляется посредством операто ра «точка». Доступ к свойствам объекта возможен также при помощи оператора [], который обычно применяется при работе с массивами. Таким образом, сле дующие два JavaScriptвыражения имеют одинаковое значение: object.property object["property"] Важное различие между этими двумя синтаксисами, на которое следует обратить внимание, состоит в том, что в первом варианте имя свойства представляет собой идентификатор, а во втором – строку. Скоро мы узнаем, почему это так важно. В Java, C, C++ и подобных языках со строгой типизацией объект может иметь только фиксированное число свойств, и имена этих свойств должны быть опре делены заранее. Поскольку JavaScript – слабо типизированный язык, к нему данное правило неприменимо; программа может создавать любое количество свойств в любом объекте. Однако в случае использования оператора «точка» для доступа к свойству объекта имя свойства задается идентификатором. Идентифи каторы должны быть частью текста JavaScriptпрограммы – они не являются типом данных и ими нельзя манипулировать из программы. В то же время при обращении к свойству объекта с помощью нотации массивов [] имя свойства задается в виде строки. Строки в JavaScript – это тип данных, поэтому они могут создаваться и изменяться во время работы программы. И по этому в JavaScript можно, например, написать следующий код: var addr = ""; for(i = 0; i < 4; i++) { addr += customer["address" + i] + '\n'; } В этом фрагменте читаются и объединяются в одну строку свойства address0, add ress1, address2 и address3 объекта customer. 126 Глава 7. Объекты и массивы Этот короткий пример демонстрирует гибкость нотации массивов при обраще нии к свойствам объекта с помощью строковых выражений. Мы могли бы напи сать этот пример и с помощью оператора «точка», но есть ситуации, где подой дет только нотация массивов. Предположим, что вы пишете программу, обра щающуюся к сетевым ресурсам для вычисления текущей суммы инвестиций пользователя на фондовом рынке. Программа разрешает пользователю вводить названия любых имеющихся у него акций, а также количество каждого вида ак ций. Можно организовать хранение этой информации при помощи объекта с именем portfolio, имеющего по одному свойству для акций каждого вида. Имя свойства – это название акции, а значение свойства – количество акций данного вида. Другими словами, если, например, у пользователя имеется 50 акций IBM, то свойство portfolio.ibm имеет значение 50. Частью этой программы должен быть цикл, запрашивающий у пользователя на звание имеющихся у него акций, а затем количество акций данного типа. Внут ри цикла должен быть код, похожий на следующий: var stock_name = get_stock_name_from_user(); var shares = get_number_of_shares(); portfolio[stock_name] = shares; Поскольку пользователь вводит названия акций во время исполнения програм мы, нет способа узнать имена свойств заранее. А раз имена свойств при написа нии программы неизвестны, то доступ к свойствам объекта portfolio при помо щи оператора «точка» невозможен. Однако можно обратиться к оператору [], т. к. в нем для имени свойства используется строковое значение (которое может изменяться во время выполнения), а не идентификатор (который должен быть задан непосредственно в тексте программы). Когда объект используется в такой форме, его часто называют ассоциативным массивом – структурой данных, позволяющей связывать произвольные значе ния с произвольными строками. Нередко для описания этой ситуации использу ется термин отображение (map): JavaScriptобъекты отображают строки (имена свойств) на их значения. Использование точки (.) для доступа к свойствам делает их похожими на стати ческие объекты в языках C++ и Java, и они прекрасно работают в этой роли. Но они также предоставляют мощное средство для связи значений с произвольны ми строками. В этом отношении JavaScriptобъекты значительно больше похо жи на массивы в Perl, чем на объекты в C++ или Java. В главе 6 был введен цикл for/in. Настоящая мощь этой JavaScriptконструкции становится понятной при ее использовании с ассоциативными массивами. Воз вращаясь к примеру с портфелем акций, после ввода пользователем данных по своему портфелю вычислить текущую общую стоимость последнего можно при помощи следующего кода: var value = 0; for (stock in portfolio) { // Для каждого вида акций в портфеле получаем стоимость // одной акции и умножаем ее на число акций. value += get_share_value(stock) * portfolio[stock]; } 7.4. Свойства и методы универсального класса Object 127 Здесь не обойтись без цикла for/in, поскольку названия акций заранее неизвест ны. Это единственный способ извлечения имен этих свойств из ассоциативного массива (JavaScriptобъекта) по имени portfolio. 7.4. Свойства и методы универсального класса Object Как уже отмечалось, все объекты в JavaScript наследуют свойства и методы класса Object. При этом специализированные классы объектов, как, например, те, что создаются с помощью конструкторов Date() или RegExp(), определяют соб ственные свойства и методы, но все объекты независимо от своего происхожде ния помимо всего прочего поддерживают свойства и методы, определенные классом Object. По причине их универсальности эти свойства и методы представ ляют особый интерес. 7.4.1. Свойство constructor В JavaScript любой объект имеет свойство constructor, которое ссылается на функ циюконструктор, используемую для инициализации объекта. Например, если объект d создается с помощью конструктора Date(), то свойство d.constructor ссы лается на функцию Date: var d = new Date( ); d.constructor == Date; // Равно true Функцияконструктор определяет категорию, или класс, объекта, поэтому свой ство constructor может использоваться для определения типа любого заданного объекта. Например, тип неизвестного объекта можно выяснить таким способом: if ((typeof o == "object") && (o.constructor == Date)) // Какието действия с объектом Date... Проверить значение свойства constructor можно с помощью оператора instan ceof, т. е. приведенный фрагмент можно записать несколько иначе: if ((typeof o == "object") && (o instanceof Date)) // Какието действия с объектом Date... 7.4.2. Метод toString() Метод toString() не требует аргументов; он возвращает строку, какимлибо обра зом представляющую тип и/или значение объекта, для которого он вызывается. Интерпретатор JavaScript вызывает этот метод объекта во всех тех случаях, ко гда ему требуется преобразовать объект в строку. Например, это происходит, ко гда используется оператор + для конкатенации строки с объектом, или при пере даче объекта такому методу, как alert() или document.write(). Метод toString() по умолчанию не очень информативен. Например, следующий фрагмент просто записывает в переменную s строку "[object Object]": var s = { x:1, y:1 }.toString( ); Этот метод по умолчанию не отображает особенно полезной информации, поэто му многие классы определяют собственные версии метода toString(). Например, 128 Глава 7. Объекты и массивы когда массив преобразуется в строку, мы получаем список элементов массива, каждый из которых преобразуется в строку, а когда в строку преобразуется функция, мы получаем исходный код этой функции. В главе 9 описывается, как можно переопределить метод toString() для своих собственных типов объектов. 7.4.3. Метод toLocaleString() В ECMAScript v3 и JavaScript 1.5 класс Object в дополнение к методу toString() определяет метод toLocaleString(). Назначение последнего состоит в получении локализованного строкового представления объекта. По умолчанию метод toLo caleString(), определяемый классом Object, никакой локализации не выполняет; он всегда возвращает в точности такую же строку, что и toString(). Однако под классы могут определять собственные версии метода toLocaleString(). В ECMA Script v3 классы Array, Date и Number определяют версии метода toLocaleString(), возвращающие локализованные значения. 7.4.4. Метод valueOf() Метод valueOf() во многом похож на метод toString(), но вызывается, когда ин терпретатору JavaScript требуется преобразовать объект в значение какоголибо элементарного типа, отличного от строки, – обычно в число. Интерпретатор Java Script вызывает этот метод автоматически, если объект используется в контексте значения элементарного типа. По умолчанию метод valueOf() не выполняет ниче го, что представляло бы интерес, но некоторые встроенные категории объектов переопределяют метод valueOf() (например, Date.valueOf()). В главе 9 описывает ся, как можно переопределить метод valueOf() в собственных типах объектов. 7.4.5. Метод hasOwnProperty() Метод hasOwnProperty() возвращает true, если для объекта определено не унасле дованное свойство с именем, указанным в единственном строковом аргументе метода. В противном случае он возвращает false. Например: var o = {}; o.hasOwnProperty("undef"); // false: свойство не определено o.hasOwnProperty("toString"); // false: toString – это унаследованное свойство Math.hasOwnProperty("cos"); // true: объект Math имеет свойство cos Порядок наследования свойств описывается в главе 9. Метод hasOwnProperty() определяется стандартом ECMAScript v3 и реализован в JavaScript 1.5 и более поздних версиях. 7.4.6. Метод propertyIsEnumerable() Метод propertyIsEnumerable() возвращает true, если в объекте определено свойст во с именем, указанным в единственном строковом аргументе метода, и это свой ство может быть перечислено циклом for/in. В противном случае метод возвра щает false. Например: var o = { x:1 }; 7.5. Массивы 129 o.propertyIsEnumerable("x"); // true: свойство существует и является перечислимым o.propertyIsEnumerable("y"); // false: свойство не существует o.propertyIsEnumerable("valueOf"); // false: свойство неперечислимое Метод propertyIsEnumerable() определяется стандартом ECMAScript v3 и реализо ван в JavaScript 1.5 и более поздних версиях. Обратите внимание: все свойства объекта, определяемые пользователем, являют ся перечислимыми. Неперечислимыми обычно являются унаследованные свой ства (тема наследования свойств рассматривается в главе 9), поэтому практиче ски всегда этот метод возвращает то же значение, что и метод hasOwnProperty(). 7.4.7. Метод isPrototypeOf() Метод isPrototypeOf() возвращает true, если объект, которому принадлежит ме тод, является прототипом объекта, передаваемого методу в качестве аргумента. В противном случае метод возвращает false. Например: var o = {}; Object.prototype.isPrototypeOf(o); // true: o.constructor == Object Object.isPrototypeOf(o); // false o.isPrototypeOf(Object.prototype); // false Function.prototype.isPrototypeOf(Object); // true: Object.constructor == Function 7.5. Массивы Массив – это тип данных, содержащий (хранящий) пронумерованные значения. Каждое пронумерованное значение называется элементом массива, а число, с ко торым связывается элемент, называется его индексом. Так как JavaScript – это нетипизированный язык, элемент массива может иметь любой тип, причем раз ные элементы одного массива могут иметь разные типы. Элементы массива могут даже содержать другие массивы, что позволяет создавать массивы массивов. На протяжении всей книги мы часто рассматриваем объекты и массивы как от дельные типы данных. Это полезное и разумное упрощение – в JavaScript объек ты и массивы можно рассматривать как разные типы для большинства задач программирования. Однако, чтобы хорошо понять поведение объектов и масси вов, следует знать правду: массив – это не что иное, как объект с тонким слоем дополнительной функциональности. Это можно увидеть, определив тип массива с помощью оператора typeof – будет получена строка "object". Легче всего создать массив можно с помощью литерала, который представляет собой простой список разделенных запятыми элементов массива в квадратных скобках. Например: var empty = []; // Пустой массив var primes = [2, 3, 5, 7, 11]; // Массив с пятью числовыми элементами var misc = [ 1.1, true, "a" ]; // 3 элемента разных типов Значения в литерале массива не обязательно должны быть константами – это могут быть любые выражения: var base = 1024; var table = [base, base+1, base+2, base+3]; 130 Глава 7. Объекты и массивы Литералы массивов могут содержать литералы объектов или литералы других массивов: var b = [[1,{x:1, y:2}], [2, {x:3, y:4}]]; Во вновь созданном массиве первое значение литерала массива сохраняется в эле менте с индексом 0, второе значение – в элементе с индексом 1, и т. д. Если в ли терале значение элемента опущено, будет создан элемент с неопределенным зна чением: var count = [1,,3]; // Массив из 3 элементов, средний элемент не определен. var undefs = [,,]; // Массив из 2 элементов, оба не определены. Другой способ создания массива состоит в вызове конструктора Array(). Вызы вать конструктор можно тремя разными способами: • Вызов конструктора без аргументов: var a = new Array( ); В этом случае будет создан пустой массив, эквивалентный литералу []. • Конструктору явно указываются значения первых n элементов массива: var a = new Array(5, 4, 3, 2, 1, "testing, testing"); В этом случае конструктор получает список аргументов. Каждый аргумент определяет значение элемента и может иметь любой тип. Нумерация элемен тов массива начинается с 0. Свойство length массива устанавливается равным количеству элементов, переданных конструктору. • Вызов с единственным числовым аргументом, определяющим длину массива: var a = new Array(10); Эта форма позволяет создать массив с заданным количеством элементов (каж дый из которых имеет значение undefined) и устанавливает свойство length массива равным указанному значению. Эта форма обращения к конструкто ру Array() может использоваться для предварительного размещения массива, если его длина известна заранее. В этой ситуации литералы массивов не очень удобны. 7.6. Чтение и запись элементов массива Доступ к элементам массива осуществляется с помощью оператора []. Слева от скобок должна присутствовать ссылка на массив. Внутри скобок должно нахо диться произвольное выражение, имеющее неотрицательное целое значение. Этот синтаксис пригоден как для чтения, так и для записи значения элемента массива. Следовательно, все приведенные далее JavaScriptинструкции допустимы: value = a[0]; a[1] = 3.14; i = 2; a[i] = 3; a[i + 1] = "hello"; a[a[i]] = a[0]; 7.6. Чтение и запись элементов массива 131 В некоторых языках первый элемент массива имеет индекс 1. Однако в Java Script (как в C, C++ и Java) первый элемент массива имеет индекс 0. Как уже отмечалось, оператор [] может также использоваться для доступа к име нованным свойствам объекта: my['salary'] *= 2; Поскольку массивы являются специализированным классом объектов, сущест вует возможность определять нечисловые свойства объекта и обращаться к ним посредством операторов . (точка) и []. Обратите внимание, что индекс массива должен быть неотрицательным числом, меньшим 232–1. Если число слишком большое, отрицательное или вещественное (или это логическое, объектное или другое значение), JavaScript преобразует его в строку и рассматривает результирующую строку как имя свойства объекта, а не как индекс массива. Таким образом, следующая строка создаст новое свой ство с именем "–1.23", а не новый элемент массива: a[1.23] = true; 7.6.1. Добавление новых элементов в массив В таких языках, как C и Java, массив имеет фиксированное число элементов, ко торое должно быть задано при создании массива. Это не относится к JavaScript – массив в JavaScript может иметь любое количество элементов, и это количество можно в любой момент изменить. Чтобы добавить новый элемент в массив, достаточно присвоить ему значение: a[10] = 10; Массивы в JavaScript могут быть разреженными. Это значит, что индексы мас сива не обязательно принадлежат непрерывному диапазону чисел; реализация JavaScript может выделять память только для тех элементов массива, которые фактически в нем хранятся. Поэтому в результате выполнения следующего фрагмента интерпретатор JavaScript скорее всего выделит память только для элементов массива с индексами 0 и 10 000, но не выделит ее для 9 999 элементов, находящихся между ними: a[0] = 1; a[10000] = "это элемент 10,000"; Обратите внимание: элементы массива могут также добавляться к объектам: var c = new Circle(1,2,3); c[0] = "это элемент массива в объекте!" Этот пример просто определяет новое свойство объекта с именем "0". Однако до бавление элемента массива в объект не делает объект массивом. 7.6.2. Удаление элементов массива Оператор delete записывает в элемент массива значение undefined, при этом сам элемент массива продолжает свое существование. Для удаления элементов так, чтобы остающиеся элементы сместились к началу массива, необходимо восполь зоваться одним из методов массива. Метод Array.shift() удаляет первый элемент 132 Глава 7. Объекты и массивы массива, метод Array.pop() – последний элемент массива, метод Array.splice() – непрерывный диапазон элементов. Эти функции описываются далее в этой гла ве, а также в третьей части книги. 7.6.3. Длина массива Все массивы, как созданные с помощью конструктора Array(), так и определен ные с помощью литерала массива, имеют специальное свойство length, устанав ливающее количество элементов в массиве. Поскольку массивы могут иметь не определенные элементы, более точная формулировка звучит так: свойство length всегда на единицу больше, чем самый большой номер элемента массива. В отли чие от обычных свойств объектов, свойство length массива автоматически обнов ляется, оставаясь корректным при добавлении новых элементов в массив. Это обстоятельство иллюстрирует следующий фрагмент: var a = new Array(); // a.length == 0 (ни один элемент не определен) a = new Array(10); // a.length == 10 (определены пустые элементы 0–9) a = new Array(1,2,3); // a.length == 3 (определены элементы 0–2) a = [4, 5]; // a.length == 2 (определены элементы 0 и 1) a[5] = 1; // a.length == 6 (определены элементы 0, 1 и 5) a[49] = 0; // a.length == 50 (определены элементы 0, 1, 5 и 49) Помните, что индексы массива должны быть меньше 232–1, т. е. максимально возможное значение свойства length равно 232–1. 7.6.4. Обход элементов массива Наиболее часто свойство length используется для перебора элементов массива в цикле: var fruits = ["манго", "банан", "вишня", "персик"]; for(var i = 0; i < fruits.length; i++) alert(fruits[i]); Конечно, в этом примере предполагается, что элементы массива расположены непрерывно и начинаются с элемента 0. Если это не так, перед обращением к каж дому элементу массива нужно проверять, определен ли он: for(var i = 0; i < fruits.length; i++) if (fruits[i] != undefined) alert(fruits[i]); Аналогичный подход может использоваться для инициализации элементов мас сива, созданного вызовом конструктора Array(): var lookup_table = new Array(1024); for(var i = 0; i < lookup_table.length; i++) lookup_table[i] = i * 512; 7.6.5. Усечение и увеличение массива Свойство length массива доступно как для чтения, так и для записи. Если устано вить свойство length в значение, меньшее текущего, массив укорачивается до но вой длины; любые элементы, не попадающие в новый диапазон индексов, отбра сываются, и их значения теряются. 7.7. Методы массивов 133 Если сделать свойство length большим, чем его текущее значение, в конец масси ва добавляются новые неопределенные элементы, увеличивая массив до нового размера. Обратите внимание: хотя объектам могут быть присвоены элементы массива, объекты не имеют свойства length. Это свойство и его специальное поведение – наиболее важная особенность, свойственная массивам. Другие особенности, от личающие массивы от объектов, – это различные методы, определяемые клас сом Array и описываемые в разделе 7.7. 7.6.6. Многомерные массивы JavaScript не поддерживает «настоящие» многомерные массивы, но позволяет неплохо имитировать их при помощи массивов из массивов. Для доступа к эле менту данных в массиве массивов достаточно использовать оператор [] дважды. Например, предположим, что переменная matrix – это массив массивов чисел. Любой элемент matrix[x] – это массив чисел. Для доступа к определенному числу в массиве надо написать matrix[x][y]. Вот конкретный пример, в котором двух мерный массив используется в качестве таблицы умножения: // Создать многомерный массив var table = new Array(10); // В таблице 10 строк for(var i = 0; i < table.length; i++) table[i] = new Array(10); // В каждой строке 10 столбцов // Инициализация массива for(var row = 0; row < table.length; row++) { for(col = 0; col < table[row].length; col++) { table[row][col] = row*col; } } // Расчет произведения 5*7 с помощью многомерного массива var product = table[5][7]; // 35 7.7. Методы массивов Помимо оператора [] с массивами можно работать посредством различных мето дов, предоставляемых классом Array. Эти методы представлены в следующих разделах. Многие из методов позаимствованы из языка программирования Perl; программистам, работавшим с Perl, они могут показаться знакомыми. Как обычно, здесь приведен только их обзор, а полные описания находятся в третьей части книги. 7.7.1. Метод join() Метод Array.join() преобразует все элементы массива в строки и объединяет их. Можно указать необязательный строковый аргумент, предназначенный для раз деления элементов в результирующей строке. Если разделитель не задан, исполь зуется запятая. Например, следующий фрагмент дает в результате строку "1,2,3": var a = [1, 2, 3]; // Создает новый массив с указанными тремя элементами var s = a.join(); // s == "1,2,3" 134 Глава 7. Объекты и массивы В следующем примере задается необязательный разделитель, что приводит к не сколько иному результату: s = a.join(", "); // s == "1, 2, 3" Обратите внимание на пробел после запятой. Метод Array.join() является обратным по отношению к методу String.split(), создающему массив путем разбиения строки на фрагменты. 7.7.2. Метод reverse() Метод Array.reverse() меняет порядок следования элементов в массиве на проти воположный и возвращает массив с переставленными элементами. Он делает это на месте, другими словами, этот метод не создает новый массив с переупорядо ченными элементами, а переупорядочивает их в уже существующем массиве. Например, следующий фрагмент, где используются методы reverse() и join(), дает в результате строку "3,2,1": var a = new Array(1,2,3); // a[0] = 1, a[1] = 2, a[2] = 3 a.reverse(); // теперь a[0] = 3, a[1] = 2, a[2] = 1 var s = a.join(); // s == "3,2,1" 7.7.3. Метод sort() Метод Array.sort() на месте сортирует элементы массива и возвращает отсорти рованный массив. Если метод sort() вызывается без аргументов, то он сортирует элементы массива в алфавитном порядке (при необходимости временно преобра зуя их в строки для выполнения сравнения): var a = new Array("banana", "cherry", "apple"); a.sort(); var s = a.join(", "); // s == "apple, banana, cherry" Неопределенные элементы переносятся в конец массива. Для сортировки в какомлибо ином порядке, отличном от алфавитного, можно передать методу sort() в качестве аргумента функцию сравнения. Эта функция устанавливает, какой из двух ее аргументов должен присутствовать раньше в от сортированном списке. Если первый аргумент должен предшествовать второму, функция сравнения возвращает отрицательное число. Если первый аргумент в отсортированном массиве должен следовать за вторым, то функция возвраща ет число, большее нуля. А если два значения эквивалентны (т. е. порядок их рас положения не важен), функция сравнения возвращает 0. Поэтому, например, для сортировки элемента в числовом порядке, а не в алфавитном, можно сделать следующее: var a = [33, 4, 1111, 222]; a.sort(); // Алфавитный порядок: 1111, 222, 33, 4 a.sort(function(a,b) { // Числовой порядок: 4, 33, 222, 1111 return ab; // Возвращает значение < 0, 0, или > 0 }); // в зависимости от порядка сортировки a и b Обратите внимание, насколько удобно использовать в этом фрагменте функцио нальный литерал. Функция сравнения вызывается только один раз, поэтому нет необходимости давать ей имя. 7.7. Методы массивов 135 В качестве еще одного примера сортировки элементов массива вы можете выпол нить алфавитную сортировку массива строк без учета регистра символов, передав методу функцию сравнения, преобразующую перед сравнением оба своих аргу мента в нижний регистр (с помощью метода toLowerCase()). Можно придумать и другие функции сортировки, сортирующие числа в различном экзотическом по рядке: обратном числовом, нечетные числа перед четными и т. д. Более интерес ные возможности, конечно же, открываются, когда сравниваемые элементы мас сива представляют собой объекты, а не простые типы, такие как числа и строки. 7.7.4. Метод concat() Метод Array.concat() создает и возвращает новый массив, содержащий элементы исходного массива, для которого был вызван метод concat(), последовательно до полненный значениями всех аргументов, переданных методу concat(). Если ка койлибо из этих аргументов сам является массивом, в результирующий массив добавляются его элементы. Однако обратите внимание, что рекурсивного разде ления массивов из массивов не происходит. Вот несколько примеров: var a = [1,2,3]; a.concat(4, 5) // Возвращает [1,2,3,4,5] a.concat([4,5]); // Возвращает [1,2,3,4,5] a.concat([4,5],[6,7]) // Возвращает [1,2,3,4,5,6,7] a.concat(4, [5,[6,7]]) // Возвращает [1,2,3,4,5,[6,7]] 7.7.5. Метод slice() Метод Array.slice() возвращает фрагмент, или подмассив, указанного массива. Два аргумента метода определяют начало и конец возвращаемого фрагмента. Возвращаемый массив содержит элемент, номер которого указан в качестве пер вого аргумента, плюс все последующие элементы, вплоть до (но не включая) эле мента, номер которого указан во втором аргументе. Если указан только один ар гумент, возвращаемый массив содержит все элементы от начальной позиции до конца массива. Если какойлибо из аргументов отрицателен, он задает номер элемента массива относительно конца массива. Так, аргумент, равный –1, задает последний элемент массива, а аргумент, равный –3, – третий элемент массива с конца. Вот несколько примеров: var a = [1,2,3,4,5]; a.slice(0,3); // Возвращает [1,2,3] a.slice(3); // Возвращает [4,5] a.slice(1,1); // Возвращает [2,3,4] a.slice(3,2); // Возвращает [3] 7.7.6. Метод splice() Метод Array.splice() – это универсальный метод для вставки или удаления эле ментов массива. Он изменяет массив на месте, а не возвращает новый массив, как это делают методы slice() и concat(). Обратите внимание: splice() и slice() имеют очень похожие имена, но выполняют разные операции. Метод splice() может удалять элементы из массива, вставлять новые элементы в массив или выполнять обе операции одновременно. Элементы массива при не 136 Глава 7. Объекты и массивы обходимости смещаются, чтобы после вставки или удаления образовывалась не прерывная последовательность. Первый аргумент splice() задает позицию в мас сиве, с которой начинается вставка и/или удаление. Второй аргумент задает ко личество элементов, которые должны быть удалены (вырезаны) из массива. Если второй аргумент опущен, удаляются все элементы массива от начального до кон ца массива. Метод splice() возвращает массив удаленных элементов или (если ни один из элементов не был удален) пустой массив. Например: var a = [1,2,3,4,5,6,7,8]; a.splice(4); // Возвращает [5,6,7,8]; a равно [1,2,3,4] a.splice(1,2); // Возвращает [2,3]; a равно [1,4] a.splice(1,1); // Возвращает [4]; a равно [1] Первые два аргумента splice() задают элементы массива, подлежащие удале нию. За этими аргументами может следовать любое количество дополнительных аргументов, задающих элементы, которые будут вставлены в массив, начиная с позиции, заданной первым аргументом. Например: var a = [1,2,3,4,5]; a.splice(2,0,'a','b'); // Возвращает []; a равно [1,2,'a','b',3,4,5] a.splice(2,2,[1,2],3); // Возвращает ['a','b']; a равно [1,2,[1,2],3,3,4,5] Обратите внимание, что, в отличие от concat(), метод splice() не разбивает на от дельные элементы вставляемые аргументымассивы. То есть если методу переда ется массив для вставки, он вставляет сам массив, а не элементы этого массива. 7.7.7. Методы push() и pop() Методы push() и pop() позволяют работать с массивами как со стеками. Метод push() добавляет один или несколько новых элементов в конец массива и возвра щает его новую длину. Метод pop() выполняет обратную операцию – удаляет по следний элемент массива, уменьшает длину массива и возвращает удаленное им значение. Обратите внимание: оба эти метода изменяют массив на месте, а не создают его модифицированную копию. Комбинация push() и pop() позволяет в JavaScript с помощью массива реализовать стек с дисциплиной обслуживания «первым вошел – последним вышел». Например: var stack = []; // стек: [] stack.push(1,2); // стек: [1,2] Возвращает 2 stack.pop(); // стек: [1] Возвращает 2 stack.push(3); // стек: [1,3] Возвращает 2 stack.pop(); // стек: [1] Возвращает 3 stack.push([4,5]); // стек: [1,[4,5]] Возвращает 2 stack.pop() // стек: [1] Возвращает [4,5] stack.pop(); // стек: [] Возвращает 1 7.7.8. Методы unshift() и shift() Методы unshift() и shift() ведут себя во многом так же, как push() и pop(), за ис ключением того, что они вставляют и удаляют элементы в начале массива, а не в его конце. Метод unshift() смещает существующие элементы в сторону боль ших индексов для освобождения места, добавляет элемент или элементы в нача ло массива и возвращает новую длину массива. Метод shift() удаляет и возвра 7.7. Методы массивов 137 щает первый элемент массива, смещая все последующие элементы вперед на од ну позицию для занятия свободного места в начале массива. Например: var a = []; // a:[] a.unshift(1); // a:[1] Возвращает: 1 a.unshift(22); // a:[22,1] Возвращает: 2 a.shift(); // a:[1] Возвращает: 22 a.unshift(3,[4,5]); // a:[3,[4,5],1] Возвращает: 3 a.shift(); // a:[[4,5],1] Возвращает: 3 a.shift(); // a:[1] Возвращает: [4,5] a.shift(); // a:[] Возвращает: 1 Обратите внимание на поведение метода unshift() при вызове с несколькими ар гументами. Аргументы вставляются не по одному, а все сразу (как в случае с ме тодом splice()). Это значит, что в результирующем массиве они будут следовать в том же порядке, в котором были указаны в списке аргументов. Будучи встав ленными по одному, они бы расположились в обратном порядке. 7.7.9. Методы toString() и toLocaleString() У массива, как и у любого другого объекта в JavaScript, имеется метод toString(). Для массива этот метод преобразует каждый из его элементов в строку (вызывая в случае необходимости методы toString() для элементов массива) и выводит спи сок этих строк через запятую. Отметьте, что результат не включает квадратных скобок или какихлибо других разделителей вокруг значений массива. Например: [1,2,3].toString() // Получается '1,2,3' ["a", "b", "c"].toString() // Получается 'a,b,c' [1, [2,'c']].toString() // Получается '1,2,c' Обратите внимание: toString() возвращает ту же строку, что и метод join() при вызове его без аргументов. Метод toLocaleString() – это локализованная версия toString(). Каждый элемент массива преобразуется в строку вызовом метода toLocaleString() элемента, а за тем результирующие строки конкатенируются с использованием специфическо го для региона (и определенного реализацией) разделителя. 7.7.10. Дополнительные методы массивов Броузер Firefox Mozilla 1.5 включает в себя новую версию JavaScript 1.6, в кото рую был добавлен набор дополнительных методов массивов, получивших назва ние дополнений к массивам (array extras). Из наиболее примечательных можно назвать методы indexOf() и lastIndexOf(), позволяющие быстро отыскать в масси ве заданное значение (описание аналогичного им метода String.indexOf() можно найти в третьей части книги). Кроме того, в состав набора входят еще несколько интересных методов: метод forEach() вызывает указанную функцию для каждо го элемента в массиве; метод map() возвращает массив, полученный в результате передачи всех элементов массива указанной функции; метод filter() возвраща ет массив элементов, для которых заданная функция возвратила значение true. На момент написания этих строк набор дополнительных методов массивов был доступен только в броузере Firefox и пока еще не является стандартом ни офици 138 Глава 7. Объекты и массивы ально, ни дефакто. Здесь эти методы не описываются. Однако если вы предпола гаете заниматься разработкой сценариев только для Firefox или в вашем распоря жении имеется библиотека, содержащая эти достаточно просто реализуемые ме тоды, то подробное их описание можно найти на сайте http://developer.mozilla.org. 7.8. Объекты, подобные массивам Массивы в JavaScript являются особенными, потому что их свойство length обла дает особенным поведением: • Значение этого свойства автоматически изменяется при добавлении к масси ву новых элементов. • Изменение этого свойства в программе приводит к усечению или увеличению массива. Массивы в JavaScript являются экземплярами класса Array (instanceof Array), и для работы с ними могут использоваться методы этого класса. Все эти характеристики являются уникальными для JavaScriptмассивов, но они не главное, что определяет массив. Бывает полезно организовать работу с произ вольным объектом, как со своего рода массивом – через свойство length и соответ ствующие неотрицательные целочисленные свойства. Такие объекты, «подобные массивам», иногда используются для решения практических задач. Хотя с ними нельзя работать через методы массивов или ожидать специфического поведения свойства length, можно организовать перебор свойств объекта теми же программ ными конструкциями, которые используются при работе с настоящими массива ми. Оказывается, что значительное число алгоритмов для работы с массивами вполне пригодно для работы с объектами, подобными массивам. Пока вы не буде те пытаться добавлять элементы в массив или изменять свойство length, вы впол не сможете обрабатывать объекты, подобные массивам, как обычные массивы. В следующем фрагменте создается обычный объект и к нему добавляются допол нительные свойства, которые превращают его в объект, подобный массиву, по сле чего производится перебор «элементов» получившегося псевдомассива. var a = {}; // Для начала создать обычный пустой объект // Добавить свойства, которые сделают его похожим на массив var i = 0; while(i < 10) { a[i] = i * i; i++; } a.length = i; // Теперь можно перебрать свойства объекта, как если бы это был настоящий массив var total = 0; for(var j = 0; j < a.length; j++) total += a[j]; Объект Argument, который описывается в разделе 8.2.2, является объектом, подоб ным массиву. В клиентском языке JavaScript такие объекты возвращают многие методы объектной модели документа (DOM), например метод document.getEle mentsByTagName(). Функции Функция – это блок программного кода на языке JavaScript, который определя ется один раз и может вызываться многократно. Функции могут иметь пара' метры, или аргументы, – локальные переменные, значения которых определя ются при вызове функции. Функции часто используют свои аргументы для вы числения возвращаемого значения, которое является значением выражения вы зова функции. Если функция вызывается в контексте объекта, она называется методом, а сам объект передается ей в виде неявного аргумента. Вероятно, вы уже знакомы с концепцией функции, если встречались с такими понятиями, как подпрограмма и процедура. В этой главе мы сосредоточимся на определении и вызове собственных Java Scriptфункций. Важно помнить, что JavaScript поддерживает некоторое коли чество встроенных функций, таких как eval() и parseInt() или метод sort() клас са Array. В клиентском языке JavaScript определяются другие функции, напри мер document.write() и alert(). Встроенные JavaScriptфункции применяются точно так же, как и функции, определенные пользователем. О функциях, упомя нутых здесь, более подробно можно узнать в третьей и четвертой частях книги. Функции и объекты в JavaScript тесно связаны между собой. По этой причине мы отложим обсуждение некоторых возможностей функций до главы 9. 8.1. Определение и вызов функций Как мы видели в главе 6, самый распространенный способ определения функции – использование инструкции function. Она состоит из ключевого слова function, за которым следуют: • имя функции; • заключенный в круглые скобки необязательный список имен параметров, разделенных запятыми; • JavaScriptинструкции, составляющие тело функции, заключенные в фигур ные скобки. 140 Глава 8. Функции В примере 8.1 показаны определения некоторых функций. Хотя эти функции короткие и простые, они содержат все перечисленные здесь элементы. Обратите внимание: в функциях может быть определено различное количество аргумен тов, функции также могут содержать или не содержать инструкцию return. Ин струкция return была описана в главе 6; она прекращает выполнение функции и возвращает значение указанного в ней выражения (если оно есть) вызываю щей стороне; при отсутствии выражения инструкция возвращает значение unde fined. Если функция не содержит инструкцию return, она просто выполняет все инструкции в своем теле и возвращает неопределенное значение (undefined). Пример 8.1. Определение JavaScript'функций // Функцияобертка, иногда ее удобно использовать вместо document.write(). // В этой функции отсутствует инструкция return, поэтому она не возвращает значение. function print(msg) { document.write(msg, "
"); } // Функция, вычисляющая и возвращающая расстояние между двумя точками. function distance(x1, y1, x2, y2) { var dx = x2  x1; var dy = y2  y1; return Math.sqrt(dx*dx + dy*dy); } // Рекурсивная функция (вызывающая сама себя), вычисляющая факториалы. // Вспомните, что x! – это произведение x и всех положительных целых чисел, меньших х. function factorial(x) { if (x <= 1) return 1; return x * factorial(x1); } Будучи один раз определенной, функция может вызываться с помощью операто ра (), описанного в главе 5. Как вы помните, скобки указываются после имени функции, а необязательный список значений (или выражений) аргументов ука зывается в скобках через запятую (фактически перед круглыми скобками может указываться любое JavaScriptвыражение, которое возвращает значениефунк цию). Функции, определенные в примере 8.1, могут быть вызваны следующим образом: print("Привет, " + name); print("Добро пожаловать на мою домашнюю страницу!"); total_dist = distance(0,0,2,1) + distance(2,1,3,5); print("Вероятность этого равна: " + factorial(5)/factorial(13)); При вызове функции вычисляются все выражения, указанные между скобками, и полученные значения используются в качестве аргументов функции. Эти зна чения присваиваются параметрам, имена которых перечислены в определении функции, и функция работает с ними, ссылаясь на эти параметры по указанным именам. Обратите внимание: эти переменныепараметры определены, только 8.1. Определение и вызов функций 141 пока выполняется функция; они не сохраняются после завершения ее работы (за одним важным исключением, которое описывается в разделе 8.8). JavaScript – язык с нестрогой типизацией, поэтому тип параметров функций указывать не требуется, и JavaScript не проверяет, соответствует ли тип данных требованиям функции. Если тип аргумента важен, вы можете проверить его са мостоятельно с помощью оператора typeof. Кроме того, JavaScript не проверяет, правильное ли количество параметров передано функции. Если аргументов больше, чем требуется функции, то дополнительные значения просто игнориру ются. Если аргументов меньше, то отсутствующим присваивается значение un defined. Некоторые функции написаны так, что могут достаточно терпимо отно ситься к нехватке аргументов, другие ведут себя некорректно, если получают меньшее число аргументов, чем предполагалось. Далее в этой главе мы познако мимся с приемом, позволяющим проверить, правильное ли количество аргумен тов передано в функцию, и организовать доступ к этим аргументам по их поряд ковым номерам в списке аргументов, а не по именам. Обратите внимание: в функции print() из примера 8.1 нет инструкции return, по этому она всегда возвращает значение undefined, и использовать ее в качестве час ти более сложного выражения не имеет смысла. А функции distance() и facto rial() могут вызываться в более сложных выражениях, что было показано в пре дыдущих примерах. 8.1.1. Вложенные функции В JavaScript допускается вложение определений функций в другие функции. Например: function hypotenuse(a, b) { function square(x) { return x*x; } return Math.sqrt(square(a) + square(b)); } Вложенные функции могут определяться только в коде функций верхнего уров ня. Это значит, что определения функций не могут находиться, например, внут ри циклов или условных инструкций.1 Обратите внимание: эти ограничения распространяются только на объявления функций с помощью инструкции func tion. Функциональные литералы (которые описываются в следующем разделе) могут присутствовать внутри любых выражений. 8.1.2. Функциональные литералы JavaScript позволяет определять функции в виде функциональных литералов. Как говорилось в главе 3, функциональный литерал – это выражение, определяю щее неименованную функцию. Синтаксис функционального литерала во многом напоминает синтаксис инструкции function, за исключением того, что он ис 1 Различные реализации JavaScript могут иметь менее строгие требования к опреде лениям функций, чем указано в стандарте. Например, реализации Netscape Java Script 1.5 допускают наличие «условных определений функций» внутри инструк ций if. 142 Глава 8. Функции пользуется как выражение, а не как инструкция, и ему не требуется имя функ ции. Следующие две строки кода определяют две более или менее идентичные функции с помощью инструкции function и функционального литерала: function f(x) { return x*x; } // инструкция function var f = function(x) { return x*x; }; // функциональный литерал Функциональные литералы создают неименованные функции, но синтаксис до пускает указание имени функции, что может пригодиться при написании ре курсивных функций, вызывающих самих себя. Например: var f = function fact(x) { if (x <= 1) return 1; else return x*fact(x 1); }; Эта строка кода определяет неименованную функцию и сохраняет ссылку на нее в переменной f. Она на самом деле не создает функцию с именем fact, но позво ляет телу функции ссылаться с помощью этого имени на саму себя. Заметим, од нако, что именованные функциональные литералы до версии JavaScript 1.5 ра ботали не вполне корректно. Функциональные литералы создаются JavaScriptвыражениями, а не инструк циями, и потому могут использоваться более гибко. Это особенно подходит для функций, которые вызываются только один раз и не должны иметь имени. На пример, функция, определенная с помощью выражения функционального лите рала, может быть сохранена в переменной, передана другой функции или даже вызвана непосредственно: f[0] = function(x) { return x*x; }; // Определить и сохранить функцию в переменной a.sort(function(a,b){return ab;}); // Определить функцию; передать ее другой функции var tensquared = (function(x) {return x*x;})(10); // Определить и вызывать 8.1.3. Именование функций В качестве имени функции может использоваться любой допустимый Java Scriptидентификатор. Старайтесь выбирать функциям достаточно описатель ные, но не длинные имена. Искусство сохранения баланса между краткостью и информативностью приходит с опытом. Правильно подобранные имена функ ций могут существенно повысить удобочитаемость (а значит, и простоту сопро вождения) ваших программ. Чаще всего в качестве имен функций выбираются глаголы или фразы, начинаю щиеся с глаголов. По общепринятому соглашению имена функций начинаются со строчной буквы. Если имя состоит из нескольких слов, в соответствии с одним из соглашений они отделяются друг от друга символом подчеркивания, пример но так: like_this(), по другому соглашению все слова, кроме первого, начинают ся с прописной буквы, примерно так: likeThis(). Имена функций, которые, как предполагается, реализуют внутреннюю, скрытую от посторонних глаз функ циональность, иногда начинаются с символа подчеркивания. В некоторых стилях программирования или в четко определенных программных платформах бывает полезно давать наиболее часто используемым функциям очень короткие имена. Примером может служить платформа Prototype клиент ского языка JavaScript (http://prototype.conio.net), в которую весьма элегантно вписалась функция с именем $() (дада, просто знак доллара) в качестве замены сложному для набора с клавиатуры имени document.getElementById(). (В главе 2 8.2. Аргументы функций 143 уже говорилось, что в идентификаторах JavaScript допускается использовать знаки доллара и подчеркивания.) 8.2. Аргументы функций Функции в JavaScript могут вызываться с произвольным числом аргументов не зависимо от того, сколько аргументов указано в определении именованной функции. Поскольку функции являются слабо типизированными, отсутствует возможность задавать типы входных аргументов, в связи с чем считается допус тимым передавать значения любых типов любым функциям. Все эти вопросы об суждаются в следующих подразделах. 8.2.1. Необязательные аргументы Когда функция вызывается с меньшим количеством аргументов, чем описывается в определении, недостающие аргументы получают значение undefined. Иногда бы вает удобным учесть необязательность некоторых аргументов – тех, которые мо гут быть опущены при вызове функции. В этом случае желательно предусмотреть возможность присваивания по умолчанию достаточно разумных значений аргу ментам, которые были опущены (или переданы со значением null). Например: // Добавить в массив a перечислимые имена свойств объекта o и вернуть массив a. // Если массив a не указан или равен null, создать и вернуть новый массив a function copyPropertyNamesToArray(o, /* необязательный */ a) { if (!a) a = []; // Если массив не определен или получено // значение null, создать новый пустой массив a for(var property in o) a.push(property); return a; } Когда функция определена таким образом, появляются более широкие возмож ности обращения к ней: // Получить имена свойств объектов o и p var a = copyPropertyNamesToArray(o); // Получить свойства объекта o // в виде нового массива copyPropertyNamesToArray(p,a); // добавить к массиву свойства объекта p Вместо инструкции if в первой строке этой функции можно использовать опера тор || следующим образом: a = a || []; В главе 5 уже говорилось, что оператор || возвращает первый аргумент, если он равен true или преобразуется в логическое значение true. В противном случае возвращается второй аргумент. В данном случае он вернет a, если переменная a определена и не содержит значение null даже в том случае, если a – это пустой массив. В противном случае он вернет новый пустой массив. Обратите внимание: при объявлении функций необязательные аргументы долж ны завершать список аргументов, чтобы их можно было опустить. Программист, который будет писать обращение к вашей функции, не сможет передать второй аргумент и при этом опустить первый. В этом случае он вынужден будет явно пе редать в первом аргументе значение undefined или null. 144 Глава 8. Функции 8.2.2. Списки аргументов переменной длины: объект Arguments В теле функции идентификатор arguments всегда имеет особый смысл; arguments – это специальное свойство объекта вызова, ссылающееся на объект, известный как объект Arguments. Объект Arguments – это нечто вроде массива (см. раздел 7.8), позволяющего извлекать переданные функции значения по номеру, а не по име ни. Объект Arguments также определяет дополнительное свойство callee, которое описано в следующем разделе. Хотя JavaScriptфункция определяется с фиксированным количеством имено ванных аргументов, при вызове ей может быть передано любое их число. Объект Arguments обеспечивает полный доступ к значениям аргументов, даже если у не которых из них нет имени. Предположим, что была определена функция f, кото рая требует один аргумент, x. Если вызвать эту функцию с двумя аргументами, то первый будет доступен в функции по имени параметра x или как arguments[0]. Второй аргумент доступен только как arguments[1]. Кроме того, как и у всех мас сивов, у arguments имеется свойство length, указывающее на количество содержа щихся в массиве элементов. То есть в теле функции f, вызываемой с двумя аргу ментами, arguments.length имеет значение 2. Объект arguments может использоваться с самыми разными целями. Следующий пример показывает, как с его помощью проверить, была ли функция вызвана с правильным числом аргументов, – ведь JavaScript этого за вас не сделает: function f(x, y, z) { // Сначала проверяется, правильное ли количество аргументов передано if (arguments.length != 3) { throw new Error("функция f вызвана с " + arguments.length + "аргументами, а требуется 3."); } // А теперь сам код функции... } Объект arguments иллюстрирует важную возможность JavaScriptфункций: они могут быть написаны таким образом, чтобы работать с любым количеством аргу ментов. Далее приводится пример, показывающий, как можно написать про стую функцию max(), принимающую любое число аргументов и возвращающую значение самого большого из них (аналогично ведет себя встроенная функция Math.max()): function max(/*...*/) { var m = Number.NEGATIVE_INFINITY; // Цикл по всем аргументам, поиск и сохранение наибольшего из них for(var i = 0; i < arguments.length; i++) if (arguments[i] > m) m = arguments[i]; // Возвращаем максимальный return m; } var largest = max(1, 10, 100, 2, 3, 1000, 4, 5, 10000, 6); 8.2. Аргументы функций 145 Функции, подобные этой и способные принимать произвольное число аргумен тов, называются функциями с переменным числом аргументов (variadic func' tions, variable arity functions или varargs functions). Этот термин возник вместе с появлением языка программирования C. Обратите внимание: функции с переменным числом аргументов не должны до пускать возможность вызова с пустым списком аргументов. Будет вполне разум ным использовать объект arguments[] при написании функции, ожидающей по лучить фиксированное число обязательных именованных аргументов, за кото рыми может следовать произвольное число необязательных неименованных ар гументов. Не следует забывать, что arguments фактически не является массивом – это объект Arguments. В каждом объекте Arguments имеются пронумерованные элементы мас сива и свойство length, но с технической точки зрения – это не массив. Лучше рас сматривать его как объект, имеющий некоторые пронумерованные свойства. Спе цификация ECMAScript не требует от объекта Arguments реализации какоголибо специфического для массива поведения. Хотя, например, допускается присваи вать значение свойству arguments.length, ECMAScript не требует этого для реаль ного изменения числа элементов массива, определенных в объекте. (Особое пове дение свойства length для настоящих объектов Array описывается в разделе 7.6.3.) У объекта Arguments есть одна очень необычная особенность. Когда у функции име ются именованные аргументы, элементы массива объекта Arguments являются си нонимами локальных переменных, содержащих аргументы функции. Массив ar guments[] и именованные аргументы – это два разных средства обращения к одной переменной. Изменение значения аргумента через имя аргумента меняет значе ние, извлекаемое через массив arguments[]. Изменение значения аргумента через массив arguments[] меняет значение, извлекаемое по имени аргумента. Например: function f(x) { print(x); // Выводит начальное значение аргумента arguments[0] = null; // Изменяя элементы массива, мы также изменяем x print(x); // Теперь выводит "null" } Определенно, это не совсем то поведение, которое можно было бы ожидать от на стоящего массива. В этом случае arguments[0] и x могли бы ссылаться на одно и то же значение, но изменение одной ссылки не должно оказывать влияния на другую. Наконец, следует учитывать, что arguments – это всего лишь обычный JavaScript идентификатор, а не зарезервированное слово. Если функция определит аргу мент или локальную переменную с таким же именем, объект Arguments станет не доступным. По этой причине следует считать слово arguments зарезервированным и стараться избегать создавать переменные с таким именем. 8.2.2.1. Свойство callee Помимо элементов своего массива объект Arguments определяет свойство callee, ссылающееся на исполняемую в данный момент функцию. Его можно использо вать, например, для рекурсивного вызова неименованных функций. Вот пример неименованного функционального литерала, вычисляющего факториал: 146 Глава 8. Функции function(x) { if (x <= 1) return 1; return x * arguments.callee(x  1); } 8.2.3. Использование свойств объекта в качестве аргументов Когда функция имеет более трех аргументов, становится трудно запоминать правильный порядок их следования. Чтобы предотвратить ошибки и избавить программиста от необходимости заглядывать в справочное руководство всякий раз, когда он намеревается вставить в программу вызов такой функции, можно предусмотреть возможность передачи аргументов в виде пар «имя–значение» в произвольном порядке. Чтобы реализовать такую возможность, при определе нии функции следует учесть передачу объекта в качестве единственного аргу мента. Благодаря такому стилю, пользователи функции смогут передавать функции литерал объекта, в котором будут определяться необходимые пары «имя–значение». В следующем фрагменте приводится пример такой функции, а также демонстрируется возможность определения значений по умолчанию для опущенных аргументов: // Скопировать length элементов из массива from в массив to. // Копирование начинается с элемента from_start в массиве from // и выполняется в элементы, начиная с to_start в массиве to. // Запомнить порядок следования аргументов такой функции довольно сложно. function arraycopy(/* array */ from, /* index */ from_start, /* array */ to, /* index */ to_start, /* integer */ length) { // здесь находится реализация функции } // Эта версия функции чуть менее эффективная, но не требует // запоминать порядок следования аргументов, а аргументы from_start // и to_start по умолчанию принимают значение 0. function easycopy(args) { arraycopy(args.from, args.from_start || 0, // Обратите внимание, как назначаются // значения по умолчанию args.to, args.to_start || 0, args.length); } // Далее следует пример вызова функции easycopy(): var a = [1,2,3,4]; var b = new Array(4); easycopy({from: a, to: b, length: 4}); 8.2.4. Типы аргументов Поскольку JavaScript является слабо типизированным языком, аргументы ме тоды объявляются без указания их типов, а во время передачи значений функ 8.2. Аргументы функций 147 ции не производится никакой проверки их типов. Вы можете сделать свой про граммный код самодокументируемым, выбирая описательные имена для аргу ментов функций и включая указание на тип в комментарии, как это сделано в только что рассмотренном примере с функцией arraycopy(). Для необязатель ных аргументов можно добавлять в комментарий слово «необязательный» («op tional»). А если метод предусматривает возможность принимать произвольное число аргументов, можно использовать многоточие: function max(/* число... */) { /* тело функции */ } Как отмечалось в главе 3, в случае необходимости JavaScript выполняет преоб разование типов. Таким образом, если вы создали функцию, которая ожидает получить строковый аргумент, а затем вызываете ее с аргументом какогонибудь другого типа, значение аргумента просто преобразуется в строку, когда функция пытается обратиться к нему как к строке. В строку может быть преобразован лю бой элементарный тип, и все объекты имеют методы toString() (правда, не всегда полезные), тем самым устраняется вероятность появления ошибки. Однако такой подход может использоваться не всегда. Рассмотрим еще раз ме тод arraycopy(), продемонстрированный ранее. Он ожидает получить массив в первом аргументе. Любое обращение к функции окажется неудачным, если первым аргументом будет не массив (или, возможно, объект, подобный масси ву). Если функция должна вызываться чаще, чем одиндва раза, следует доба вить в нее проверку соответствия типов аргументов. При передаче аргументов ошибочных типов должно генерироваться исключение, которое зафиксирует этот факт. Гораздо лучше сразу же прервать вызов функции в случае передачи аргументов ошибочных типов, чем продолжать исполнение, которое потерпит неудачу, когда, например, функция попытается получить доступ к элементу массива с помощью числового аргумента, как в следующем фрагменте: // Возвращает сумму элементов массива (или объекта, подобного массиву) a. // Все элементы массива должны быть числовыми, при этом значения null // и undefined игнорируются. function sum(a) { if ((a instanceof Array) || // если это массив (a && typeof a == "object" && "length" in a)) { // или объект, подобный массиву var total = 0; for(var i = 0; i < a.length; i++) { var element = a[i]; if (!element) continue; // игнорировать значения null и undefined if (typeof element == "number") total += element; else throw new Error("sum(): все элементы должны быть числами"); } return total; } else throw new Error("sum(): аргумент должен быть массивом"); } Метод sum() весьма строго относится к проверке типов входных аргументов и ге нерирует исключения с достаточно информативными сообщениями, если типы входных аргументов не соответствуют ожидаемым. Тем не менее он остается дос таточно гибким, обслуживая наряду с настоящими массивами объекты, подоб ные массивам, и игнорируя элементы, имеющие значения null и undefined. 148 Глава 8. Функции JavaScript – чрезвычайно гибкий и к тому же слабо типизированный язык, бла годаря чему можно писать функции, которые достаточно терпимо относятся к количеству и типам входных аргументов. Далее приводится метод flexsum(), реализующий такой подход (и, вероятно, являющийся примером другой край ности). Например, он принимает любое число входных аргументов и рекурсивно обрабатывает те из них, которые являются массивами. Вследствие этого он мо жет принимать переменное число аргументов или массив в виде аргумента. Кро ме того, он прилагает максимум усилий, чтобы преобразовать нечисловые аргу менты в числа, прежде чем сгенерировать исключение: function flexisum(a) { var total = 0; for(var i = 0; i < arguments.length; i++) { var element = arguments[i]; if (!element) continue; // Игнорировать значения null и undefined // Попытаться преобразовать аргумент в число n исходя из типа аргумента var n; switch(typeof element) { case "number": n = element; // Преобразование не требуется break; case "object": if (element instanceof Array) // Рекурсивный обход массива n = flexisum.apply(this, element); else n = element.valueOf( ); // Для других объектов вызвать valueOf break; case "function": n = element( ); // Попытаться вызвать функцию break; case "string": n = parseFloat(element); // Попытаться преобразовать строку break; case "boolean": n = NaN; // Логические значения преобразовать невозможно break; } // Если было получено нормально число – добавить его к сумме. if (typeof n == "number" && !isNaN(n)) total += n; // В противном случае сгенерировать исключение else throw new Error("sum(): ошибка преобразования " + element + " в число"); } return total; } 8.3. Функции как данные Самые важные особенности функций заключаются в том, что они могут опреде ляться и вызываться, что было показано в предыдущем разделе. Определение и вызов функций – это синтаксические средства JavaScript и большинства дру гих языков программирования. Однако в JavaScript функции – это не только 8.3. Функции как данные 149 синтаксические конструкции, но и данные, а это означает, что они могут при сваиваться переменным, храниться в свойствах объектов или элементах масси вов, передаваться как аргументы функциями и т. д.1 Чтобы понять, как в JavaScript функции могут быть одновременно синтаксиче скими конструкциями и данными, рассмотрим следующее определение функции: function square(x) { return x*x; } Это определение создает новый объект функции и присваивает его переменной square. Имя функции действительно нематериально – это просто имя перемен ной, содержащей функцию. Функция может быть присвоена другой перемен ной, и при этом работать так же, как и раньше: var a = square(4); // a содержит число 16 var b = square; // b теперь ссылается на ту же функцию, что и square var c = b(5); // c содержит число 25 Функции могут быть также присвоены не только глобальным переменным, но и свойствам объектов. В этом случае их называют методами: var o = new Object; o.square = function(x) { return x*x; }; // функциональный литерал y = o.square(16); // y равно 256 У функций даже не обязательно должны быть имена, например в случае при сваивании их элементам массива: var a = new Array(3); a[0] = function(x) { return x*x; } a[1] = 20; a[2] = a[0](a[1]); // a[2] содержит 400 Синтаксис вызова функции в последнем примере выглядит необычно, однако это вполне допустимый вариант применения оператора () в JavaScript! В примере 8.2 подробно показано, что можно делать, когда функции выступают в качестве данных. Этот пример демонстрирует, каким образом функции могут передаваться другим функциям. Хотя пример может показаться вам несколько сложным, комментарии объясняют, что происходит, и он вполне достоин тща тельного изучения. Пример 8.2. Использование функций как данных // Здесь определяются несколько простых функций function add(x,y) { return x + y; } function subtract(x,y) { return x  y; } function multiply(x,y) { return x * y; } function divide(x,y) { return x / y; } // Эта функция принимает одну из вышеприведенных функций // в качестве аргумента и вызывает ее для двух операндов 1 Это может показаться не столь интересным, если вы незнакомы с такими языка ми, как Java, в которых функции являются частью программы, но не могут про граммой управляться. 150 Глава 8. Функции function operate(operator, operand1, operand2) { return operator(operand1, operand2); } // Вот так можно вызвать эту функцию для вычисления значения выражения (2+3) + (4*5): var i = operate(add, operate(add, 2, 3), operate(multiply, 4, 5)); // Ради примера, мы реализуем эти функции снова, на этот раз с помощью // функциональных литералов внутри литерала объекта. var operators = { add: function(x,y) { return x+y; }, subtract: function(x,y) { return xy; }, multiply: function(x,y) { return x*y; }, divide: function(x,y) { return x/y; }, pow: Math.pow // Для предопределенных функций это тоже работает }; // Эта функция принимает имя оператора, отыскивает оператор в объекте, // а затем вызывает его для переданных операндов. Обратите внимание // на синтаксис вызова функции оператора. function operate2(op_name, operand1, operand2) { if (typeof operators[op_name] == "function") return operators[op_name](operand1, operand2); else throw "неизвестный оператор"; } // Вот так мы можем вызвать эту функцию для вычисления значения // ("hello" + " " + "world"): var j = operate2("add", "hello", operate2("add", " ", "world")) // Используем предопределенную функцию Math.pow(): var k = operate2("pow", 10, 2) Если предыдущий пример не убедил вас в удобстве передачи функций в качестве аргументов другим функциям и других способов использования функций как значений, обратите внимание на функцию Array.sort(). Она сортирует элементы массива. Существует много возможных порядков сортировки (числовой, алфа витный, по датам, по возрастанию, по убыванию и т. д.), поэтому функция sort() принимает в качестве необязательного аргумента другую функцию, которая со общает о том, как выполнять сортировку. Эта функция делает простую работу – получает два элемента массива, сравнивает их, а затем возвращает результат, указывающий, какой из элементов должен быть первым. Этот аргумент функ ции делает метод Array.sort() совершенно универсальным и бесконечно гибким – он может сортировать любой тип данных в любом мыслимом порядке! (Пример использования функции Array.sort() вы найдете в разделе 7.7.3.) 8.4. Функции как методы Метод – это не что иное, как функция, которая хранится в свойстве объекта и вызывается посредством этого объекта. Не забывайте, что функции – это всего лишь значения данных, а в именах, с которыми они определены, нет ничего не обычного. Поэтому функции могут присваиваться любым переменным, равно 8.4. Функции как методы 151 как и свойствам объектов. Например, если имеется функция f и объект o, вполне возможно так определить метод с именем m: o.m = f; Определив в объекте o метод m(), к нему можно обратиться следующим образом: o.m(); Или, если метод m() ожидает получить два аргумента: o.m(x, x+2); Методы обладают одним очень важным свойством: объект, посредством которо го вызывается метод, становится значением ключевого слова this в теле метода. То есть когда вызывается метод o.m(), в теле метода можно получить доступ к объекту o с помощью ключевого слова this. Это утверждение демонстрируется в следующем примере: var calculator = { // Литерал объекта operand1: 1, operand2: 1, compute: function( ) { this.result = this.operand1 + this.operand2; } }; calculator.compute(); // Сколько будет 1+1? print(calculator.result); // Выводит результат Ключевое слово this играет очень важную роль. Любая функция, вызываемая как метод, получает в свое распоряжение дополнительный неявный аргумент – объект, посредством которого эта функция была вызвана. Как правило, методы выполняют некоторые действия над этим объектом, таким образом, синтаксис вызова методов наглядно отражает тот факт, что функция оперирует объектом. Сравните следующие две строки программы: rect.setSize(width, height); setRectSize(rect, width, height); Гипотетически функции, вызывающиеся в этих двух строках, могут произво дить абсолютно идентичные действия над объектом rect (гипотетическим), но синтаксис вызова метода в первой строке более наглядно демонстрирует, что в центре внимания находится объект rect. (Если первая строка не показалась вам более естественной, это означает, что у вас еще нет опыта объектноориенти рованного программирования.) Когда функция вызывается как функция, а не как метод, ключевое слово this ссылается на глобальный объект. Самое странное, что это верно даже для функ ций (если они вызываются как функции), вложенных в методы, которые в свою очередь вызываются как методы. Ключевое слово this имеет одно значение в объ емлющей функции и ссылается на глобальный объект в теле вложенной функ ции (что интуитивно совершенно не очевидно). Обратите внимание: this – это именно ключевое слово, а не имя переменной или свойства. Синтаксис JavaScript не допускает возможность присваивания значе ний элементу this. 152 Глава 8. Функции 8.5. Функцияконструктор Конструктор – это функция, которая выполняет инициализацию свойств объ екта и предназначена для использования совместно с инструкцией new. Подроб ное описание конструкторов приводится в главе 9. Однако коротко можно отме тить, что инструкция new создает новый объект Function, после чего вызывает функциюконструктор, передавая ей вновь созданный объект в качестве значе ния ключевого слова this. 8.6. Свойства и методы функций Мы видели, что в JavaScriptпрограммах функции могут использоваться как значения. Инструкция typeof возвращает для функций строку "function", однако в действительности функции в JavaScript – это особого рода объекты. А раз функции являются объектами, то они имеют свойства и методы – так же как, на пример, объекты RegExp и Date. 8.6.1. Свойство length Как мы видели, в теле функции свойство length массива arguments определяет ко личество аргументов, переданных этой функции. Однако свойство length самой функции имеет другой смысл. Это доступное только для чтения свойство возвра щает количество аргументов, которое функция ожидает получить, т. е. объяв ленных в ее списке параметров. Вспомним, что функция может вызываться с любым количеством аргументов, которые могут быть извлечены через массив arguments, независимо от того, сколько их объявлено. Свойство length объекта Function в точности определяет, сколько объявленных параметров имеется у функции. Обратите внимание: в отличие от свойства arguments.length, указан ное свойство length доступно как внутри, так и вне тела функции. В следующем фрагменте определяется функция с именем check(), получающая массив аргументов от другой функции. Она сравнивает свойство arguments.length со свойством Function.length (доступным как arguments.callee.length) и проверя ет, передано ли функции столько аргументов, сколько она ожидает. Если это не так, генерируется исключение. За функцией check() следует тестовая функция f(), демонстрирующая порядок вызова функции check(): function check(args) { var actual = args.length; // Фактическое число аргументов var expected = args.callee.length; // Ожидаемое число аргументов if (actual != expected) { // Если числа не совпадают, генерируется исключение throw new Error("неверное число аргументов: ожидается: " + expected + "; фактически передано " + actual); } } function f(x, y, z) { // Проверяем, соответствует ли ожидаемому фактическое количество // аргументов. Если не соответствует, генерируем исключение check(arguments); // Теперь выполняем оставшуюся часть функции обычным образом 8.6. Свойства и методы функций 153 return x + y + z; } 8.6.2. Свойство prototype Любая функция имеет свойство prototype, ссылающееся на предопределенный объект'прототип. Этот объект, который вступает в игру, когда функция ис пользуется в качестве конструктора с оператором new, играет важную роль в про цессе определения новых типов объектов. Мы подробно изучим свойство proto type в главе 9. 8.6.3. Определение собственных свойств функций Когда функции требуется переменная, значение которой должно сохраняться между ее вызовами, часто оказывается удобным использовать свойство объекта Function, позволяющее не занимать пространство имен определениями глобаль ных переменных. Предположим, что надо написать функцию, возвращающую уникальный идентификатор при каждом своем вызове. Функция никогда не должна возвращать одно и то же значение дважды. Чтобы обеспечить это, функ ция запоминает последнее возвращенное значение, и эта информация сохраня ется между ее вызовами. Хотя указанная информация может храниться в гло бальной переменной, в этом нет никакой необходимости, и лучше сохранять ее в свойстве объекта Function, т. к. эта информация используется только самой функцией. Вот пример функции, которая возвращает уникальное целое значе ние при каждом вызове: // Создаем и инициализируем "статическую" переменную. // Объявления функций обрабатываются до исполнения кода, // поэтому мы действительно можем выполнить это присваивание // до объявления функции uniqueInteger.counter = 0; // Сама функция. Она возвращает разные значения при каждом // вызове и использует собственное "статическое" свойство // для отслеживания последнего возвращенного значения. function uniqueInteger() { // Наращиваем и возвращаем значение "статической" переменной return uniqueInteger.counter++; } 8.6.4. Методы apply и call() В ECMAScript есть два метода, определенные для всех функций, – call() и apply(). Эти методы позволяют вызывать функцию так, будто она является методом не которого объекта. Первый аргумент методов call() и apply() – это объект, для которого выполняется функция; этот аргумент становится значением ключевого слова this в теле функции. Все оставшиеся аргументы call() – это значения, пере даваемые вызываемой функции. Так, чтобы передать функции f() два числа и вы звать ее как метод объекта o, можно использовать следующий прием: f.call(o, 1, 2); Это аналогично следующим строкам программы: 154 Глава 8. Функции o.m = f; o.m(1,2); delete o.m; Метод apply() похож на метод call(), за исключением того, что передаваемые функции аргументы задаются в виде массива: f.apply(o, [1,2]); Например, чтобы найти наибольшее число в массиве чисел, можно вызвать ме тод apply() для передачи элементов массива функции Math.max(): var biggest = Math.max.apply(null, array_of_numbers); 8.7. Практические примеры функций В этом разделе приводятся примеры нескольких функций для работы с объекта ми и массивами, имеющие практическую ценность. Пример 8.3 содержит функ ции для работы с объектами. Пример 8.3. Функции для работы с объектами // Возвращает массив, содержащий имена перечислимых свойств объекта "o" function getPropertyNames(/* объект */o) { var r = []; for(name in o) r.push(name); return r; } // Копирует перечислимые свойства объекта "from" в объект "to". // Если аргумент "to" равен null, создается новый объект. // Функция возвращает объект "to" или вновь созданный объект. function copyProperties(/* объект */ from, /* необязательный объект */ to) { if (!to) to = {}; for(p in from) to[p] = from[p]; return to; } // Копирует перечислимые свойства объекта "from" в объект "to", // но только те, которые еще не определены в объекте "to". // Это может оказаться необходимым, например, когда объект "from" содержит // значения по умолчанию, которые необходимо скопировать в свойства, // если они еще не были определены в объекте "to". function copyUndefinedProperties(/* объект */ from, /* объект */ to) { for(p in from) { if (!p in to) to[p] = from[p]; } } В следующем примере 8.4 приводятся функции для работы с массивами. Пример 8.4. Функции для работы с массивами // Передать каждый элемент массива "a" заданной функции проверки. // Вернуть массив, хранящий только те элементы, для которых 8.7. Практические примеры функций 155 // функция проверки вернула значение true function filterArray(/* массив */ a, /* функция проверки */ predicate) { var results = []; var length = a.length; // На случай, если функция проверки изменит свойство length! for(var i = 0; i < length; i++) { var element = a[i]; if (predicate(element)) results.push(element); } return results; } // Возвращает массив значений, которые являются результатом передачи // каждого элемента массива функции "f" function mapArray(/* массив */a, /* функция */ f) { var r = []; // результаты var length = a.length; // На случай, если f изменит свойство length! for(var i = 0; i < length; i++) r[i] = f(a[i]); return r; } Наконец, функции из примера 8.5 предназначены для работы с функциями. Фактически они используют и возвращают вложенные функции. Вложенные функции возвращаются способом, получившим некогда название «замыкание». Замыкания, которые могут оказаться сложными для понимания, рассматрива ются в следующем разделе. Пример 8.5. Функции для работы с функциями // Возвращает самостоятельную функцию, которая в свою очередь вызывает // функцию "f" как метод объекта "o". Эта функция может использоваться, // когда возникает необходимость передать в функцию метод. // Если не связать метод с объектом, ассоциация будет утрачена, и метод, // переданный функции, будет вызван как обычная функция. function bindMethod(/* объект */ o, /* функция */ f) { return function() { return f.apply(o, arguments) } } // Возвращает самостоятельную функцию, которая в свою очередь вызывает // функцию "f" с заданными аргументами и добавляет дополнительные // аргументы, передаваемые возвращаемой функции. // (Этот прием иногда называется "currying".) function bindArguments(/* функция */ f /*, начальные аргументы... */) { var boundArgs = arguments; return function() { // Создать массив аргументов. Он будет начинаться с аргументов, // определенных ранее, и заканчиваться аргументами, переданными сейчас var args = []; for(var i = 1; i < boundArgs.length; i++) args.push(boundArgs[i]); for(var i = 0; i < arguments.length; i++) args.push(arguments[i]); // Теперь вызвать функцию с новым списком аргументов return f.apply(this, args); } } 156 Глава 8. Функции 8.8. Область видимости функций и замыкания Как говорилось в главе 4, в JavaScript тело функции исполняется в локальной области видимости, которая отличается от глобальной. В этом разделе рассмат риваются вопросы, связанные с областью видимости, включая замыкания.1 8.8.1. Лексическая область видимости Функции в JavaScript имеют не динамическую, а лексическую область видимо сти. Это означает, что они исполняются в области видимости, которая была соз дана на момент определения функции, а не на момент ее исполнения. В момент определения функции текущая цепочка областей видимости сохраняется и ста новится частью внутреннего состояния функции. На верхнем уровне область ви димости просто состоит из глобального объекта, и о лексической области види мости говорить не приходится. Однако когда объявляется вложенная функция, ее цепочка областей видимости включает объемлющую функцию. Это означает, что вложенная функция обладает возможностью доступа ко всем аргументам и локальным переменным объемлющей функции. Обратите внимание: несмотря на то, что цепочка областей видимости фиксиру ется в момент определения функции, перечень свойств, определенных в этой це почке, не фиксируется. Цепочка областей видимости подвержена изменениям, и функция может обращаться ко всем элементам, существующим на момент ис полнения. 8.8.2. Объект вызова Когда интерпретатор JavaScript вызывает функцию, в первую очередь он уста навливает область видимости в соответствии с цепочкой областей видимости, ко торая действовала на момент определения функции. Затем он добавляет в нача ло цепочки новый объект, известный как объект вызова – в спецификации ECMAScript используется термин объект активации (activation object). В объ ект вызова добавляется свойство arguments, которое ссылается на объект Argu ments функции. После этого в объект вызова добавляются именованные аргумен ты функции. Любые локальные переменные, объявленные с помощью инструк ции var, также определяются внутри объекта. Поскольку данный объект вызова располагается в начале цепочки областей видимости, все локальные перемен ные, аргументы функции и объект Arguments становятся видимыми из тела функ ции. Помимо всего прочего это означает, что все одноименные свойства оказыва ются за пределами области видимости. Обратите внимание: this, в отличие от arguments, – это не свойство объекта вызо ва, а ключевое слово. 8.8.3. Объект вызова как пространство имен Иногда бывает удобно создать функцию только ради того, чтобы получить объ ект вызова, который действует как временное пространство имен, где можно оп 1 Этот раздел содержит материал повышенной сложности, который при первом прочтении можно пропустить. 8.8. Область видимости функций и замыкания 157 ределять переменные и свойства, не беспокоясь о возможных конфликтах с гло бальным пространством имен. Предположим, для примера, что имеется файл с программным кодом на языке JavaScript, который необходимо использовать в разных JavaScriptпрограммах (или, если дело касается клиентского языка JavaScript, на разных вебстраницах). Допустим, что в этом коде, как и любом другом, определяются переменные, предназначенные для хранения промежу точных результатов вычислений. Проблема заключается в следующем: посколь ку этот код будет использоваться в разных программах, в нем могут определять ся переменные с именами, конфликтующими с именами, определяемыми в са мих программах. Чтобы избежать подобных конфликтов, импортируемый код можно поместить внутрь функции и затем обращаться к ней. Благодаря этому переменные будут определяться внутри объекта вызова функции: function init( ) { // Здесь располагается импортируемый программный код. // Любые объявленные переменные станут свойствами объекта вызова, // тем самым будет исключена вероятность конфликтов // с глобальным пространством имен. } init( ); // Не забудьте вызвать функцию! Этот фрагмент добавляет единственное свойство в глобальное пространство имен – свойство init, которое ссылается на функцию. Если даже добавление единствен ного свойства покажется вам излишним, можно определить и вызвать аноним ную функцию в одном выражении. Вот фрагмент, который работает именно та ким образом: (function( ) { // Это безымянная функция. // Здесь располагается импортируемый программный код. Любые // объявленные переменные станут свойствами объекта вызова, тем самым // исключается вероятность конфликтов с глобальным пространством имен. })(); // конец функционального литерала и его вызов. Обратите внимание на круглые скобки, окружающие функциональный лите рал, – этого требует синтаксис JavaScript. 8.8.4. Вложенные функции в качестве замыканий Тот факт, что JavaScript допускает объявление вложенных функций, позволяет использовать функции как обычные данные и способствует организации взаи модействий между цепочками областей видимости, что позволяет получать ин тересные и мощные эффекты. Прежде чем приступить к описанию, рассмотрим функцию g, которая определяется внутри функции f. Когда вызывается функ ция f, ее цепочка областей видимости содержит объект вызова, за которым сле дует глобальный объект. Функция g определяется внутри функции f, таким об разом, цепочка областей видимости этой функции сохраняется как часть опре деления функции g. Когда вызывается функция g, ее цепочка областей видимо сти содержит уже три объекта: собственный объект вызова, объект вызова функции f и глобальный объект. 158 Глава 8. Функции Порядок работы вложенных функций совершенно понятен, когда они вызыва ются в той же лексической области видимости, в которой определены. Напри мер, следующий фрагмент не содержит ничего необычного: var x = "глобальная"; function f( ) { var x = "локальная"; function g() { alert(x); } g(); } f(); // При обращении к этой функции будет выведено слово "локальная" Однако в JavaScript функции рассматриваются как обычные данные, поэтому их можно возвращать из других функций, присваивать свойствам объектов, со хранять в массивах и т. д. В этом нет ничего необычного до тех пор, пока на аре ну не выходят вложенные функции. Рассмотрим следующий фрагмент, где оп ределена функция, которая возвращает вложенную функцию. При каждом об ращении к ней она возвращает функцию. Сам JavaScriptкод при этом не меня ется, но от вызова к вызову может изменяться область видимости, поскольку при каждом обращении к объемлющей функции могут изменяться ее аргумен ты. (То есть в цепочке областей видимости будет изменяться объект вызова объ емлющей функции.) Если попробовать сохранить возвращаемые функции в мас сиве и затем вызвать каждую из них, можно заметить, что они будут возвращать разные значения. Поскольку программный код функции при этом не изменяет ся и каждая из них вызывается в той же самой области видимости, единствен ное, чем можно объяснить разницу, – это различия между областями видимо сти, в которых функции были определены: // Эта функция возвращает другую функцию // От вызова к вызову изменяется область видимости, // в которой была определена вложенная функция function makefunc(x) { return function() { return x; } } // Вызвать makefunc() несколько раз и сохранить результаты в массиве: var a = [makefunc(0), makefunc(1), makefunc(2)]; // Теперь вызвать функции и вывести полученные от них значения. // Хотя тело каждой функции остается неизменным, их области видимости // изменяются, и при каждом вызове они возвращают разные значения: alert(a[0]( )); // Выведет 0 alert(a[1]( )); // Выведет 1 alert(a[2]( )); // Выведет 2 Результаты работы этого фрагмента в точности соответствуют ожиданиям, если строго следовать правилу лексической области видимости: функция исполняет ся в той области видимости, в которой она была определена. Однако самое инте ресное состоит в том, что области видимости продолжают существовать и после выхода из объемлющей функции. В обычной ситуации этого не происходит. Ко гда вызывается функция, создается объект вызова и размещается в ее области видимости. Когда функция завершает работу, объект вызова удаляется из це почки вызова. Пока дело не касается вложенных функций, цепочка видимости 8.8. Область видимости функций и замыкания 159 является единственной ссылкой на объект вызова. Когда ссылка на объект уда ляется из цепочки, в дело вступает сборщик мусора. Однако ситуация меняется с появлением вложенных функций. Когда создается определение вложенной функции, оно содержит ссылку на объект вызова, по скольку этот объект находится на вершине цепочки областей видимости, в кото рой определяется функция. Если вложенная функция используется только внутри объемлющей функции, единственная ссылка на вложенную функцию – это объект вызова. Когда внешняя функция возвращает управление, вложенная функция ссылается на объект вызова, а объект вызова – на вложенную функ цию, и никаких других ссылок на них не существует, благодаря этому они ста новятся доступными для механизма сборки мусора. Все меняется, если ссылка на вложенную функцию сохраняется в глобальной области видимости. Это происходит, когда вложенная функция передается в ви де возвращаемого значения объемлющей функции или сохраняется в виде свой ства какоголибо другого объекта. В этом случае появляется внешняя ссылка на вложенную функцию, при этом вложенная функция продолжает ссылаться на объект вызова объемлющей функции. В результате все объекты вызова, создан ные при каждом таком обращении к объемлющей функции, продолжают свое существование, а вместе с ними продолжают существование имена и значения аргументов функции и локальных переменных. JavaScriptпрограммы не имеют возможности напрямую воздействовать на объект вызова, но его свойства явля ются частью цепочки областей видимости, создаваемой при любом обращении к вложенной функции. (Примечательно, что если объемлющая функция сохра нит глобальные ссылки на две вложенные функции, эти вложенные функции будут совместно использовать один и тот же объект вызова, а изменения, по явившиеся в результате обращения к одной из функций, будут видимы в другой.) Функции в JavaScript представляют собой комбинацию исполняемого про граммного кода и области видимости, в которой этот код исполняется. Такая комбинация программного кода и области видимости в литературе по компью терной тематике называется замыканием (closure). Все JavaScriptфункции яв ляются замыканиями. Однако все эти замыкания представляют интерес лишь в только что рассмотренной ситуации, когда вложенная функция экспортирует ся за пределы области видимости, в которой она была определена. Вложенные функции, используемые таким образом, нередко явно называют замыканиями. Замыкания – это очень интересная и мощная техника программирования. Хотя замыкания используются довольно редко, они достойны того, чтобы изучить их. Если вы поймете механизм замыканий, вы без труда разберетесь в областях ви димости и без ложной скромности сможете назвать себя опытным программи стом на JavaScript. 8.8.4.1. Примеры замыканий Иногда возникает необходимость, чтобы функция запоминала некоторое значе ние между вызовами. Значение не может сохраняться в локальной переменной, поскольку между обращениями к функции не сохраняется сам объект вызова. С ситуацией поможет справиться глобальная переменная, но это приводит к за хламлению пространства имен. В разделе 8.6.3 была представлена функция uniqueInteger(), которая задействует для этих целей собственное свойство. Одна 160 Глава 8. Функции ко можно пойти дальше и для создания частной (private) неисчезающей пере менной использовать замыкание. Вот пример такой функции, для начала без за мыкания: // При каждом вызове возвращает разные значения uniqueID = function() { if (!arguments.callee.id) arguments.callee.id = 0; return arguments.callee.id++; }; Проблема заключается в том, что свойство uniqueID.id доступно за пределами функции и может быть установлено в значение 0, вследствие чего будет наруше но соглашение, по которому функция обязуется никогда не возвращать одно и то же значение дважды. Для решения этой проблемы можно сохранять значение в замыкании, доступ к которому будет иметь только эта функция: uniqueID = (function() { // Значение сохраняется в объекте вызова функции var id = 0; // Это частная переменная, сохраняющая свое // значение между вызовами функции // Внешняя функция возвращает вложенную функцию, которая имеет доступ // к этому значению. Эта вложенная функция сохраняется // в переменной uniqueID выше. return function() { return id++; }; // Вернуть и увеличить })(); // Вызов внешней функции после ее определения. Пример 8.6 – это еще один пример замыкания. В нем демонстрируется, как част ные переменные, подобные той, что была показана ранее, могут совместно ис пользоваться несколькими функциями. Пример 8.6. Создание частных свойств с помощью замыканий // Эта функция добавляет методы доступа к свойству объекта "o" // с заданными именами. Методы получают имена get // и set. Если дополнительно предоставляется // функция проверки, метод записи будет использовать ее // для проверки значения перед сохранением. Если функция проверки // возвращает false, метод записи генерирует исключение. // // Необычность такого подхода заключается в том, что значение // свойства, доступного методам, сохраняется не в виде свойства // объекта "o", а в виде локальной переменной этой функции. // Кроме того, методы доступа определены локально, в этой функции // и обеспечивают доступ к этой локальной переменной. // Примечательно, что значение доступно только для этих двух методов // и не может быть установлено или изменено иначе, как методом записи. function makeProperty(o, name, predicate) { var value; // This is the property value // Метод чтения просто возвращает значение. o["get" + name] = function() { return value; }; // Метод записи сохраняет значение или генерирует исключение, // если функция проверки отвергает это значение. o["set" + name] = function(v) { if (predicate && !predicate(v)) throw "set" + name + ": неверное значение " + v; 8.8. Область видимости функций и замыкания 161 else value = v; }; } // Следующий фрагмент демонстрирует работу метода makeProperty(). var o = {}; // Пустой объект // Добавить методы доступа к свойству с именами getName() и setName() // Обеспечить допустимость только строковых значений makeProperty(o, "Name", function(x) { return typeof x == "string"; }); o.setName("Frank"); // Установить значение свойства print(o.getName( )); // Получить значение свойства o.setName(0); // Попытаться установить значение ошибочного типа Самый практичный и наименее искусственный пример использования замыка ний, который мне известен, – это механизм точек останова, разработанный Сти вом Йеном (Steve Yen) и опубликованный на сайте http://trimpath.com как часть клиентской платформы TrimPath. Точка останова – это точка внутри функции, где останавливается исполнение программы, и разработчик получает возмож ность просмотреть значения переменных, вычислить выражения, вызвать функ ции и тому подобное. В механизме точек останова, придуманном Стивом, замы кания служат для хранения контекста исполнения текущей функции (включая локальные переменные и входные аргументы) и с помощью глобальной функ ции eval() позволяют просмотреть содержимое этого контекста. Функция eval() исполняет строки на языке JavaScript и возвращает полученные значения (по дробнее об этой функции можно прочитать в третьей части книги). Вот пример вложенной функции, которая работает как замыкание, выполняющее проверку своего контекста исполнения: // Запомнить текущий контекст и позволить проверить его // с помощью функции eval( ) var inspector = function($) { return eval($); } В качестве имени аргумента эта функция использует малораспространенный идентификатор $, чем снижается вероятность конфликта имен в инспектируе мой области видимости. Создать точку останова можно, передав это замыкание в функцию, как показано в примере 8.7. Пример 8.7. Точки останова на основе замыканий // Эта функция является реализацией точки останова. Она предлагает // пользователю ввести выражение, вычисляет его с использованием // замыкания и выводит результат. Используемое замыкание предоставляет // доступ к проверяемой области видимости, таким образом любая функция // будет создавать собственное замыкание. // // Реализовано по образу и подобию функции breakpoint() Стива Йена // http://trimpath.com/project/wiki/TrimBreakpoint function inspect(inspector, title) { var expression, result; // Существует возможность отключать точки останова 162 Глава 8. Функции // за счет создания свойства "ignore" у этой функции. if ("ignore" in arguments.callee) return; while(true) { // Определить, как вывести запрос перед пользователем var message = ""; // Если задан аргумент title, вывести его первым if (title) message = title + "\n"; // Если выражение уже вычислено, вывести его вместе с его значением if (expression) message += "\n"+expression+" ==> "+result+"\n"; else expression = ""; // Типовое приглашение к вводу всегда должно выводиться: message += "Введите выражение, которое следует вычислить:"; // Получить ввод пользователя, вывести приглашение и использовать // последнее выражение как значение по умолчанию. expression = prompt(message, expression); // Если пользователь ничего не ввел (или щелкнул на кнопке Отменить), // работу в точке останова можно считать оконченной // и вернуть управление. if (!expression) return; // В противном случае вычислить выражение с использованием // замыкания в инспектируемом контексте исполнения. // Результаты будут выведены на следующей итерации. result = inspector(expression); } } Обратите внимание: для вывода информации и ввода строки пользователя функ ция inspect() из примера 8.7 задействует метод Window.prompt() (подробнее об этом методе рассказывается в четвертой части книги). Рассмотрим пример функции, вычисляющей факториал числа и использующей механизм точек останова: function factorial(n) { // Создать замыкание для этой функции var inspector = function($) { return eval($); } inspect(inspector, "Вход в функцию factorial()"); var result = 1; while(n > 1) { result = result * n; n; inspect(inspector, "factorial( ) loop"); } inspect(inspector, "Выход из функции factorial()"); return result; } 8.8.4.2. Замыкания и утечки памяти в Internet Explorer В вебброузере Microsoft Internet Explorer используется достаточно слабая разно видность механизма сборки мусора для объектов ActiveX и DOMэлементов на 8.9. Конструктор Function() 163 стороне клиента. Для этих элементов на стороне клиента выполняется подсчет ссылок, и они утилизируются интерпретатором, только когда значение счетчика ссылок достигает нуля. Однако такая схема оказывается неработоспособной в случае циклических ссылок, например, когда базовый JavaScriptобъект ссыла ется на элемент документа, а этот элемент имеет свойство (например, обработчик события), которое, в свою очередь, хранит ссылку на базовый JavaScriptобъект. Такого рода циклические ссылки часто возникают при работе с замыканиями. При использовании техники замыканий не забывайте, что объект вызова замк нутой функции, включающий в себя все аргументы функции и локальные пере менные, будет продолжать существовать до тех пор, пока существует само замы кание. Если какиелибо аргументы функции или локальные переменные ссыла ются на объект, может возникнуть утечка памяти. Обсуждение этой проблемы выходит за рамки темы книги. За дополнительной информацией обращайтесь по адресу: http://msdn.microsoft.com/library/en'us/ IETechCol/dnwebgen/ie_leak_patterns.asp. 8.9. Конструктор Function() Как уже говорилось ранее, функции обычно определяются с помощью ключево го слова function либо в форме определения функции, либо посредством функ ционального литерала. Однако помимо этого существует возможность создания функций с помощью конструктора Function(). Создание функций с помощью конструктора Function() обычно сложнее, чем с помощью функционального ли терала, поэтому такая методика распространена не так широко. Вот пример соз дания функции подобным образом: var f = new Function("x", "y", "return x*y;"); Эта строка создает новую функцию, более или менее эквивалентную функции, определенной с помощью более привычного синтаксиса: function f(x, y) { return x*y; } Конструктор Function() принимает любое количество строковых аргументов. По следний аргумент – это тело функции. Он может содержать произвольные Java Scriptинструкции, отделенные друг от друга точками с запятой. Все остальные аргументы конструктора представляют собой строки, задающие имена парамет ров определяемой функции. Если вы определяете функцию без аргументов, кон структору передается только одна строка – тело функции. Обратите внимание: конструктору Function() не передается аргумент, задающий имя создаваемой им функции. Неименованные функции, созданные с помощью конструктора Function(), иногда называются анонимными функциями. Есть несколько моментов, связанных с конструктором Function(), о которых сле дует упомянуть особо: • Конструктор Function() позволяет динамически создавать и компилировать функции во время исполнения программы. В чемто он напоминает функцию eval() (за информацией обращайтесь к третьей части книги). 164 Глава 8. Функции • Конструктор Function() компилирует и создает новую функцию при каждом вызове. Если вызов конструктора производится в теле цикла или часто вызы ваемой функции, это может отрицательно сказаться на производительности программы. В противовес этому функциональные литералы или вложенные функции, находящиеся внутри цикла, не перекомпилируются на каждой итерации, а кроме того, в случае литералов не создается новый объект функ ции. (Хотя, как уже отмечалось ранее, может создаваться новое замыкание, хранящее лексический контекст, в котором была определена функция.) • И последний очень важный момент: когда функция создается с помощью конструктора Function(), не учитывается текущая лексическая область види мости – функции, созданные таким способом, всегда компилируются как функции верхнего уровня, что наглядно демонстрируется в следующем фраг менте: var y = "глобальная"; function constructFunction() { var y = "локальная"; return new Function("return y"); // Не сохраняет локальный контекст! } // Следующая строка выведет слово "глобальная", потому что функция, // созданная конструктором Function(), не использует локальный контекст. // Если функция была определена как литерал, // эта строка вывела бы слово "локальная". alert(constructFunction()()); // Выводит слово "глобальная" Классы, конструкторы и прототипы Введение в JavaScriptобъекты было дано в главе 7, в которой каждый объект трактовался как уникальный набор свойств, отличающих его от любых других объектов. В большинстве объектноориентированных языков программирования существует возможность определять классы объектов и затем создавать отдель ные объекты как экземпляры этих классов. Например, можно объявить класс Complex, призванный представлять комплексные числа и выполнять арифметиче ские действия с этими числами, тогда объект Complex представлял бы единствен ное комплексное число и мог бы создаваться как экземпляр этого класса. Язык JavaScript не обладает полноценной поддержкой классов, как другие язы ки, например Java, C++ или C#.1 Тем не менее в JavaScript существует возмож ность определять псевдоклассы с помощью таких инструментальных средств, как функцииконструкторы и прототипы объектов. В этой главе рассказывается о конструкторах и прототипах и приводится ряд примеров некоторых псевдо классов и даже псевдоподклассов JavaScript. За неимением лучшего термина в этой главе неофициально я буду пользоваться словом «класс», поэтому будьте внимательны и не путайте эти неформальные «классы» с настоящими классами, которые поддерживаются в JavaScript 2.0 и в других языках программирования. 9.1. Конструкторы В главе 7 демонстрировался порядок создания новых пустых объектов как с по мощью литерала {}, так и с помощью следующего выражения: new Object() Кроме того, была продемонстрирована возможность создания объектов других типов примерно следующим образом: 1 Полноценную поддержку классов планируется реализовать в JavaScript 2.0. 166 Глава 9. Классы, конструкторы и прототипы var array = new Array(10); var today = new Date( ); За оператором new должно быть указано имя функцииконструктора. Оператор создает новый пустой объект без какихлибо свойств, а затем вызывает функ цию, передавая ей только что созданный объект в виде значения ключевого слова this. Функция, применяемая совместно с оператором new, называется функцией' конструктором, или просто конструктором. Главная задача конструктора за ключается в инициализации вновь созданного объекта – установке всех его свойств, которые необходимо инициализировать до того, как объект сможет ис пользоваться программой. Чтобы определить собственный конструктор, доста точно написать функцию, добавляющую новые свойства к объекту, на который ссылается ключевое слово this. В следующем фрагменте приводится определе ние конструктора, с помощью которого затем создаются два новых объекта: // Определяем конструктор. // Обратите внимание, как инициализируется объект с помощью "this". function Rectangle(w, h) { this.width = w; this.height = h; } // Вызываем конструктор для создания двух объектов Rectangle. Мы передаем ширину и высоту // конструктору, чтобы можно было правильно проинициализировать оба новых объекта. var rect1 = new Rectangle(2, 4); // rect1 = { width:2, height:4 }; var rect2 = new Rectangle(8.5, 11); // rect2 = { width:8.5, height:11 }; Обратите внимание на то, как конструктор использует свои аргументы для ини циализации свойств объекта, на который ссылается ключевое слово this. Здесь мы определили класс объектов, просто создав соответствующую функциюкон структор – все объекты, созданные с помощью конструктора Rectangle(), гаран тированно будут иметь инициализированные свойства width и height. Это означа ет, что учитывая данное обстоятельство, можно организовать единообразную ра боту со всеми объектами класса Rectangle. Поскольку каждый конструктор опре деляет отдельный класс объектов, стилистически очень важно присвоить такое имя функцииконструктору, которое будет явно отражать класс объектов, созда ваемых с ее помощью. Например, строка new Rectangle(1, 2), создающая объект прямоугольника, выглядит гораздо более понятно, нежели new init_rect(1, 2). Обычно функцииконструкторы ничего не возвращают, они лишь инициализи руют объект, полученный в качестве значения ключевого слова this. Однако для конструкторов допускается возможность возвращать объект, в этом случае возвра щаемый объект становится значением выражения new. При этом объект, передан ный конструктору в виде значения ключевого слова this, просто уничтожается. 9.2. Прототипы и наследование В главе 8 говорилось, что метод – это функция, которая вызывается как свойст во объекта. Когда функция вызывается таким способом, объект, посредством ко торого производится вызов, становится значением ключевого слова this. Пред положим, что необходимо рассчитать площадь прямоугольника, представленно го объектом Rectangle. Вот один из возможных способов: function computeAreaOfRectangle(r) { return r.width * r.height; } 9.2. Прототипы и наследование 167 Эта функция прекрасно справляется с возложенными на нее задачами, но она не является объектноориентированной. Работая с объектом, лучше всего вызывать методы этого объекта, а не передавать объекты посторонним функциям в качест ве аргументов. Этот подход демонстрируется в следующем фрагменте: // Создать объект Rectangle var r = new Rectangle(8.5, 11); // Добавить к объекту метод r.area = function() { return this.width * this.height; } // Теперь рассчитать площадь, вызвав метод объекта var a = r.area(); Конечно же, не совсем удобно добавлять новый метод к объекту перед его ис пользованием. Однако ситуацию можно улучшить, если инициализировать свойство area в функцииконструкторе. Вот как выглядит улучшенная реализа ция конструктора Rectangle(): function Rectangle(w, h) { this.width = w; this.height = h; this.area = function( ) { return this.width * this.height; } } С новой версией конструктора тот же самый алгоритм можно реализовать по другому: // Найти площадь листа бумаги формата U.S. Letter в квадратных дюймах var r = new Rectangle(8.5, 11); var a = r.area(); Такое решение выглядит гораздо лучше, но оно попрежнему не является опти мальным. Каждый созданный прямоугольник будет иметь три свойства. Свойст ва width и height могут иметь уникальные значения для каждого прямоугольни ка, но свойство area каждого отдельно взятого объекта Rectangle всегда будет ссылаться на одну и ту же функцию (разумеется, это свойство можно изменить в процессе работы, но, как правило, предполагается, что методы объекта не должны меняться). Применение отдельных свойств для хранения методов объ ектов, которые могли бы совместно использоваться всеми экземплярами одного и того же класса, – это достаточно неэффективное решение. Однако и эту проблему можно решить. Оказывается, все объекты в JavaScript содержат внутреннюю ссылку на объект, известный как прототип. Любые свой ства прототипа становятся свойствами другого объекта, для которого он являет ся прототипом. То есть, говоря другими словами, любой объект в JavaScript на' следует свойства своего прототипа. В предыдущем разделе было показано, как оператор new создает пустой объект и затем вызывает функциюконструктор. Но история на этом не заканчивается. После создания пустого объекта оператор new устанавливает в этом объекте ссыл ку на прототип. Прототипом объекта является значение свойства prototype функцииконструктора. Все функции имеют свойство prototype, которое ини циализируется в момент определения функции. Начальным значением этого свойства является объект с единственным свойством. Это свойство называется constructor и значением его является ссылка на саму функциюконструктор, 168 Глава 9. Классы, конструкторы и прототипы с которой этот прототип ассоциируется. (Описание свойства constructor приводи лось в главе 7, здесь же объясняется, почему каждый объект обладает свойством constructor.) Любые свойства, добавленные к прототипу, автоматически стано вятся свойствами объектов, инициализируемых конструктором. Более понятно это можно объяснить на примере. Вот новая версия конструктора Rectangle(): // Функцияконструктор инициализирует те свойства, которые могут // иметь уникальные значения для каждого отдельного экземпляра. function Rectangle(w, h) { this.width = w; this.height = h; } // Прототип объекта содержит методы и другие свойства, которые должны // совместно использоваться всеми экземплярами этого класса. Rectangle.prototype.area = function() { return this.width * this.height; } Конструктор определяет «класс» объектов и инициализирует свойства, такие как width и height, которые могут отличаться для каждого экземпляра класса. Объект прототип связан с конструктором, и каждый объект, инициализируемый конст руктором, наследует тот набор свойств, который имеется в прототипе. Это значит, что объектпрототип – идеальное место для методов и других свойствконстант. Обратите внимание, что наследование осуществляется автоматически как часть процесса поиска значения свойства. Свойства не копируются из объектапрото типа в новый объект; они просто присутствуют, как если бы были свойствами этих объектов. Это имеет два важных следствия. Вопервых, использование объ ектовпрототипов может в значительной степени уменьшить объем памяти, тре буемый для каждого объекта, т. к. объекты могут наследовать многие из своих свойств. Вовторых, объект наследует свойства, даже если они были добавлены в прототип после создания объекта. Это означает наличие возможности добав лять новые методы к существующим классам (хотя это не совсем правильно). Унаследованные свойства ничем не отличаются от обычных свойств объекта. Они поддаются перечислению в цикле for/in и могут проверяться с помощью опе ратора in. Отличить их можно только с помощью метода Object.hasOwnProperty(): var r = new Rectangle(2, 3); r.hasOwnProperty("width"); // true: width – непосредственное свойство "r" r.hasOwnProperty("area"); // false: area – унаследованное свойство "r" "area" in r; // true: area – свойство объекта "r" 9.2.1. Чтение и запись унаследованных свойств У каждого класса имеется один объектпрототип с одним наборов свойств, но по тенциально может существовать множество экземпляров класса, каждый из кото рых наследует свойства прототипа. Свойство прототипа может наследоваться мно гими объектами, поэтому интерпретатор JavaScript должен обеспечить фундамен тальную асимметричность между чтением и записью значений свойств. Когда вы читаете свойство p объекта o, JavaScript сначала проверяет, есть ли у объекта o свойство с именем p. Если такого свойства нет, то проверяется, есть ли свойство с именем p в объектепрототипе. Так работает наследование на базе прототипов. 9.2. Прототипы и наследование 169 Однако когда свойству присваивается значение, JavaScript не использует объ ектпрототип. Чтобы понять почему, подумайте, что произошло бы в этом слу чае: предположим, вы пытаетесь установить значение свойства o.p, а у объекта o нет свойства с именем p. Предположим теперь, что JavaScript идет дальше и ищет свойство p в объектепрототипе объекта o и позволяет вам изменить зна чение свойства прототипа. В результате вы изменяете значение p для всего клас са объектов, а это вовсе не то, что требовалось. Поэтому наследование свойств происходит только при чтении значений свойств, но не при их записи. Если вы устанавливаете свойство p в объекте o, который на следует это свойство от своего прототипа, происходит создание нового свойства непосредственно в объекте p. Теперь, когда объект o имеет собственное свойство с именем p, он больше не наследует значение p от прототипа. Когда вы читаете значение p, JavaScript сначала ищет его в свойствах объекта o. Так как он нахо дит свойство p, определенное в o, ему не требуется искать его в объектепрототи пе, и JavaScript никогда не будет искать определенное в нем значение p. Мы ино гда говорим, что свойство p «затеняет» (скрывает) свойство p объектапрототипа. Наследование прототипов может показаться запутанным, но все вышеизложен ное хорошо иллюстрирует рис. 9.1. Рис. 9.1. Объекты и прототипы 170 Глава 9. Классы, конструкторы и прототипы Свойства прототипа совместно используются всеми объектами класса, поэтому, как правило, их имеет смысл применять только для определения свойств, совпа дающих для всех объектов класса. Это делает прототипы идеальными для опре деления методов. Другие свойства с постоянными значениями (такими как ма тематические константы) также подходят для определения в качестве свойств прототипа. Если класс определяет свойство с очень часто используемым значе нием по умолчанию, то можно определить это свойство и его значение по умол чанию в объектепрототипе. Тогда те немногие объекты, которые хотят изме нить значение по умолчанию, могут создавать свои частные копии свойства и оп ределять собственные значения, отличные от значения по умолчанию. 9.2.2. Расширение встроенных типов Не только классы, определенные пользователем, имеют объектыпрототипы. Встроенные классы, такие как String и Date, также имеют объектыпрототипы, и вы можете присваивать им значения. Например, следующий фрагмент опреде ляет новый метод, доступный всем объектам String: // Возвращает true, если последним символом является значение аргумента c String.prototype.endsWith = function(c) { return (c == this.charAt(this.length1)) } Определив новый метод endsWith() в объектепрототипе String, мы сможем обра титься к нему следующим образом: var message = "hello world"; message.endsWith('h') // Возвращает false message.endsWith('d') // Возвращает true Против такого расширения возможностей встроенных типов можно привести достаточно сильные аргументы: в случае расширения некоторого встроенного типа, по сути, создается самостоятельная версия базового прикладного Java Scriptинтерфейса. Любые другие программисты, которые будут читать или со провождать ваш код, могут прийти в недоумение, встретив методы, о которых они ранее не слышали. Если только вы не собираетесь создавать низкоуровне вую JavaScriptплатформу, которая будет воспринята многими другими про граммистами, лучше оставить прототипы встроенных объектов в покое. Обратите внимание: никогда не следует добавлять свойства к объекту Object.pro totype. Любые добавляемые свойства и методы становятся перечислимыми для цикла for/in, поэтому добавив их к объекту Object.prototype, вы сделаете их до ступными во всех JavaScriptобъектах. Пустой объект {}, как предполагается, не имеет перечислимых свойств. Любое расширение Object.prototype превратит ся в перечислимое свойство пустого объекта, что, скорее всего, приведет к нару шениям в функционировании программного кода, который работает с объекта ми как с ассоциативными массивами. Техника расширения встроенных объектов, о которой сейчас идет речь, гаранти рованно работает только в случае применения к «родным» объектам базового языка JavaScript. Когда JavaScript встраивается в некоторый контекст, напри мер в вебброузер или в Javaприложение, он получает доступ к дополнитель ным «платформозависимым» объектам, таким как объекты вебброузера, пред 9.2. Прототипы и наследование 171 ставляющие содержимое документа. Эти объекты, как правило, не имеют ни конструктора, ни прототипа и потому недоступны для расширения. Один из случаев, когда можно расширять прототипы встроенных объектов дос таточно безопасно и даже желательно, – это добавление стандартных методов прототипов в старых несовместимых реализациях JavaScript, где эти свойства и методы отсутствуют. Например, метод Function.apply() в Microsoft Internet Exp lorer версий 4 и 5 не поддерживается. Это достаточно важная функция, поэтому иногда вам может встретиться код, который добавляет эту функцию: // Если функция Function.apply() не реализована, можно добавить // этот фрагмент, основанный на разработках Аарона Будмана (Aaron Boodman). if (!Function.prototype.apply) { // Вызвать эту функцию как метод заданного объекта с указанными // параметрами. Для этих целей здесь используется функция eval() Function.prototype.apply = function(object, parameters) { var f = this; // Вызываемая функция var o = object || window; // Объект, через который выполняется вызов var args = parameters || []; // Передаваемые аргументы // Временно превратить функцию в метод объекта o. // Для этого выбирается имя метода, которое скорее всего отсутствует o._$_apply_$_ = f; // Вызов метода выполняется с помощью eval(). // Для этого необходимо сконструировать строку вызова. // В первую очередь собирается список аргументов. var stringArgs = []; for(var i = 0; i < args.length; i++) stringArgs[i] = "args[" + i + "]"; // Объединить строки с аргументами в единый список, // разделив аргументы запятыми. var arglist = stringArgs.join(","); // Теперь собрать всю строку вызова метода var methodcall = "o._$_apply_$_(" + arglist + ");"; // С помощью функции eval() вызвать метод var result = eval(methodcall); // Удалить метод из объекта delete o._$_apply_$_; // И вернуть результат return result; }; } В качестве еще одного примера рассмотрим новые методы массивов, реализован ные в Firefox 1.5 (см. раздел 7.7.10). Если необходимо задействовать метод Ar ray.map() и при этом желательно сохранить совместимость с платформами, где этот метод не поддерживается, можно воспользоваться следующим фрагментом: // Array.map() вызывает функцию f для каждого элемента массива // и возвращает новый массив, содержащий результаты каждого вызова функции. // Если map() вызывается с двумя аргументами, функция f вызывается как метод 172 Глава 9. Классы, конструкторы и прототипы // второго аргумента. Функции f() передается 3 аргумента. Первый представляет // значение элемента массива, второй – индекс элемента, третий – сам массив. // В большинстве случаев достаточно передать только первый аргумент. if (!Array.prototype.map) { Array.prototype.map = function(f, thisObject) { var results = []; for(var len = this.length, i = 0; i < len; i++) { results.push(f.call(thisObject, this[i], i, this)); } return results; } } 9.3. Объектноориентированный язык JavaScript Хотя JavaScript поддерживает тип данных, который мы называем объектом, в нем нет формального понятия класса. Это в значительной степени отличает его от классических объектноориентированных языков программирования, таких как C++ и Java. Общая черта объектноориентированных языков – это их стро гая типизация и поддержка механизма наследования на базе классов. По этому критерию JavaScript легко исключить из числа истинно объектноориентиро ванных языков. С другой стороны, мы видели, что JavaScript активно использу ет объекты и имеет особый тип наследования на базе прототипов. JavaScript – это истинно объектноориентированный язык. Он был реализован под влиянием некоторых других (относительно малоизвестных) объектноориентированных языков, в которых вместо наследования на основе классов реализовано наследо вание на базе прототипов. Несмотря на то что JavaScript – это объектноориентированный язык, не бази рующийся на классах, он неплохо имитирует возможности языков на базе клас сов, таких как Java и C++. Я употребил термин «класс» в этой главе неформаль но. В данном разделе проводятся более формальные параллели между JavaScript и истинным наследованием на базе классов в таких языках, как Java и C++.1 Начнем с того, что определим некоторые базовые термины. Объект, как мы уже видели, – это структура данных, которая содержит различные фрагменты име нованных данных, а также может содержать методы для работы с этими фраг ментами данных. Объект группирует связанные значения и методы в единый удобный набор, который, как правило, облегчает процесс программирования, увеличивая степень модульности и возможности для многократного использова ния кода. Объекты в JavaScript могут иметь произвольное число свойств, и свой ства могут добавляться в объект динамически. В строго типизированных язы ках, таких как Java и C++, это не так. В них любой объект имеет предопределен ный набор свойств2, а каждое свойство имеет предопределенный тип. Имитируя объектноориентированные приемы программирования при помощи JavaScript 1 Этот раздел рекомендуется прочитать даже тем, кто незнаком с этими языками и упомянутым стилем объектноориентированного программирования. 2 Обычно в Java и C++ они называются «полями», но здесь мы будем называть их свойствами, поскольку такая терминология принята в JavaScript. 9.3. Объектно*ориентированный язык JavaScript 173 объектов, мы, как правило, заранее определяем набор свойств для каждого объ екта и тип данных, содержащихся в каждом свойстве. В Java и C++ класс определяет структуру объекта. Класс точно задает поля, ко торые содержатся в объекте, и типы данных этих полей. Он также определяет методы для работы с объектом. В JavaScript нет формального понятия класса, но, как мы видели, в этом языке приближение к возможностям классов реализу ется с помощью конструкторов и объектовпрототипов. И JavaScript, и объектноориентированные языки, основывающиеся на классах, допускают наличие множества объектов одного класса. Мы часто говорим, что объект – это экземпляр класса. Таким образом, одновременно может существо вать множество экземпляров любого класса. Иногда для описания процесса созда ния объекта (т. е. экземпляра класса) используется термин создание экземпляра. В Java обычная практика программирования заключается в присвоении клас сам имен с первой прописной буквой, а объектам – со всеми строчными. Это со глашение помогает отличать классы и объекты в исходных текстах. Этому же соглашению желательно следовать и при написании программ на языке Java Script. Например, в предыдущих разделах мы определили класс Rectangle и соз давали экземпляры этого класса с именами, такими как rect. Члены Javaкласса могут принадлежать одному из четырех основных типов: свойства экземпляра, методы экземпляра, свойства класса и методы класса. В сле дующих разделах мы рассмотрим различия между этими типами и поговорим о том, как JavaScript имитирует эти типы. 9.3.1. Свойства экземпляра Каждый объект имеет собственные копии свойств экземпляра. Другими слова ми, если имеется 10 объектов данного класса, то имеется и 10 копий каждого свойства экземпляра. Например, в нашем классе Rectangle любой объект Rectang le имеет свойство width, определяющее ширину прямоугольника. В данном слу чае width представляет собой свойство экземпляра. А поскольку каждый объект имеет собственную копию свойства экземпляра, доступ к этим свойствам можно получить через отдельные объекты. Если, например, r – это объект, представ ляющий собою экземпляр класса Rectangle, мы можем получить его ширину сле дующим образом: r.width По умолчанию любое свойство объекта в JavaScript является свойством экземп ляра. Однако чтобы понастоящему имитировать объектноориентированное программирование, мы будем говорить, что свойства экземпляра в JavaScript – это те свойства, которые создаются и/или инициализируются функциейконст руктором. 9.3.2. Методы экземпляра Метод экземпляра во многом похож на свойство экземпляра, за исключением того, что это метод, а не значение. (В Java функции и методы не являются дан ными, как это имеет место в JavaScript, поэтому в Java данное различие выра жено более четко.) Методы экземпляра вызываются по отношению к определен 174 Глава 9. Классы, конструкторы и прототипы ному объекту, или экземпляру. Метод area() нашего класса Rectangle представ ляет собой метод экземпляра. Он вызывается для объекта Rectangle следующим образом: a = r.area( ); Методы экземпляра ссылаются на объект, или экземпляр, с которым они работа ют, при помощи ключевого слова this. Метод экземпляра может быть вызван для любого экземпляра класса, но это не значит, что каждый объект содержит собст венную копию метода, как в случае свойства экземпляра. Вместо этого каждый метод экземпляра совместно используется всеми экземплярами класса. В Java Script мы определяем метод экземпляра класса путем присваивания функции свойству объектапрототипа в конструкторе. Так, все объекты, созданные дан ным конструктором, совместно используют унаследованную ссылку на функцию и могут вызывать ее с помощью приведенного синтаксиса вызова методов. 9.3.2.1. Методы экземпляра и ключевое слово this Если у вас есть опыт работы с такими языками, как Java или C++, вы наверняка заметили одно важное отличие между методами экземпляров в этих языках и методами экземпляров в JavaScript. В Java и C++ область видимости методов экземпляров включает объект this. Так, например, метод area в Java может быть реализован проще: return width * height; Однако в JavaScript приходится явно вставлять ключевое слово this перед име нами свойств: return this.width * this.height; Если вам покажется неудобным вставлять this перед каждым именем свойства эк земпляра, можно воспользоваться инструкцией with (описываемой в разделе 6.18), например: Rectangle.prototype.area = function( ) { with(this) { return width*height; } } 9.3.3. Свойства класса Свойство класса в Java – это свойство, связанное с самим классом, а не с каж дым экземпляром этого класса. Независимо от того, сколько создано экземпля ров класса, есть только одна копия каждого свойства класса. Так же, как свойст ва экземпляра доступны через экземпляр класса, доступ к свойствам класса можно получить через сам класс. Запись Number.MAX_VALUE – это пример обраще ния к свойству класса в JavaScript, означающая, что свойство MAX_VALUE доступно через класс Number. Так как имеется только одна копия каждого свойства класса, свойства класса по существу являются глобальными. Однако их достоинство со стоит в том, что они связаны с классом и имеют логичную нишу, позицию в про странстве имен JavaScript, где они вряд ли будут перекрыты другими свойства ми с тем же именем. Очевидно, что свойства класса имитируются в JavaScript 9.3. Объектно*ориентированный язык JavaScript 175 простым определением свойства самой функцииконструктора. Например, свой ство класса Rectangle.UNIT для хранения единичного прямоугольника с размера ми 1x1 можно создать так: Rectangle.UNIT = new Rectangle(1,1); Здесь Rectangle – это функцияконструктор, но поскольку функции в JavaScript представляют собой объекты, мы можем создать свойство функции точно так же, как свойства любого другого объекта. 9.3.4. Методы класса Метод класса – это метод, связанный с классом, а не с экземпляром класса; он вызывается через сам класс, а не через конкретный экземпляр класса. Метод Date.parse() (описываемый в третьей части книги) – это метод класса. Он всегда вызывается через объект конструктора Date, а не через конкретный экземпляр класса Date. Поскольку методы класса вызываются через функциюконструктор, они не могут использовать ключевое слово this для ссылки на какойлибо конкретный экземп ляр класса, поскольку в данном случае this ссылается на саму функциюконст руктор. (Обычно ключевое слово this в методах классов вообще не используется.) Как и свойства класса, методы класса являются глобальными. Методы класса не работают с конкретным экземпляром, поэтому их, как правило, проще рассмат ривать в качестве функций, вызываемых через класс. Как и в случае со свойст вами класса, связь этих функций с классом дает им в пространстве имен Java Script удобную нишу и предотвращает возникновение конфликтов имен. Для то го чтобы определить метод класса в JavaScript, требуется сделать соответствую щую функцию свойством конструктора. 9.3.5. Пример: класс Circle В примере 9.1 приводится программный код функцииконструктора и объекта прототипа, используемых для создания объектов, представляющих круг. Здесь можно найти примеры свойств экземпляра, методов экземпляра, свойств класса и методов класса. Пример 9.1. Класс Circle // Начнем с конструктора. function Circle(radius) { // r – свойство экземпляра, оно определяется // и инициализируется конструктором. this.r = radius; } // Circle.PI – свойство класса, т. е. свойство функцииконструктора. Circle.PI = 3.14159; // Метод экземпляра, который рассчитывает площадь круга. Circle.prototype.area = function( ) { return Circle.PI * this.r * this.r; } // Метод класса – принимает два объекта Circle и возвращает объект с большим радиусом. Circle.max = function(a,b) { 176 Глава 9. Классы, конструкторы и прототипы if (a.r > b.r) return a; else return b; } // Примеры использования каждого из этих полей: var c = new Circle(1.0); // Создание экземпляра класса Circle c.r = 2.2; // Установка свойства экземпляра r var a = c.area(); // Вызов метода экземпляра area() var x = Math.exp(Circle.PI); // Обращение к свойству PI класса для выполнения расчетов var d = new Circle(1.2); // Создание другого экземпляра класса Circle var bigger = Circle.max(c,d); // Вызов метода класса max() 9.3.6. Пример: комплексные числа В примере 9.2 представлен еще один способ определения класса объектов в Java Script, но несколько более формальный, чем предыдущий. Код и комментарии достойны тщательного изучения. Пример 9.2. Класс комплексных чисел /* * Complex.js: * В этом файле определяется класс Complex для представления комплексных чисел. * Вспомним, что комплексное число – это сумма вещественной и мнимой * частей числа, и что мнимое число i – это квадратный корень из 1. */ /* * Первый шаг в определении класса – это определение функцииконструктора * класса. Этот конструктор должен инициализировать все свойства * экземпляра объекта. Это неотъемлемые "переменные состояния", * делающие все экземпляры класса разными. */ function Complex(real, imaginary) { this.x = real; // Вещественная часть числа this.y = imaginary; // Мнимая часть числа } /* * Второй шаг в определении класса – это определение методов экземпляра * (и возможно других свойств) в объектепрототипе конструктора. * Любые свойства, определенные в этом объекте, будут унаследованы всеми * экземплярами класса. Обратите внимание, что методы экземпляра * неявно работают с ключевым словом this. Для многих методов никаких * других аргументов не требуется. */ // Возвращает модуль комплексного числа. Он определяется как расстояние // на комплексной плоскости до числа от начала координат (0,0). Complex.prototype.magnitude = function() { return Math.sqrt(this.x*this.x + this.y*this.y); }; // Возвращает комплексное число с противоположным знаком. Complex.prototype.negative = function() { 9.3. Объектно*ориентированный язык JavaScript 177 return new Complex(this.x, this.y); }; // Складывает данное комплексное число с заданным и возвращает // сумму в виде нового объекта. Complex.prototype.add = function(that) { return new Complex(this.x + that.x, this.y + that.y); } // Умножает данное комплексное число на заданное и возвращает // произведение в виде нового объекта. Complex.prototype.multiply = function(that) { return new Complex(this.x * that.x  this.y * that.y, this.x * that.y + this.y * that.x); } // Преобразует объект Complex в строку в понятном формате. // Вызывается, когда объект Complex используется как строка. Complex.prototype.toString = function() { return "{" + this.x + "," + this.y + "}"; }; // Проверяет равенство данного комплексного числа с заданным. Complex.prototype.equals = function(that) { return this.x == that.x && this.y == that.y; } // Возвращает вещественную часть комплексного числа. // Эта функция вызывается, когда объект Complex рассматривается // как числовое значение. Complex.prototype.valueOf = function() { return this.x; } /* * Третий шаг в определении класса – это определение методов класса, * констант и других необходимых свойств класса как свойств самой * функцииконструктора (а не как свойств объектапрототипа * конструктора). Обратите внимание, что методы класса не используют * ключевое слово this, они работают только со своими аргументами. */ // Складывает два комплексных числа и возвращает результат. Complex.add = function (a, b) { return new Complex(a.x + b.x, a.y + b.y); }; // Умножает два комплексных числа и возвращает полученное произведение. Complex.multiply = function(a, b) { return new Complex(a.x * b.x  a.y * b.y, a.x * b.y + a.y * b.x); }; // Несколько предопределенных комплексных чисел. // Они определяются как свойства класса, в котором могут использоваться как "константы". // (Хотя в JavaScript невозможно определить свойства, доступные только для чтения.) Complex.ZERO = new Complex(0,0); Complex.ONE = new Complex(1,0); Complex.I = new Complex(0,1); 178 Глава 9. Классы, конструкторы и прототипы 9.3.7. Частные члены Одна из наиболее общих характеристик традиционных объектноориентирован ных языков программирования, таких как C++, заключается в возможности объявления частных (private) свойств класса, обращаться к которым можно только из методов этого класса и недоступных за пределами класса. Распростра ненная техника программирования, называемая инкапсуляцией данных, заклю чается в создании частных свойств и организации доступа к этим свойствам толь ко через специальные методы чтения/записи. JavaScript позволяет имитировать такое поведение посредством замыканий (эта тема обсуждается в разделе 8.8), но для этого необходимо, чтобы методы доступа хранились в каждом экземпляре класса и по этой причине не могли наследоваться от объектапрототипа. Следующий фрагмент демонстрирует, как можно добиться этого. Он содержит реализацию объекта прямоугольника Rectangle, ширина и высота которого дос тупны и могут изменяться только путем обращения к специальным методам: function ImmutableRectangle(w, h) { // Этот конструктор не создает свойства объекта, где может храниться // ширина и высота. Он просто определяет в объекте методы доступа // Эти методы являются замыканиями и хранят значения ширины и высоты // в своих цепочках областей видимости. this.getWidth = function() { return w; } this.getHeight = function() { return h; } } // Обратите внимание: класс может иметь обычные методы в объектепрототипе. ImmutableRectangle.prototype.area = function( ) { return this.getWidth( ) * this.getHeight( ); }; Первенство открытия этой методики (или, по крайней мере, первенство публи кации), вообще говоря, принадлежит Дугласу Крокфорду (Douglas Crockford). Его обсуждение этой темы можно найти на странице http://www.crockford.com/ javascript/private.html. 9.4. Общие методы класса Object Когда в JavaScript определяется новый класс, некоторые из его методов следует считать предопределенными. Эти методы подробно описываются в следующих подразделах. 9.4.1. Метод toString() Идея метода toString() состоит в том, что каждый класс объектов должен иметь собственное особое строковое представление и поэтому определять соответствую щий метод toString() для преобразования объектов в строковую форму. То есть определяя класс, необходимо определить для него специальный метод toString(), чтобы экземпляры класса могли быть преобразованы в осмысленные строки. Строка должна содержать информацию о преобразуемом объекте, т. к. это может потребоваться для нужд отладки. Если способ преобразования в строку выбран правильно, он также может быть полезным в самих программах. Кроме того, 9.4. Общие методы класса Object 179 можно создать собственную реализацию статического метода parse() для преоб разования строки, возвращаемой методом toString(), обратно в форму объекта. Класс Complex из примера 9.2 уже содержит реализацию метода toString(), а в сле дующем фрагменте приводится возможная реализация метода toString() для класса Circle: Circle.prototype.toString = function () { return "[Круг радиуса " + this.r + " с центром в точке (" + this.x + ", " + this.y + ").]"; } После определения такого метода toString() типичный объект Circle может быть преобразован в следующую строку: "Круг радиуса 1 с центром в точке (0, 0)." 9.4.2. Метод valueOf() Метод valueOf() во многом похож на метод toString(), но вызывается, когда Java Script требуется преобразовать объект в значение какоголибо элементарного ти па, отличного от строкового – обычно в число. Когда это возможно, функция должна возвращать элементарное значение, какимлибо образом представляю щее значение объекта, на который ссылается ключевое слово this. По определению объекты не являются элементарными значениями, поэтому большинство объектов не имеют эквивалентного элементарного типа. Вследст вие этого метод valueOf(), определяемый по умолчанию классом Object, не вы полняет преобразования, а просто возвращает объект, с которым он был вызван. Такие классы, как Number и Boolean, имеют очевидные элементарные эквивален ты, поэтому они переопределяют метод valueOf() так, чтобы он возвращал соот ветствующие значения. Именно поэтому объекты Number и Boolean могут вести се бя во многом так же, как эквивалентные им элементарные значения. Иногда можно определить класс, имеющий какойто разумный элементарный эквивалент. В этом случае может потребоваться определить для этого класса специальный метод valueOf(). Если мы вернемся к примеру 9.2, то увидим, что метод valueOf() определен для класса Complex. Этот метод просто возвращает ве щественную часть комплексного числа. Поэтому в числовом контексте объект Complex ведет себя так, как будто является вещественным числом без мнимой со ставляющей. Рассмотрим, например, следующий фрагмент: var a = new Complex(5,4); var b = new Complex(2,1); var c = Complex.sum(a,b); // c это комплексное число {7,5} var d = a + b; // d это число 7 При наличии метода valueOf() следует соблюдать одну осторожность: в случае преобразования объекта в строку метод valueOf() иногда имеет приоритет перед методом toString(). Поэтому, когда для класса определен метод valueOf() и надо, чтобы объект этого класса был преобразован в строку, может потребоваться явно указать на это, вызвав метод toString(). Продолжим пример с классом Complex: alert("c = " + c); // Используется valueOf(); выводит "c = 7" alert("c = " + c.toString()); // Выводит "c = {7,5}" 180 Глава 9. Классы, конструкторы и прототипы 9.4.3. Методы сравнения Операторы сравнения в JavaScript сравнивают объекты по ссылке, а не по значе нию. Так, если имеются две ссылки на объекты, то выясняется, ссылаются они на один и тот же объект или нет, но не выясняется, обладают ли разные объекты оди наковыми свойствами с одинаковыми значениями.1 Часто бывает удобным иметь возможность выяснить эквивалентность объектов или даже определить порядок их следования (например, с помощью операторов отношения < и >). Если вы опре деляете класс и хотите иметь возможность сравнивать экземпляры этого класса, вам придется определить соответствующие методы, выполняющие сравнение. В языке программирования Java сравнение объектов производится с помощью методов, и подобный подход можно с успехом использовать в JavaScript. Чтобы иметь возможность сравнивать экземпляры класса, можно определить метод эк земпляра с именем equals(). Этот метод должен принимать единственный аргу мент и возвращать true, если аргумент эквивалентен объекту, метод которого был вызван. Разумеется, вам решать, что следует понимать под словом «эквива лентен» в контексте вашего класса. Обычно для определения того, равны ли объ екты, сравниваются значения свойств экземпляров двух объектов. Класс Complex из примера 9.2 как раз обладает таким методом equals(). Иногда возникает необходимость реализовать операции сравнения, чтобы выяс нить порядок следования объектов. Так, для некоторых классов вполне можно сказать, что один экземпляр «меньше» или «больше» другого. Например, поря док следования объектов класса Complex определяется на основе значения, воз вращаемого методом magnitude(). В то же время для объектов класса Circle слож но определить смысл слов «меньше» и «больше» – следует ли сравнивать вели чину радиуса или нужно сравнивать координаты X и Y? А может быть, следует учитывать величины всех трех параметров? При попытке сравнения JavaScriptобъектов с помощью операторов отношения, таких как < и <=, интерпретатор сначала вызовет методы valueOf() объектов, и ес ли методы вернут значения элементарных типов, сравнит эти значения. По скольку класс Complex имеет метод valueOf(), который возвращает вещественную часть комплексного числа, экземпляры класса Complex можно сравнивать как обычные вещественные числа, не имеющие мнимой части.2 Это может совпадать или не совпадать с вашими намерениями. Чтобы сравнивать объекты для выяс нения порядка их следования по вашему выбору, вам необходимо (опять же, сле дуя соглашениям, принятым в языке программирования Java) реализовать ме тод с именем compareTo(). Метод compareTo() должен принимать единственный аргумент и сравнивать его с объектом, метод которого был вызван. Если объект this меньше, чем объект, 1 То есть являются эквивалентными копиямиэкземплярами одного класса. – При' меч. науч. ред. 2 При этом получается результат, очень странный для всякого, кто работает в об ластях применения комплексной математики. Это хороший пример того, как ско ропалительное определение метода valueOf() (да и любого метода, особенно из чис ла базовых) в дальнейшем может преподносить пользователю большие сюрпри зы, не согласующиеся с его логикой восприятия. – Примеч. науч. ред. 9.4. Общие методы класса Object 181 представленный аргументом, метод compareTo() должен возвращать значение меньше нуля. Если объект this больше, чем объект, представленный аргумен том, метод должен возвращать значение больше нуля. И если оба объекта равны, метод должен возвращать значение, равное нулю. Эти соглашения о возвращае мом значении весьма важны, потому что позволяют выполнять замену операто ров отношения следующими выражениями: Вот одна из возможных реализаций метода compareTo() для класса Complex из при мера 9.2, в которой сравниваются комплексные числа по их модулям: Complex.prototype.compareTo = function(that) { // Если аргумент не был передан или он не имеет метода // magnitude(), необходимо сгенерировать исключение. // Как вариант – можно было бы вернуть значение 1 или 1, // чтобы както обозначить, что комплексное число всегда меньше // или больше, чем любое другое значение. if (!that || !that.magnitude || typeof that.magnitude != "function") throw new Error("неверный аргумент в Complex.compareTo()"); // Здесь используется свойство операции вычитания, которая // возвращает значение меньшее, большее или равное нулю. // Этот прием можно использовать во многих реализациях метода compareTo(). return this.magnitude()  that.magnitude(); } Одна из причин, по которым может потребоваться сравнивать экземпляры клас са, – возможность сортировки массива этих экземпляров в некотором порядке. Метод Array.sort() может принимать в виде необязательного аргумента функ цию сравнения, которая должна следовать тем же соглашениям о возвращаемом значении, что и метод compareTo(). При наличии метода compareTo() достаточно просто организовать сортировку массива комплексных чисел примерно следую щим образом: complexNumbers.sort(new function(a,b) { return a.compareTo(b); }); Сортировка имеет большое значение, и потому следует рассмотреть возможность реализации статического метода compare() в любом классе, где определен метод экземпляра compareTo(). Особенно если учесть, что первый может быть легко реа лизован в терминах второго, например: Complex.compare = function(a,b) { return a.compareTo(b); }; Выражение отношения Выражение замены a < b a.compareTo(b) < 0 a <= b a.compareTo(b) <= 0 a > b a.compareTo(b) > 0 a >= b a.compareTo(b) >= 0 a == b a.compareTo(b) == 0 a != b a.compareTo(b) != 0 182 Глава 9. Классы, конструкторы и прототипы При наличии этого метода сортировка массива может быть реализована еще проще: complexNumbers.sort(Complex.compare); Обратите внимание: реализации методов compare() и compareTo() не были включе ны в определение класса Complex из примера 9.2. Дело в том, что они не согласу ются с методом equals(), который был определен в этом примере. Метод equals() утверждает, что два объекта класса Complex эквивалентны, если их веществен ные и мнимые части равны. Однако метод compareTo() возвращает нулевое значе ние для любых двух комплексных чисел, которые имеют равные модули. Числа 1+0i и 0+1i имеют одинаковые модули и эти два числа будут объявлены равны ми при вызове метода compareTo(), но метод equals() утверждает, что они не рав ны. Таким образом, если вы собираетесь реализовать методы equals() и compa reTo() в одном и том же классе, будет совсем нелишним их както согласовать. Несогласованность в понимании термина «равенство» может стать источником пагубных ошибок. Рассмотрим реализацию метода compareTo(), который согла суется с существующим методом equals():1 // При сравнении комплексных чисел в первую очередь сравниваются // их вещественные части. Если они равны, сравниваются мнимые части Complex.prototype.compareTo = function(that) { var result = this.x  that.x; // Сравнить вещественные части // с помощью операции вычитания if (result == 0) // Если они равны... result = this.y  that.y; // тогда сравнить мнимые части // Теперь результат будет равен нулю только в том случае, // если равны и вещественные, и мнимые части return result; }; 9.5. Надклассы и подклассы В Java, C++ и других объектноориентированных языках на базе классов имеет ся явная концепция иерархии классов. Каждый класс может иметь надкласс, от которого он наследует свойства и методы. Любой класс может быть расширен, т. е. иметь подкласс, наследующий его поведение. Как мы видели, JavaScript поддерживает наследование прототипов вместо наследования на базе классов. Тем не менее в JavaScript могут быть проведены аналогии с иерархией классов. В JavaScript класс Object – это наиболее общий класс, и все другие классы явля ются его специализированными версиями, или подклассами. Можно также ска зать, что Object – это надкласс всех встроенных классов. Все классы наследуют несколько базовых методов класса Object. Мы узнали, что объекты наследуют свойства от объектапрототипа их конструк тора. Как они могут наследовать свойства еще и от класса Object? Вспомните, что объектпрототип сам представляет собой объект; он создается с помощью конст 1 Но при таком определении достаточно «странную» семантику обретают операто ры отношения < и >. – Примеч. науч. ред. 9.5. Надклассы и подклассы 183 руктора Object(). Это значит, что объектпрототип наследует свойства от Ob ject.prototype! Поэтому объект класса Complex наследует свойства от объекта Com plex.prototype, который в свою очередь наследует свойства от Object.prototype. Когда выполняется поиск некоторого свойства в объекте Complex, сначала выпол няется поиск в самом объекте. Если свойство не найдено, поиск продолжается в объекте Complex.prototype. И наконец, если свойство не найдено и в этом объек те, выполняется поиск в объекте Object.prototype. Обратите внимание: поскольку в объектепрототипе Complex поиск происходит раньше, чем в объектепрототипе Object, свойства объекта Complex.prototype скрывают любые свойства с тем же именем из Object.prototype. Так, в классе, по казанном в примере 9.2, мы определили в объекте Complex.prototype метод toString(). Object.prototype также определяет метод с этим именем, но объекты Complex никогда не увидят его, поскольку определение toString() в Complex.proto type будет найдено раньше. Все классы, которые мы показали в этой главе, представляют собой непосредст венные подклассы класса Object. Это типично для программирования на Java Script; обычно в создании более сложной иерархии классов нет никакой необхо димости. Однако когда это требуется, можно создать подкласс любого другого класса. Предположим, что мы хотим создать подкласс класса Rectangle, чтобы до бавить в него свойства и методы, связанные с координатами прямоугольника. Для этого мы просто должны быть уверены, что объектпрототип нового класса сам является экземпляром Rectangle и потому наследует все свойства Rectang le.prototype. Пример 9.3 повторяет определение простого класса Rectanle и затем расширяет это определение за счет создания нового класса PositionedRectangle. Пример 9.3. Создание подкласса в JavaScript // Определение простого класса прямоугольников. // Этот класс имеет ширину и высоту и может вычислять свою площадь function Rectangle(w, h) { this.width = w; this.height = h; } Rectangle.prototype.area = function( ) { return this.width * this.height; } // Далее идет определение подкласса function PositionedRectangle(x, y, w, h) { // В первую очередь необходимо вызвать конструктор надкласса // для инициализации свойств width и height нового объекта. // Здесь используется метод call, чтобы конструктор был вызван // как метод инициализируемого объекта. // Это называется вызов конструктора по цепочке. Rectangle.call(this, w, h); // Далее сохраняются координаты верхнего левого угла прямоугольника this.x = x; this.y = y; } // Если мы будем использовать объектпрототип по умолчанию, // который создается при определении конструктора PositionedRectangle(), // был бы создан подкласс класса Object. 184 Глава 9. Классы, конструкторы и прототипы // Чтобы создать подкласс класса Rectangle, необходимо явно создать объектпрототип. PositionedRectangle.prototype = new Rectangle(); // Мы создали объектпрототип с целью наследования, но мы не собираемся // наследовать свойства width и height, которыми обладают все объекты // класса Rectangle, поэтому удалим их из прототипа. delete PositionedRectangle.prototype.width; delete PositionedRectangle.prototype.height; // Поскольку объектпрототип был создан с помощью конструктора // Rectangle(), свойство constructor в нем ссылается на этот // конструктор. Но нам нужно, чтобы объекты PositionedRectangle // ссылались на другой конструктор, поэтому далее выполняется // присваивание нового значения свойству constructor PositionedRectangle.prototype.constructor = PositionedRectangle; // Теперь у нас имеется правильно настроенный прототип для нашего // подкласса, можно приступать к добавлению методов экземпляров. PositionedRectangle.prototype.contains = function(x,y) { return (x > this.x && x < this.x + this.width && y > this.y && y < this.y + this.height); } Как видно из примера 9.3, создание подклассов в JavaScript выглядит более сложным, чем наследование от класса Object. Первая проблема связана с необхо димостью вызова конструктора надкласса из конструктора подкласса, причем конструктор надкласса приходится вызывать как метод вновь созданного объек та. Затем приходится хитрить и подменять конструктор объектапрототипа под класса. Нам потребовалось явно создать этот объектпрототип как экземпляр надкласса, после чего надо было явно изменить свойство constructor объекта прототипа.1 Может также появиться желание удалить любые свойства, которые создаются конструктором надкласса в объектепрототипе, поскольку очень важ но, чтобы свойства объектапрототипа наследовались из его прототипа. Имея такое определение класса PositionedRectangle, его можно использовать в сво их программах примерно так: var r = new PositionedRectangle(2,2,2,2); print(r.contains(3,3)); // Вызывается метод экземпляра print(r.area( )); // Вызывается унаследованный метод экземпляра // Работа с полями экземпляра класса: print(r.x + ", " + r.y + ", " + r.width + ", " + r.height); // Наш объект может рассматриваться как экземпляр всех 3 классов 1 В версии Rhino 1.6r1 и более ранних (интерпретатор JavaScript, написанный на языке Java) имеется ошибка, которая делает свойство constructor неудаляемым и доступным только для чтения. В этих версиях Rhino в программном коде, вы полняющем настройку свойства constructor, происходит сбой без вывода сообще ний об ошибке. В результате экземпляры класса PositionedRectangle наследуют значение свойства constructor, которое ссылается на конструктор Rectangle(). На практике эта ошибка почти не проявляется, потому что свойства наследуются правильно и оператор instanceof корректно различает экземпляры классов Positi onedRectangle и Rectangle. 9.5. Надклассы и подклассы 185 print(r instanceof PositionedRectangle && r instanceof Rectangle && r instanceof Object); 9.5.1. Изменение конструктора В только что продемонстрированном примере функцияконструктор Positioned Rectangle() должна явно вызывать функциюконструктор надкласса. Это назы вается вызовом конструктора по цепочке и является обычной практикой при создании подклассов. Вы можете упростить синтаксис конструктора, добавив свойство superclass в объектпрототип подкласса: // Сохранить ссылку на конструктор надкласса. PositionedRectangle.prototype.superclass = Rectangle; Однако следует заметить, что такой прием можно использовать только при усло вии неглубокой иерархии наследования. Так, если класс В является наследником класса А, а класс С – наследником класса В, и в обоих классах В и С используется прием с обращением к свойству superclass, то при попытке создать экземпляр класса С ссылка this.superclass будет указывать на конструктор В(), что в резуль тате приведет к бесконечному рекурсивному зацикливанию конструктора В(). Поэтому для всего, что не является простым подклассом, используйте методику вызова конструктора по цепочке, которая продемонстрирована в примере 9.3. После того как свойство определено, синтаксис вызова конструктора по цепочке становится значительно проще: function PositionedRectangle(x, y, w, h) { this.superclass(w,h); this.x = x; this.y = y; } Обратите внимание: функцияконструктор явно вызывается в контексте объек та this. Это означает, что можно отказаться от использования метода call() или apply() для вызова конструктора надкласса как метода данного объекта. 9.5.2. Вызов переопределенных методов Когда в подклассе определяется метод, имеющий то же самое имя, что и метод надкласса, подкласс переопределяет (overrides) этот метод. Ситуация, когда подкласс порождается от существующего класса, встречается достаточно часто. Например, в любой момент можно определить метод toString() класса и тем са мым переопределить метод toString() класса Object. Зачастую переопределение методов производится не с целью полной замены, а лишь для того, чтобы расширить их функциональность. Для этого метод должен иметь возможность вызывать переопределенный метод. В определенном смысле такой прием по аналогии с конструкторами можно назвать вызовом методов по цепочке. Однако вызвать переопределенный метод гораздо менее удобно, чем кон структор надкласса. Рассмотрим следующий пример. Предположим, что класс Rectangle определяет метод toString() (что должно быть сделано чуть ли не в первую очередь) следую щим образом: 186 Глава 9. Классы, конструкторы и прототипы Rectangle.prototype.toString = function( ) { return "[" + this.width + "," + this.height + "]"; } Если уж вы реализовали метод toString() в классе Rectangle, то тем более его не обходимо переопределить в классе PositionedRectangle, чтобы экземпляры под класса могли иметь строковое представление, отражающее значения не только ширины и высоты, но и всех остальных их свойств. PositionedRectangle – очень простой класс и для него достаточно, чтобы метод toString() просто возвращал значения всех его свойств. Однако ради примера будем обрабатывать значения свойств координат в самом классе, а обработку свойств width и height делегируем надклассу. Сделать это можно примерно следующим образом: PositionedRectangle.prototype.toString = function() { return "(" + this.x + "," + this.y + ") " + // поля этого класса Rectangle.prototype.toString.apply(this); // вызов надкласса по цепочке } Реализация метода toString() надкласса доступна как свойство объектапрото типа надкласса. Обратите внимание: мы не можем вызвать метод напрямую – нам пришлось воспользоваться методом apply(), чтобы указать, для какого объ екта вызывается метод. Однако если в PositionedRectangle.prototype добавить свойство superclass, можно сделать так, чтобы этот код не зависел от типа надкласса: PositionedRectangle.prototype.toString = function( ) { return "(" + this.x + "," + this.y + ") " + // поля этого класса this.superclass.prototype.toString.apply(this); } Еще раз обратите внимание, что свойство superclass может использоваться в ие рархии наследования только один раз. Если оно будет задействовано классом и его подклассом, это приведет к бесконечной рекурсии. 9.6. Расширение без наследования Предыдущее обсуждение проблемы создания подклассов описывает порядок создания новых классов, наследующих методы других классов. Язык JavaScript настолько гибкий, что создание подклассов и использование механизма насле дования – это не единственный способ расширения функциональных возможно стей классов. Поскольку функции в JavaScript – это просто значения данных, они могут легко копироваться (или «заимствоваться») из одного класса в дру гой. В примере 9.4 демонстрируется функция, которая заимствует все методы одного класса и создает их копии в объектепрототипе другого класса. Пример 9.4. Заимствование методов одного класса для использования в другом // Заимствование методов одного класса для использования в другом. // Аргументы должны быть функциямиконструкторами классов. // Методы встроенных типов, таких как Object, Array, Date и RegExp // не являются перечислимыми и потому не заимствуются этой функцией. function borrowMethods(borrowFrom, addTo) { var from = borrowFrom.prototype; // прототиписточник 9.6. Расширение без наследования 187 var to = addTo.prototype; // прототипприемник for(m in from) { // Цикл по всем свойствам прототипаисточника if (typeof from[m] != "function") continue; // Игнорировать все, // что не является функциями to[m] = from[m]; // Заимствовать метод } } Многие методы настолько тесно связаны с классом, в котором они определены, что нет смысла пытаться использовать их в другом классе. Однако некоторые методы могут быть достаточно универсальными и пригодяться в любом классе. В примере 9.5 приводятся определения двух классов, ничего особо полезного не делающих, зато реализующих методы, которые могут быть заимствованы дру гими классами. Подобные классы, разрабатываемые специально с целью заим ствования, называются классами'смесями, или просто смесями. Пример 9.5. Классы'смеси с универсальными методами, предназначенными для заимствования // Сам по себе этот класс не очень хорош. Но он определяет универсальный // метод toString(), который может представлять интерес для других классов. function GenericToString() {} GenericToString.prototype.toString = function( ) { var props = []; for(var name in this) { if (!this.hasOwnProperty(name)) continue; var value = this[name]; var s = name + ":" switch(typeof value) { case 'function': s += "function"; break; case 'object': if (value instanceof Array) s += "array" else s += value.toString( ); break; default: s += String(value); break; } props.push(s); } return "{" + props.join(", ") + "}"; } // Следующий класс определяет метод equals(), который сравнивает простые объекты. function GenericEquals() {} GenericEquals.prototype.equals = function(that) { if (this == that) return true; // объекты равны, только если объект this имеет те же свойства, // что и объект that, и не имеет никаких других свойств // Обратите внимание: нам не требуется глубокое сравнение. // Значения просто должны быть === друг другу. Из этого следует, 188 Глава 9. Классы, конструкторы и прототипы // если есть свойства, ссылающиеся на другие объекты, они должны ссылаться // на те же самые объекты, а не на объекты, для которых equals() возвращает true var propsInThat = 0; for(var name in that) { propsInThat++; if (this[name] !== that[name]) return false; } // Теперь необходимо убедиться, что объект this не имеет дополнительных свойств var propsInThis = 0; for(name in this) propsInThis++; // Если объект this обладает дополнительными свойствами, // следовательно, объекты не равны if (propsInThis != propsInThat) return false; // Два объекта выглядят равными. return true; } Вот как выглядит простой класс Rectangle, который заимствует методы toString() и equals(), определенные в классахсмесях: // Простой класс Rectangle function Rectangle(x, y, w, h) { this.x = x; this.y = y; this.width = w; this.height = h; } Rectangle.prototype.area = function( ) { return this.width * this.height; } // Заимствование некоторых методов borrowMethods(GenericEquals, Rectangle); borrowMethods(GenericToString, Rectangle); Ни один из представленных здесь классовсмесей не имеет собственного конст руктора, однако это не значит, что конструкторы нельзя заимствовать. В сле дующем фрагменте приводится определение нового класса с именем ColoredRec tangle. Он наследует функциональность класса Rectangle и заимствует конструк тор и метод из классасмеси Colored: // Эта смесь содержит метод, зависящий от конструктора. Оба они, // и конструктор, и метод должны быть заимствованы. function Colored(c) { this.color = c; } Colored.prototype.getColor = function() { return this.color; } // Определение конструктора нового класса function ColoredRectangle(x, y, w, h, c) { this.superclass(x, y, w, h); // Вызов конструктора надкласса Colored.call(this, c); // и заимствование конструктора Colored } // Настройка объектапрототипа на наследование методов от Rectangle ColoredRectangle.prototype = new Rectangle(); ColoredRectangle.prototype.constructor = ColoredRectangle; ColoredRectangle.prototype.superclass = Rectangle; 9.7. Определение типа объекта 189 // Заимствовать методы класса Colored в новый класс borrowMethods(Colored, ColoredRectangle); Класс ColoredRectangle расширяет класс Rectangle (и наследует его методы), а так же заимствует методы класса Colored. Сам класс Rectangle наследует класс Object и заимствует методы классов GenericEquals и GenericToString. Хотя подобные ана логии здесь неуместны, можно воспринимать это как своего рода множественное наследование. Так как класс ColoredRectangle заимствует методы класса Colored, экземпляры класса ColoredRectangle можно одновременно рассматривать как эк земпляры класса Colored. Оператор instanceof не сможет сообщить об этом, но в разделе 9.7.3 мы создадим более универсальный метод, который позволит оп ределять, наследует или заимствует некоторый объект методы заданного класса. 9.7. Определение типа объекта Язык JavaScript – это слабо типизированный язык, а JavaScriptобъекты еще менее типизированы. Тем не менее в JavaScript существует несколько приемов, которые могут служить для определения типа произвольного значения. Конечно же, самый распространенный прием основан на использовании опера тора typeof (подробности см. в разделе 5.10.2). В первую очередь typeof позволя ет различать объекты и элементарные типы, однако он обладает некоторыми странностями. Вопервых, выражение typeof null дает в результате строку "ob ject", тогда как выражение typeof undefined возвращает строку "undefined". По мимо этого в качестве типа любого массива возвращается строка "object", по скольку все массивы – объекты, однако для любой функции возвращается стро ка "function", хотя функции фактически также являются объектами. 9.7.1. Оператор instanceof и конструктор После того как выяснится, что некоторое значение является объектом, а не эле ментарным значением и не функцией, его можно передать оператору instanceof, чтобы подробнее выяснить его природу. Например, если x является массивом, тогда следующее выражение вернет true: x instanceof Array Слева от оператора instanceof располагается проверяемое значение, справа – имя функцииконструктора, определяющей класс объектов. Обратите внимание: объект расценивается как экземпляр собственного класса и всех его надклассов. Таким образом, для любого объекта o выражение o instanceof Object всегда вер нет true. Интересно, что оператор instanceof может работать и с функциями, так, все нижеследующие выражения возвращают значение true: typeof f == "function" f instanceof Function f instanceof Object В случае необходимости можно убедиться, что некоторый объект является эк земпляром определенного класса, а не одного из подклассов – для этого доста точно проверить значение свойства constructor. В следующем фрагменте выпол няется такая проверка: var d = new Date(); // Объект Date; Date – подкласс Object 190 Глава 9. Классы, конструкторы и прототипы var isobject = d instanceof Object; // Возвращает true var realobject = d.constructor==Object; // Возвращает false 9.7.2. Определение типа объекта с помощью метода Object.toString() Недостаток оператора instanceof и свойства constructor заключается в том, что они позволяют проверять объекты на принадлежность только известным вам классам, но не дают никакой полезной информации при исследовании неизвест ных объектов, что может потребоваться, например, при отладке. В такой ситуа ции на помощь может прийти метод Object.toString(). Как уже говорилось в главе 7, класс Object содержит определение метода toString() по умолчанию. Любой класс, который не определяет собственный ме тод, наследует реализацию по умолчанию. Интересная особенность метода по умолчанию toString() состоит в том, что он выводит некоторую внутреннюю ин формацию о типе встроенных объектов. Спецификация ECMAScript требует, чтобы метод по умолчанию toString() всегда возвращал строку в формате: [object class] Здесь class – это внутренний тип объекта, который обычно соответствует имени функцииконструктора этого объекта. Например, для массивов class – это "Array", для функций – "Function", и для объектов даты/времени – "Date". Для встроенно го класса Math возвращается "Math", а для всех классов семейства Error – строка "Error". Для объектов клиентского языка JavaScript и любых других объектов, определяемых реализацией JavaScript, в качестве строки class возвращается строка, определяемая реализацией (например, "Window", "Document" или "Form"). Однако для типов объектов, определяемых пользователем, таких как Circle и Complex, описанных ранее в этой главе, в качестве строки class всегда возвра щается строка "Object". То есть метод toString() способен определять только встроенные типы объектов. Поскольку в большинстве классов метод по умолчанию toString() переопределя ется, не следует ожидать, что вызвав его непосредственно из объекта, вы полу чите имя класса. Поэтому необходимо обращаться к функции по умолчанию Ob ject.prototype явно и использовать для этого метод apply() с указанием объекта, тип которого требуется узнать: Object.prototype.toString.apply(o); // Всегда вызывается метод по умолчанию toString() Этот прием используется в примере 9.6 в определении функции, реализующей расширенные возможности по выяснению типа. Как уже отмечалось ранее, ме тод toString() не работает с пользовательскими классами, в этом случае показан ная далее функция проверяет строковое значение свойства classname и возвраща ет его значение, если оно определено. Пример 9.6. Улучшенные возможности определения типа function getType(x) { // Если значение x равно null, возвращается "null" if (x == null) return "null"; // Попробовать определить тип с помощью оператора typeof var t = typeof x; 9.7. Определение типа объекта 191 // Если получен непонятный результат, вернуть его if (t != "object") return t; // В противном случае, x – это объект. Вызвать метод toString() // по умолчанию и извлечь подстроку с именем класса. var c = Object.prototype.toString.apply(x); // В формате "[object class]" c = c.substring(8, c.length1); // Удалить "[object" и "]" // Если имя класса  не Object, вернуть его. if (c != "Object") return c; // Если получен тип "Object", проверить, может быть x // действительно принадлежит этому классу. if (x.constructor == Object) return c; // Тип действительно "Object" // Для пользовательских классов извлечь строковое значение свойства // classname, которое наследуется от объектапрототипа if ("classname" in x.constructor.prototype && // наследуемое имя класса typeof x.constructor.prototype.classname == "string") // это строка return x.constructor.prototype.classname; // Если определить тип так и не удалось, так и скажем об этом. return ""; } 9.7.3. Грубое определение типа Существует старое высказывание: «Если оно ходит как утка и крякает как утка, значит, это утка!». Перевести этот афоризм на язык JavaScript довольно сложно, однако попробуем: «Если в этом объекте реализованы все методы некоторого класса, значит, это экземпляр данного класса». В гибких языках программиро вания со слабой типизацией, таких как JavaScript, это называется «грубым оп ределением типа»: если объект обладает всеми свойствами класса X, его можно рассматривать как экземпляр класса X, даже если на самом деле этот объект не был создан с помощью функцииконструктора X().1 Грубое определение типа особенно удобно использовать для классов, «заимст вующих» методы у других классов. Ранее в этой главе демонстрировался класс Rectangle, заимствующий метод equals() у класса с именем GenericEquals. В ре зультате любой экземпляр класса Rectangle можно рассматривать как экземпляр класса GenericEquals. Оператор instanceof не может определить этот факт, но в на ших силах создать для этого собственный метод (пример 9.7). Пример 9.7. Проверка факта заимствования объектом методов заданного класса // Возвращает true, если каждый из методов c.prototype был // заимствован объектом o. Если o – это функция, а не объект, // вместо самого объекта o производится проверка его прототипа. // Обратите внимание: для этой функции необходимо, чтобы методы были // скопированы, а не реализованы повторно. Если класс заимствовал метод, // а затем переопределил его, данная функция вернет значение false. function borrows(o, c) { 1 Термин «грубое определение типа» появился благодаря языку программирова ния Ruby. Точное его название – алломорфизм. 192 Глава 9. Классы, конструкторы и прототипы // Если объект o уже является экземпляром класса c, можно вернуть true if (o instanceof c) return true; // Совершенно невозможно выполнить проверку факта заимствования методов // встроенного класса, поскольку методы встроенных типов неперечислимы. // В этом случае вместо того, чтобы генерировать, исключение возвращается // значение undefined, как своего рода ответ "Я не знаю". // Значение undefined ведет себя во многом похоже на false, // но может отличаться от false, если это потребуется вызывающей программе. if (c == Array || c == Boolean || c == Date || c == Error || c == Function || c == Number || c == RegExp || c == String) return undefined; if (typeof o == "function") o = o.prototype; var proto = c.prototype; for(var p in proto) { // Игнорировать свойства, не являющиеся функциями if (typeof proto[p] != "function") continue; if (o[p] != proto[p]) return false; } return true; } Метод borrows() из примера 9.7 достаточно ограничен: он возвращает значение true, только если объект o имеет точные копии методов, определяемых классом c. В действительности грубое определение типа должно работать более гибко: объ ект o должен рассматриваться как экземпляр класса c, если содержит методы, напоминающие методы класса c. В JavaScript «напоминающие» означает «имею щие те же самые имена» и (возможно) «объявленные с тем же количеством аргу ментов». В примере 9.8 демонстрируется метод, реализующий такую проверку. Пример 9.8. Проверка наличия одноименных методов // Возвращает true, если объект o обладает методами с теми же именами // и количеством аргументов, что и класс c.prototype. В противном случае // возвращается false. Генерирует исключение, если класс с принадлежит // встроенному типу с методами, не поддающимися перечислению. function provides(o, c) { // Если o уже является экземпляром класса c, он и так будет "напоминать" класс c if (o instanceof c) return true; // Если вместо объекта был передан конструктор объекта, использовать объектпрототип if (typeof o == "function") o = o.prototype; // Методы встроенных классов не поддаются перечислению, поэтому // возвращается значение undefined. В противном случае любой объект // будет напоминать любой из встроенных типов. if (c == Array || c == Boolean || c == Date || c == Error || c == Function || c == Number || c == RegExp || c == String) return undefined; var proto = c.prototype; for(var p in proto) { // Цикл по всем свойствам в c.prototype // Игнорировать свойства, не являющиеся функциями if (typeof proto[p] != "function") continue; // Если объект o не имеет одноименного свойства, вернуть false if (!(p in o)) return false; 9.7. Определение типа объекта 193 // Если это свойство, а не функция, вернуть false if (typeof o[p] != "function") return false; // Если две функции объявлены с разным числом аргументов, вернуть false. if (o[p].length != proto[p].length) return false; } // Если были проверены все методы, можно смело возвращать true. return true; } В качестве примера грубого определения типа и использования метода provide() рассмотрим метод compareTo(), описанный в разделе 9.4.3. Как правило, метод compareTo() не предназначен для заимствования, но иногда бывает желательно выяснить, обладают ли некоторые объекты возможностью сравнения с помощью метода compareTo(). С этой целью определим класс Comparable: function Comparable( ) {} Comparable.prototype.compareTo = function(that) { throw "Comparable.compareTo() – абстрактный метод. Не подлежит вызову!"; } Класс Comparable является абстрактным: его методы не предназначены для вы зова, он просто определяет прикладной интерфейс. Однако при наличии опреде ления этого класса можно проверить, допускается ли сравнение двух объектов: // Проверить, допускается ли сравнение объектов o и p // Они должны принадлежать одному типу и иметь метод compareTo() if (o.constructor == p.constructor && provides(o, Comparable)) { var order = o.compareTo(p); } Обратите внимание: обе функции, представленные в этом разделе, borrows() и provides(), возвращают значение undefined, если им передается объект одного из встроенных типов JavaScript, например Array. Сделано это по той простой причине, что свойства объектовпрототипов встроенных типов не поддаются пе речислению в цикле for/in. Если бы функции не могли выполнять проверку на принадлежность встроенным типам и возвращать undefined, тогда обнаружилось бы, что встроенные типы не имеют методов, и для них всегда возвращалось бы значение true. Однако на типе Array следует остановиться особо. Вспомним, что в разделе 7.8 приводилась масса алгоритмов (таких как обход элементов массива), которые прекрасно работают с объектами, не являющимися настоящими массивами, а лишь подобными им. Метод грубого определения типа можно использовать для выяснения, является ли некоторый экземпляр объектом, напоминающим массив. Один из вариантов решения этой задачи приводится в примере 9.9. Пример 9.9. Проверка объектов, напоминающих массивы function isArrayLike(x) { if (x instanceof Array) return true; // Настоящий массив if (!("length" in x)) return false; // Массивы имеют свойство length if (typeof x.length != "number") return false; // Свойство length должно быть число, if (x.length < 0) return false; // причем неотрицательным if (x.length > 0) { // Если массив непустой, в нем как минимум должно быть свойство с именем length1 194 Глава 9. Классы, конструкторы и прототипы if (!((x.length1) in x)) return false; } return true; } 9.8. Пример: вспомогательный метод defineClass() Данная глава заканчивается определением вспомогательного метода define Class(), воплощающего в себе обсуждавшиеся темы о конструкторах, прототи пах, подклассах, заимствовании и предоставлении методов. Реализация метода приводится в примере 9.10. Пример 9.10. Вспомогательная функция для определения классов /** * defineClass() – вспомогательная функция для определения JavaScriptклассов. * * Эта функция ожидает получить объект в виде единственного аргумента. * Она определяет новый JavaScriptкласс, основываясь на данных в этом * объекте, и возвращает функциюконструктор нового класса. Эта функция * решает задачи, связанные с определением классов: корректно устанавливает * наследование в объектепрототипе, копирует методы из других классов и пр. * * Объект, передаваемый в качестве аргумента, должен иметь все * или некоторые из следующих свойств: * * name: Имя определяемого класса. * Если определено, это имя сохранится в свойстве classname объектапрототипа. * * extend: Конструктор наследуемого класса. В случае отсутствия будет * использован конструктор Object(). Это значение сохранится * в свойстве superclass объектапрототипа. * * construct: Функцияконструктор класса. В случае отсутствия будет использована новая * пустая функция. Это значение станет возвращаемым значением функции, * а также сохранится в свойстве constructor объектапрототипа. * * methods: Объект, который определяет методы (и другие свойства, * совместно используемые разными экземплярами) экземпляра класса. * Свойства этого объекта будут скопированы в объектпрототип класса. * В случае отсутствия будет использован пустой объект. * Свойства с именами "classname", "superclass" и "constructor" * зарезервированы и не должны использоваться в этом объекте. * * statics: Объект, определяющий статические методы (и другие статические * свойства) класса. Свойства этого объекта станут свойствами * функцииконструктора. В случае отсутствия будет использован пустой объект. * * borrows: Функцияконструктор или массив функцийконструкторов. * Методы экземпляров каждого из заданных классов будут * скопированы в объектпрототип этого нового класса, таким образом * новый класс будет заимствовать методы каждого из заданных классов. 9.8. Пример: вспомогательный метод defineClass() 195 * Конструкторы обрабатываются в порядке их следования, вследствие * этого методы классов, стоящих в конце массива, могут переопределить * методы классов, стоящих выше. * Обратите внимание: заимствуемые методы сохраняются * в объектепрототипе до того, как будут скопированы свойства * и методы вышеуказанных объектов. * Поэтому методы, определяемые этими объектами, могут * переопределить заимствуемые. При отсутствии этого свойства * заимствование методов не производится. * * provides: Функцияконструктор или массив функцийконструкторов. * После того как объектпрототип будет инициализирован, данная функция * проверит, что прототип включает методы с именами и количеством * аргументов, совпадающими с методами экземпляров указанных классов. * Ни один из методов не будет скопирован, она просто убедится, * что данный класс "предоставляет" функциональность, обеспечиваемую * указанным классом. Если проверка окажется неудачной, данный метод * сгенерирует исключение. В противном случае любой экземпляр нового класса * может рассматриваться (с использованием методики грубого определения типа) * как экземпляр указанных типов. Если данное свойство не определено, * проверка выполняться не будет. **/ function defineClass(data) { // Извлечь значения полей из объектааргумента. // Установить значения по умолчанию. var classname = data.name; var superclass = data.extend || Object; var constructor = data.construct || function( ) {}; var methods = data.methods || {}; var statics = data.statics || {}; var borrows; var provides; // Заимствование может производиться как из единственного конструктора, // так и из массива конструкторов. if (!data.borrows) borrows = []; else if (data.borrows instanceof Array) borrows = data.borrows; else borrows = [ data.borrows ]; // То же для предоставляемых свойств. if (!data.provides) provides = []; else if (data.provides instanceof Array) provides = data.provides; else provides = [ data.provides ]; // Создать объект, который станет прототипом класса. var proto = new superclass(); // Удалить все неунаследованные свойства из нового объектапрототипа. for(var p in proto) if (proto.hasOwnProperty(p)) delete proto[p]; // Заимствовать методы из классовсмесей, скопировав их в прототип. for(var i = 0; i < borrows.length; i++) { var c = data.borrows[i]; borrows[i] = c; // Скопировать методы из прототипа объекта c в наш прототип 196 Глава 9. Классы, конструкторы и прототипы for(var p in c.prototype) { if (typeof c.prototype[p] != "function") continue; proto[p] = c.prototype[p]; } } // Скопировать методы экземпляра в объектпрототип // Эта операция может переопределить методы, скопированные из классовсмесей for(var p in methods) proto[p] = methods[p]; // Установить значения зарезервированных свойств "constructor", // "superclass" и "classname" в прототипе proto.constructor = constructor; proto.superclass = superclass; // Свойство classname установить, только если оно действительно задано. if (classname) proto.classname = classname; // Убедиться, что прототип предоставляет все предполагаемые методы. for(var i = 0; i < provides.length; i++) { // для каждого класса var c = provides[i]; for(var p in c.prototype) { // для каждого свойства if (typeof c.prototype[p] != "function") continue; // только методы if (p == "constructor" || p == "superclass") continue; // Проверить наличие метода с тем же именем и тем же количеством // объявленных аргументов. Если метод имеется, продолжить цикл if (p in proto && typeof proto[p] == "function" && proto[p].length == c.prototype[p].length) continue; // В противном случае возбудить исключение throw new Error("Класс " + classname + " не предоставляет метод "+ c.classname + "." + p); } } // Связать объектпрототип с функциейконструктором constructor.prototype = proto; // Скопировать статические свойства в конструктор for(var p in statics) constructor[p] = data.statics[p]; // И в заключение вернуть функциюконструктор return constructor; } В примере 9.11 приводится фрагмент, который демонстрирует использование метода defineClass(). Пример 9.11. Использование метода defineClass() // Класс Comparable с абстрактным методом, благодаря которому // можно определить классы, "предоставляющие" интерфейс Comparable. var Comparable = defineClass({ name: "Comparable", methods: { compareTo: function(that) { throw "abstract"; } } }); // Класссмесь с универсальным методом equals() для заимствования var GenericEquals = defineClass({ 9.8. Пример: вспомогательный метод defineClass() 197 name: "GenericEquals", methods: { equals: function(that) { if (this == that) return true; var propsInThat = 0; for(var name in that) { propsInThat++; if (this[name] !== that[name]) return false; } // Убедиться, что объект this не имеет дополнительных свойств var propsInThis = 0; for(name in this) propsInThis++; // Если имеются дополнительные свойства, объекты равны не будут if (propsInThis != propsInThat) return false; // Похоже, что два объекта эквивалентны. return true; } } }); // Очень простой класс Rectangle, который предоставляет интерфейс Comparable var Rectangle = defineClass({ name: "Rectangle", construct: function(w,h) { this.width = w; this.height = h; }, methods: { area: function() { return this.width * this.height; }, compareTo: function(that) { return this.area( )  that.area( ); } }, provides: Comparable }); // Подкласс класса Rectangle, который вызывает по цепочке конструктор своего // надкласса, наследует методы надкласса, определяет свои методы экземпляра // и статические методы и заимствует метод equals(). var PositionedRectangle = defineClass({ name: "PositionedRectangle", extend: Rectangle, construct: function(x,y,w,h) { this.superclass(w,h); // вызов по цепочке this.x = x; this.y = y; }, methods: { isInside: function(x,y) { return x > this.x && x < this.x+this.width && y > this.y && y < this.y+this.height; } }, statics: { comparator: function(a,b) { return a.compareTo(b); } }, borrows: [GenericEquals] }); Модули и пространства имен В первые годы после появления язык JavaScript чаще всего использовался для создания маленьких и простых сценариев, встроенных прямо в вебстраницы. По мере становления вебброузеров и вебстандартов программы на языке JavaS cript становились все больше и все сложнее. В настоящее время многие Java Scriptсценарии используют в своей работе внешние модули, или библиотеки программного JavaScriptкода.1 К моменту написания этих строк ведутся работы по созданию модулей много кратного использования, распространяемых с открытыми исходными текстами на языке JavaScript. Сеть архивов JavaScript (JavaScript Archive Network, JSAN) реализуется по образу и подобию всемирной сети архивов Perl (Compre hensive Perl Archive Network, CPAN), причем предполагается, что она станет для JavaScript тем же, чем стала CPAN для языка программирования и сообще ства Perl. Подробную информацию о JSAN и примеры программного кода мож но найти на сайте http://www.openjsan.org. Язык JavaScript не предусматривает синтаксических конструкций, предназна ченных для создания и управления модулями, поэтому написание переносимых модулей многократного использования на языке JavaScript в значительной сте пени является вопросом следования некоторым основным соглашениям, описы ваемым в этой главе. Наиболее важное соглашение связано с концепцией пространства имен. Основ ная цель этой концепции – предотвратить конфликты имен, которые могут воз никнуть при одновременном использовании двух модулей, объявляющих гло 1 В базовом языке JavaScript отсутствуют какиелибо механизмы загрузки или подключения внешних модулей. Эту задачу берет на себя окружение, в которое встраивается интерпретатор JavaScript. В клиентском языке JavaScript задача решается с использованием тега