Язык javascript ч 1

Page 1

Современный учебник JavaScript Часть 1

Перед вами современный учебник по JavaScript, начиная с основ, включающий в себя DOM, замыкания, ООП, тонкости и много практических задач. От основ к продвинутой, грамотной разработке. Хорошего чтения! Часть 1. Изучаем JavaScript. Введение в JavaScript

1. 2. 3. 4. 5.

Что такое JavaScript?

Что умеет JavaScript? Что НЕ умеет JavaScript? В чем уникальность JavaScript? Тенденции развития. 1. HTML 5 2. EcmaScript 6. Недостатки JavaScript Давайте посмотрим, что такого особенного в JavaScript, почему именно он, и какие еще технологии существуют, кроме JavaScript. Что такое JavaScript? JavaScript изначально создавался для того, чтобы сделать web-странички «живыми». Программы на этом языке называются скриптами. Они подключаются напрямую к HTML и, как только загружается страничка — тут же выполняются. Программы на JavaScript — обычный текст. Они не требуют какой-то специальной подготовки. В этом плане JavaScript сильно отличается от другого языка, который называется Java.

Почему JavaScript? Когда создавался язык JavaScript, у него изначально было другое название: «LiveScript». Но тогда был очень популярен язык Java, и маркетологи решили, что схожее название сделает новый язык более популярным. Планировалось, что JavaScript будет эдаким «младшим братом» Java. Однако, история распорядилась по-своему, JavaScript сильно вырос, и сейчас это совершенно независимый язык, со своей спецификацией, которая называется ECMAScript, и к Java не имеет никакого отношения. У него много особенностей, которые усложняют освоение, но по ходу учебника мы с ними разберемся.


Чтобы читать и выполнять текст на JavaScript, нужна специальная программа — интерпретатор. Процесс выполнения скрипта называют «интерпретацией».

Компиляция и интерпретация, для программистов Строго говоря, для выполнения программ существуют «компиляторы» и «интерпретаторы». Когда-то между ними была большая разница. Компиляторы преобразовывали программу в машинный код, который потом можно выполнять. А интерпретаторы — просто выполняли. Сейчас разница гораздо меньше. Современные интерпретаторы перед выполнением преобразуют JavaScript в машинный код (или близко к нему), чтобы выполнялся он быстрее. То есть, по сути, компилируют, а затем запускают. Во все основные браузеры встроен интерпретатор JavaScript, именно поэтому они могут выполнять скрипты на странице. Но, разумеется, этим возможности JavaScript не ограничены. Это полноценный язык, программы на котором можно запускать и на сервере, и даже в стиральной машинке, если в ней установлен соответствующий интерпретатор. Что умеет JavaScript? Современный JavaScript — это «безопасный» язык программирования общего назначения. Он не предоставляет низкоуровневых средств работы с памятью, процессором, так как изначально был ориентирован на браузеры, в которых это не требуется. В браузере JavaScript умеет делать все, что относится к манипуляции со страницей, взаимодействию с посетителем и, в какой-то мере, с сервером: • Создавать новые HTML-теги, удалять существующие, менять стили элементов, прятать, показывать элементы и т.п. • Реагировать на действия посетителя, обрабатывать клики мыши, перемещение курсора, нажатие на клавиатуру и т.п. • Посылать запросы на сервер и загружать данные без перезагрузки страницы(эта технология называется "AJAX"). • Получать и устанавливать cookie, запрашивать данные, выводить сообщения…

• …и многое, многое другое! Что НЕ умеет JavaScript? JavaScript — быстрый и мощный язык, но браузер накладывает на его исполнение некоторые ограничения. Это сделано для безопасности пользователей, чтобы злоумышленник не мог с помощью JavaScript получить личные данные или как-то навредить компьютеру пользователя. В браузере Firefox существует способ «подписи» скриптов с целью обхода части ограничений, но он нестандартный и не кросс-браузерный. Этих ограничений нет там, где JavaScript используется вне браузера, например на сервере.


Большинство возможностей JavaScript в браузере ограничено текущим окном и страницей.

• JavaScript не может читать/записывать произвольные файлы на жесткий диск, копировать их или вызывать программы. Он не имеет прямого доступа к операционной системе. Современные браузеры могут работать с файлами, но эта возможность ограничена специально выделенной директорией — песочницей. Возможности по доступу к устройствам также прорабатываются в современных стандартах и, частично, доступны в некоторых браузерах. • JavaScript, работающий в одной вкладке, почти не может общаться с другими вкладками и окнами. За исключением случая, когда он сам открыл это окно или несколько вкладок из одного источника (одинаковый домен, порт, протокол). Есть способы это обойти, и они раскрыты в учебнике, но для этого требуется как минимум явное согласие обеих сторон. Просто так взять и залезть в произвольную вкладку с другого домена нельзя. • Из JavaScript можно легко посылать запросы на сервер, с которого пришла страничка. Запрос на другой домен тоже возможен, но менее удобен, т.к. и здесь есть ограничения безопасности. В чем уникальность JavaScript? Есть как минимум три замечательных особенности JavaScript: 1. Полная интеграция с HTML/CSS. 2. Простые вещи делаются просто. 3. Поддерживается всеми распространенными браузерами и включен по умолчанию. Этих трех вещей одновременно нет больше ни в одной браузерной технологии. Поэтому JavaScript и является самым распространенным средством создания браузерных интерфейсов. Тенденции развития. Перед тем, как вы планируете изучить новую технологию, полезно ознакомиться с ее развитием и перспективами. Здесь в JavaScript все более чем хорошо. HTML 5 HTML 5 — эволюция стандарта HTML, добавляющая новые теги и, что более важно, ряд новых


возможностей браузерам. Вот несколько примеров: • Чтение/запись файлов на диск (в специальной «песочнице», то есть не любые). • Встроенная в браузер база данных, которая позволяет хранить данные на компьютере пользователя. • Многозадачность с одновременным использованием нескольких ядер процессора. • Проигрывание видео/аудио, без Flash. • 2d и 3d-рисование с аппаратной поддержкой, как в современных играх. Многие возможности HTML5 все еще в разработке, но браузеры постепенно начинают их поддерживать. Тенденция: JavaScript становится все более и более мощным и возможности браузера растут в сторону десктопных приложений. EcmaScript Сам язык JavaScript улучшается. Современный стандарт EcmaScript 5 включает в себя новые возможности для разработки. Современные браузеры улучшают свои движки, чтобы увеличить скорость исполнения JavaScript, исправляют баги и стараются следовать стандартам. Тенденция: JavaScript становится все быстрее и стабильнее. Очень важно то, что новые стандарты HTML5 и ECMAScript сохраняют максимальную совместимость с предыдущими версиями. Это позволяет избежать неприятностей с уже существующими приложениями. Впрочем, небольшая проблема с HTML5 все же есть. Иногда браузеры стараются включить новые возможности, которые еще не полностью описаны в стандарте, но настолько интересны, что разработчики просто не могут ждать.

…Однако, со временем стандарт меняется и браузерам приходится подстраиваться к нему, что может привести к ошибкам в уже написанном (старом) коде. Поэтому следует дважды подумать перед тем, как применять на практике такие «супер-новые» решения. При этом все браузеры сходятся к стандарту, и различий между ними уже гораздо меньше, чем всего лишь несколько лет назад. Тенденция: все идет к полной совместимости со стандартом. Недостатки JavaScript Зачастую, недостатки подходов и технологий — это обратная сторона их полезности. Стоит ли упрекать молоток в том, что он — тяжелый? Да, неудобно, зато гвозди забиваются лучше. В JavaScript, однако, есть вполне объективные недоработки, связанные с тем, что язык, по выражению его автора (Brendan Eich) делался «за 10 бессонных дней и ночей». Поэтому некоторые моменты продуманы плохо, есть и откровенные ошибки (которые признает тот же Brendan). Конкретные примеры мы увидим в дальнейшем, т.к. их удобнее обсуждать в процессе освоения языка.


Пока что нам важно знать, что некоторые «странности» языка не являются чем-то очень умным, а просто не были достаточно хорошо продуманы в своё время. В этом учебнике мы будем обращать особое внимание на основные недоработки и «грабли». Ничего критичного в них нет, если знаешь — не наступишь. В новых версиях JavaScript (ECMAScript) эти недостатки постепенно убирают. Процесс внедрения небыстрый, в первую очередь из-за старых версий IE, но они постепенно вымирают. Современный IE в этом отношении несравнимо лучше.

Альтернативные технологии

1. 2. 3. 4. 5.

Java ActiveX/NPAPI, плагины и расширения для браузера Adobe Flash Dart Итого

Вместе с JavaScript на страницах используются и другие технологии. Самые извеcтные — это Flash, Java, ActiveX/NPAPI. Они бывают нужны, так как возможности JavaScript ограничены. Связка JavaScript с Java, Flash или ActiveX может помочь достигнуть более интересных результатов. Важно и то, что все эти технологии хорошо взаимодействуют между собой. Java Java — язык общего назначения, на нем можно писать самые разные программы. Для интернетстраниц есть особая возможность - написание апплетов. Апплет — это программа на языке Java, которую можно подключить к HTML при помощи тега applet:

1 <applet code="BTApplet.class" codebase="/files/tutorial/intro/alt/"> 2 <param name="nodes" value="50,30,70,20,40,60,80,35,65,75,85,90"> 3 <param name="root" value="50"> 4 </applet> Такой тег загружает Java-программу из файла BTApplet.class и выполняет ее с параметрами param. Конечно, для этого на компьютере должна быть установлена и включена среда выполнения Java. Статистика показывает, что примерно на 80% компьютеров Java будет работать. Если она включена у вас, то ниже вы увидите апплет в действии:

</p > <p><applet code="BTApplet.class" codebase="/files/tutorial/intro/alt/" width="600" height="300"><br /><param name="nodes" value="50,30,70,20,40,60,80,35,65,75,85,90" /><param name="root" value="50" /></applet></p>


<br />

Апплет выполняется в отдельной части страницы, в прямоугольном «контейнере». Все действия пользователя внутри него обрабатывает апплет. Контейнер, впрочем, может быть и спрятан, если апплету нечего показывать. Чем нам, JavaScript-разработчикам, может быть интересен Java? В первую очередь тем, что ограничения безопасности JavaScript в принципе нельзя преодолеть… А специальным образом подписанный (это несложно) Java-апплет может делать все, если посетитель ему доверяет. То есть, возможности, которые по ограничениям безопасности не поддерживает JavaScript, реализуемы через доверенный Java-апплет. При попытке сделать потенциально опасное действие — пользователь получает вопрос, который выглядит примерно так:

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


• Java может делать все от имени посетителя, совсем как установленная десктопная программа. В целях безопасности, потенциально опасные действия требуют подписанного апплета и доверия пользователя. • Java требует больше времени для загрузки • Среда выполнения Java должна быть установлена на компьютере посетителя и включена. Таких посетителей в интернет — около 80%. • Java-апплет не интегрирован с HTML-страницей, а выполняется отдельно. Но он может вызывать функции JavaScript. Подписанный Java-апплет - это возможность делать все, что угодно, на компьютере посетителя, если он вам доверяет. Можно вынести в него все вызовы, которым нужно обойти контекст безопасности, а для самой страницы использовать JavaScript. ActiveX/NPAPI, плагины и расширения для браузера ActiveX для IE и NPAPI для остальных браузеров позволяют писать плагины для браузера, в том числе на языке C. Как и в ситуации с Java-апплетом, посетитель поставит их в том случае, если вам доверяет. Эти плагины могут как отображать содержимое специального формата (плагин для проигрывания музыки, для показа PDF), так и взаимодействовать со страницей. ActiveX при этом еще и очень удобен в установке. Лично я - не фанат Microsoft, но видел отличные приложения, написанные на ActiveX и я могу понять, почему люди используют его и привязываются к IE. Adobe Flash Adobe Flash - кросс-браузерная платформа для мультимедиа-приложений, анимаций, аудио и видео. Flash-ролик - это скомпилированная программа, написанная на языке ActionScript. Ее можно подключить к HTML-странице и запустить в прямоугольном контейнере. Нам Flash интересен тем, что позволяет делать многое, что JavaScript пока не умеет, например работа с микрофоном, камерой, с буфером обмена. В отличие от технологий, рассмотренных ранее, он не может «все», но зато работает безопасно и не требует доверия посетителя. • Большие возможности для работы в сети(сокеты, UDP для P2P) • Поддержка мультмедиа: изображения, аудио, видео. Работа с веб-камерой и микрофоном.


• Flash должен быть установлен и включен. А на некоторых устройствах он вообще не поддерживается. • Flash не интегрирован с HTML-страницей, а выполняется отдельно. • Существуют ограничения безопасности, однако они немного другие, чем в JavaScript. JavaScript и ActionScript могут вызывать функции друг друга, поэтому обычно сайты используют JavaScript, а там, где он не справляется - можно подумать о Flash. Dart Язык Dart предложен компанией Google как замена JavaScript, у которого, по выражению создателей Dart, есть фатальные недостатки. Сейчас этот язык, хотя и доступен, находится в стадии разработки и тестирования. Многие из возможностей еще ожидают своей реализации, есть ряд проблем. Другие ведущие интернеткомпании объявляли о своей незаинтересованности в Dart. ..Но в будущем он может составить конкуренцию JS, если его доведут до ума… Ну или если Google завоюет мир

.

Итого Язык JavaScript уникален благодаря своей полной интеграции с HTML/CSS. Он работает почти у всех посетителей. ..Но хороший JavaScript-программист не должен забывать и о других технологиях. Ведь наша цель — создание хороших приложений, и здесь Flash, Java, ActiveX/NPAPI имеют свои уникальные возможности, которые можно использовать вместе с JavaScript. На Dart сейчас тратить время не стоит, но, возможно, будет иметь смысл поглядеть на него через годик-другой.

Книги по JS, HTML/CSS и не только

1. 2. 3. 4. 5. 6. 7.

CSS JavaScript jQuery Объектно-ориентированное программирование Регулярные выражения Алгоритмы и структуры данных Разработка и организация кода

При освоении JavaScript вам понадобятся как смежные технологии, так и знание общей методологии программирования.


Так как это учебник, то здесь вы найдете конкретную литературу, с которой целесообразно начинать изучение. Всего несколько книг на каждую тему. Из большего количества все равно пришлось бы выбирать. Кстати, по всем книжкам, особенно тех, которые касаются технологий, всегда ищите последнее издание. P.S. Скачать книги здесь нельзя. Эта страница содержит только рекомендации. CSS CSS стоит изучать по одной из этих книг. Можно сразу по обеим.

• CSS ручной работы. Дэн Седерхольм.

• Большая книга CSS. Дэвид Макфарланд. Для того, чтобы разобраться в конкретных вопросах CSS, и в качестве справочника полезна книга Эрика Мейера CSS. Каскадные таблицы стилей. Подробное руководство., а также стандарт CSS 2.1. JavaScript Полезное чтение о языке, встроенных методах и конструкциях JavaScript:

• JavaScript. Подробное руководство. Дэвид Флэнаган.

• JavaScript. Шаблоны. Стоян Стефанов. jQuery Кроме документации:

• jQuery. Подробное руководство по продвинутому JavaScript. Бер Бибо, Иегуда Кац. Объектно- ориентированное программирование Объектно-ориентированное программирование (ООП) — это концепция построения программных систем на основе объектов и взаимодействия между ними. При изучении ООП рассматриваются полезные архитектурные приёмы, как организовать программу более эффективно. Умение создавать объект, конструктор, вызывать методы — это основные, самые базовые «кирпичики». Их следует освоить первыми, например используя этот учебник. Затем, когда основы более-менее освоены, стоит уделить внимание теории объектно-ориентированной разработки:

• Объектно-ориентированный анализ и проектирование с примерами приложений. Гради Буч и др..

• Приемы объектно-ориентированного проектирования. Паттерны проектирования. Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес. Регулярн ые в ыражения

• Регулярные выражения. Джеффри Фридл. Эта книга описывает более широкий класс регэкспов, по сравнению с текущим JavaScript. С одной стороны, какая-то информация будет лишней, с другой — регулярные выражения вообще


очень важная и полезная тема. Алгоритмы и структур ы данн ых

• Алгоритмы. Построение и анализ. Т. Кормен, Ч. Лейзерсон, Р. Ривест, К. Штайн. Есть и другая классика, например «Искусство программирования», Дональд Кнут. Она требует более серьёзной математической подготовки. Будьте готовы читать и вникать долго и упорно. Результат — апгрейд мозговых извилин и общего умения программировать. Серьёзно, оно того стоит. Разработка и организация кода

• Совершенный код. Стив Макконнелл. Это желательно изучать уже после получения какого-то опыта в программировании.

Терминология, поддержка, справочники

1. 2. 3. 4. 5.

Уровни поддержки

HTML 5 JS-Фреймворки Справочники, и как в них искать Спецификации 1. Спецификация ECMAScript 2. Спецификации HTML/CSS 6. Итого В этом разделе мы познакомимся с важными терминами и справочниками — тем, что обязательно понадобится при создании веб-приложений. Уровни поддержки Иногда кросс-браузерная разработка становится непростым делом. Поэтому браузеры разделяют по степени поддержки. Например: A. Последние версии Firefox, Internet Explorer, Safari/Chrome Идеальная поддержка. B. Opera и предыдущие версии современных браузеров Возможны незначительные «помарки», не ломающие функционал. C. Старые браузеры Поддерживаются только базовые возможности D. Очень старые браузеры, текстовые браузеры Не поддерживаются. В зависимости от целевой аудитории сайта, Opera может включаться в уровень A, классификация может меняться, учитывать мобильные браузеры и т.п. При любой веб-разработке важно определиться, что поддерживается и до какой степени. В задачах учебника предусматривается поддержка современных браузеров, включая Firefox,


Safari/Chrome, Opera, IE8+ в группе A, а также поддержка IE7 по группе B. Вы можете выбрать свои уровни поддержки и решать задачи, соответственно. ориентируясь на них.

Терминология Иногда в интернет можно встретить названия не браузеров, а их движков. Полезно понимать, что есть что. Gecko

Открытый движок Firefox и некоторых менее известных браузеров от компании Mozilla.

Webkit

Общий браузерный движок браузеров Safari и Chrome. Он отвечает за показ HTML/CSS, возможности HTML 5, кроме собственно языка JavaScript. Здесь браузеры расходятся: Chrome использует Google V8, а Safari — свой собственный закрытый движок Squirrelfish. Но так как оба этих браузера основаны на Webkit, то между ними много общего.

Presto

Старый движок браузера Opera. Этот браузер находится в процессе перехода на Webkit, после чего предыдущие трюки с Opera станут неактуальны.

Trident

Закрытый движок браузера Internet Explorer. Trident ужасен в IE6 и IE7, улучшен в IE8, в IE9 он, за небольшим исключением, полностью поддерживает современный стандарт, ну а в IE10 вообще вполне хорош.


HTML 5 Как вам, наверное, известно из опыта работы с HTML, существует два режима документа: нормальныйStandards Mode и старый Quirks mode. По правде говоря, есть еще и третий режим, который называется Almost Standards Mode, Браузер выбирает режим в соответствии с директивой DOCTYPE, прописанной в начале HTML документа. Если для вас это новость - про режимы рендеринга можно почитать, например, на википедии, в статье Wikipedia quirks mode page. Для нас здесь важно одно — HTML обязан начинаться с DOCTYPE. Мы будем использовать DOCTYPE для HTML 5, который, впрочем, поддерживается и старыми браузерами:

1 <!DOCTYPE HTML> 2 <html> 3 ... 4 </html>

DOCTYPE влияет на всё! DOCTYPE влияет не только на HTML/CSS — JavaScript также зависит от него. Использование неподходящего DOCTYPE или его отсутствие может стоить времени на отладку. Просто используйте <!DOCTYPE HTML>. JS-Фреймворки Большая часть разработки осуществляется при помощи фреймворков — специальных библиотек, которые позволяют упростить код, а также содержат большое количество готовых компонентов. Таких фреймворков много. Вот лишь некоторые из них: • jQuery • Dojo • Ext.JS • Angular.JS • Backbone.JS

• … Бывают фреймворки общего назначения, которые содержат всего понемногу, бывают — направленные на конкретные аспекты разработки, например, создание интерфейсов или мобильные устройства.


Выбор конкретного фреймворка зависит от задачи. Фреймворк — это инструмент. Для решения задач бывает нужно несколько инструментов, а чем шире задача — тем больше. Сначала нужно изучить сам JavaScript, затем работу с браузером, а уже потом приступать к фреймворкам. Справочники, и как в них искать Есть три основных справочника по JavaScript на английском языке:

1. Mozilla Developer Network — содержит информацию, верную для основных браузеров. Также там присутствуют расширения только для Firefox, они помечены. Когда мне нужно быстро найти «стандартную» информацию по RegExp - ввожу в Google«RegExp MDC», и ключевое слово «MDC» (Mozilla Developer Center) приводит к информации из этого справочника.

2. MSDN — справочник от Microsoft. Там много информации, в том числе и по JavaScript (они называют его «JScript»). Если нужно что-то, специфичное для IE — лучше лезть сразу туда. Например, для информации об особенностях RegExp в IE — полезное сочетание: «RegExp msdn». Иногда к поисковой фразе лучше добавить термин «JScript»: «RegExp msdn jscript».

3. Safari Developer Library — менее известен и используется реже, но в нём тоже можно найти ценную информацию. Есть ещё справочники, не от разработчиков браузеров, но тоже хорошие:

1. http://help.dottoro.com — содержит подробную информацию по HTML/CSS/JavaScript. 2. http://javascript.ru/manual — справочник по JavaScript на русском языке, он содержит основную информацию по языку, без функций для работы с документом. К нему можно обращаться и по адресу, если знаете, что искать. Например, так: http://javascript.ru/RegExp.

3. http://www.quirksmode.org — информация о поддержке тех или иных возможностей и несовместимостях. Для поиска можно пользоваться комбинацией «quirksmode onkeypress» в Google. Спецификации Спецификация — это самый главный, определяющий документ, в котором написано, как себя ведёт JavaScript, браузер, CSS и т.п. Если что-то непонятно, и справочник не даёт ответ, то спецификация, как правило, раскрывает тему гораздо глубже и позволяет расставить точки над i. Спецификация ECMAScript Спецификация (формальное описание синтаксиса, базовых объектов и алгоритмов) языка Javascript называется ECMAScript.


Ее перевод есть на сайте в разделе стандарт языка.

Почему не просто "JavaScript" ? Вы можете спросить: «Почему спецификация для JavaScript не называется просто«JavaScript», зачем существует какое-то отдельное название?» Всё потому, что JavaScript™ — зарегистрированная торговая марка, принадлежащая корпорации Oracle. Название «ECMAScript» было выбрано, чтобы сохранить спецификацию независимой от владельцев торговой марки.

Спецификация может рассказать многое о том, как работает язык, и является самым фундаментальным, доверенным источником информации. Мы живем во время, когда все быстро изменяется. Современный стандарт — это ECMA-262 5.2 (или просто ES5), поддерживается всеми современными браузерами. Не за горами — новая спецификация ES6, в которой предусмотрены еще много полезных возможностей, делающих разработку быстрее и веселее Спецификации HTML/CSS JavaScript — язык общего назначения, поэтому в спецификации ECMAScript нет ни слова о браузерах. Соответствующую информацию вы можете найти на сайте w3.org. Там расположены стандарты HTML, CSS и многие другие. К сожалению, найти в этой куче то, что нужно, может быть нелегко, особенно когда неизвестно в каком именно стандарте искать. Самый лучший способ — попросить Google с указанием сайта. Например, для поиска document.cookie набрать document.cookie site:w3.org. Последние версии стандартов расположены на домене dev.w3.org. Итого Итак, посмотрим какие у нас есть источники информации. Справочники:

• Mozilla Developer Network — информация для Firefox и большинства браузеров. Google-комбо: "RegExp MDC", ключевое слово «MDC».


• MSDN — информация по IE. Google-комбо: "RegExp msdn". Иногда лучше добавить термин «JScript»:"RegExp msdn jscript".

• Safari Developer Library — информация по Safari. • http://help.dottoro.com — подробная информация по HTML/CSS/JavaScript с учетом браузерной совместимости. Google-комбо: "RegExp dottoro".

• http://javascript.ru/manual — справочник по JavaScript на русском языке. К нему можно обращаться и по адресу, если знаете, что искать. Например, так: http://javascript.ru/RegExp. Google-комбо: "RegExp site:javascript.ru". Спецификации содержат важнейшую информацию о том, как оно «должно работать»:

• JavaScript, современный стандарт ES5 (англ), и предыдущий ES3 (рус). • HTML/DOM/CSS — на сайте w3.org. Google-комбо: "document.cookie site:w3.org". То, как оно на самом деле работает и несовместимости:

• Смотрите http://www.quirksmode.org/. Google-комбо: "innerHeight quirksmode". ‹ Книги по JS, HTML/CSS и не только Редакторы для кода ›

См. также • • • • •

MSDN Safari Developer Library Mozilla Developer Network http://help.dottoro.com http://javascript.ru/manual

Редакторы для кода

1. 2. 3. 4.

IDE Лёгкие редакторы Top 3 P.S.

Есть два вида редакторов: IDE и «лёгкие». Разница между ними — в том, что IDE загружает весь проект целиком, поэтому может предоставлять автодополнение по функциям всего проекта, удобную навигацию по его файлам и т.п. Лёгкие редакторы — редактируют конкретный файл (или несколько) и знать не знают о связях между ними. Некоторые IDE можно использовать как лёгкие редакторы, но обычно IDE сложнее, тяжелее и работают медленнее.


Обязательно нужен хороший редактор. Тот, который вы выберете должен иметь в своем арсенале: 1. Подсветку синтаксиса. 2. Автодополнение. 3. «Фолдинг» (от англ. folding) — возможность скрыть-раскрыть блок кода. IDE Если вы еще не задумывались над этим, присмотритесь к следующим вариантам.

• Продукты IntelliJ: WebStorm, а также в зависимости от дополнительного языка программирования PHPStorm (PHP), IDEA (Java) и другие. • Visual Studio, в сочетании с разработкой под .NET (Win)

• Продукты на основе Eclipse, в частности Aptana и Zend Studio • Komodo IDE и его облегчённая версия Komodo Edit. • Netbeans Почти все они, за исключением Visual Studio, кросс-платформенные. Сортировка в этом списке ничего не означает. Выбор осуществляется по вкусу и по другим технологиям, которые нужно использовать вместе с JavaScript. Большинство IDE — платные. Но их стоимость невелика, по сравнению с зарплатой вебразработчика, поэтому ориентироваться можно на удобство. Лёгкие редактор ы Такие редакторы не такие мощные, как IDE, но они быстрые и простые, мгновенно стартуют. Как правило, под IDE понимают мощный редактор, с упором на проекты. А «лёгкие» редакторы предназначены в первую очередь для редактирования отдельных файлов. Но на практике граница между IDE и «лёгким» редактором может быть размыта, и спорить что именно редактор, а что IDE — не имеет смысла. Достойны внимания:

• Sublime Text (кросс-платформенный, платный). • TextMate (Mac, платный)

• SciTe простой, легкий и очень быстрый (Windows, бесплатный). • Notepad++ (Windows, бесплатный). • Vim, Emacs. Если умеете их готовить. Выберите любой редактор из перечисленных выше, главное чтобы он что-то умел, кроме простого блокнота. Top 3 Лично мои любимые редакторы:

• Sublime Text. • Редакторы от Jetbrains: WebStorm, а также в зависимости от дополнительного языка программирования PHPStorm (PHP), IDEA (Java) и другие. • Visual Studio, если разработка идёт под платформу .NET (Win) Если не знаете, что выбрать — можно посмотреть на них


P.S. В списках выше перечислены редакторы, которые используют многие мои знакомые — хорошие разработчики. Конечно, существуют и другие отличные редакторы, если вам что-то нравится — пользуйтесь. Выбор редактора, как и любого инструмента, во многом индивидуален и зависит от ваших проектов, привычек, личных предпочтений.

Sublime Text: шпаргалка

1. Горячие клавиши 2. Плагины Одним из наиболее мощных, и при этом простых, редакторов является Sublime Text. В нём хорошо развита система команд и горячих клавиш. На этой странице размещена «шпаргалка» с плагинами и самыми частыми сочетаниями клавиш, которые сильно упрощают жизнь. Горячие клавиши Для наибольшего удобства «шпаргалка» должна быть распечатана и повешена перед глазами, поэтому она сделана в виде 3-колоночного PDF. Скачать шпаргалку в формате PDF Шпаргалка пока под Mac. Для Windows сочетания похожи, обычно вместо Mac-клавиши Cmd под Windows будет Ctrl. А если в сочетании есть и Cmd и Ctrl, то под Windows будет Ctrl + Shift. Вы часто используете сочетание, но его нет в списке? Поделитесь им в комментариях! Плагин ы Мои любимые плагины: • • • • • • •

Package Control sublime-emmet JsFormat SideBarEnhancements AdvancedNewFile sublime-jsdocs SublimeCodeIntel

Остальные: • • • • • • • •

Alignment CSSComb EncodingHelper GoToRecent HTML5 jQuery Prefixr View In Browser


Чтобы узнать о плагине подробнее — введите его название в Google. Есть и другие хорошие плагины, кроме перечисленных. Кроме того, Sublime позволяет легко писать свои плагины и команды.

Установка браузеров, JS-консоль

1. Firefox 1. Установка Firebug 2. Включите консоль 3. Просмотр ошибок 2. Internet Explorer 1. Включаем отладку 2. Просмотр ошибок 3. Другие браузеры 1. Google Chrome 2. Safari 3. Opera 4. IE<8 Мы будем писать скрипты, которые поддерживают все современные браузеры. Хотя они и стремятся поддерживать стандарты, но все-таки бывают отличия. Как минимум, вам потребуются: Firefox, Chrome и Internet Explorer. Если вы работаете в Linux или MacOS, то вам потребуется виртуальная машина с Windows для IE. Большинство разработчиков сначала пишут скрипты под Firefox или Chrome. Если все работает, скрипт тестируется в остальных браузерах. Firefox Для разработки в Firefox используется расширение Firebug. Его нужно поставить после установки браузера. Установка Firebug Поставьте его со страницы:

1. https://addons.mozilla.org/ru/firefox/addon/firebug/ Перезапустите браузер. Firebug появится в правом-нижнем углу окна:


Если иконки не видно — возможно, у вас выключена панель расширений. Нажмите Ctrl+\ для ее показа. Ну а если ее нет и там, то нажмите F12 —- это горячая клавиша для запуска Firebug, мышкой его обычно никто не запускает. Включите консоль Итак, откройте Firebug. Здесь иллюстрации на английском, русский вариант аналогичен. Консоль вначале выключена. Нужно включить её в меню Консоль -> панель включена:

Просмотр ошибок С открытым Firebug зайдите на страницу с ошибкой: bug.html. Вы можете посмотреть её исходный код, нажав Ctrl + U. Консоль покажет ошибку:


В данном случае код lalala непонятен интерпретатору и вызвал ошибку. Кликните на строчке с ошибкой и браузер покажет исходный код. При необходимости включайте дополнительные панели. Об основных возможностях можно прочитать на сайте firebug.ru. Internet Explorer В IE начиная с версии 8 (а лучше 9) есть похожий отладчик. По умолчанию он отключен. Включаем отладку Зайдите в меню. IE 8 Tools -> Internet Options (рус. Инструменты -> Свойства обозревателя)


IE 9 Колесико в правом-верхнем углу - Свойства обозревателя:

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


Перезапустите браузер. Просмотр ошибок Зайдите на страницу с ошибкой: bug.html. Появится окно с предложением начать отладку. Нажмите «Да» — и вы в отладчике.

Теперь отладчик вместе с другими инструментами разработки доступен по кнопке F12. Другие браузер ы Google Chrome Горячие клавиши: Ctrl+Shift+I, Ctrl+Shift+J. Меню Инструменты -> Инструменты разработчика:


Safari Горячие клавиши: Ctrl+Shift+I, Ctrl+Alt+C. Для доступа к функционалу разработки через меню: 1. В Safari первым делом нужно активировать меню разработки. Откройте меню, нажав на колесико справа-сверху и выберите Настройки. Затем вкладка Дополнительно:

Отметьте Показывать меню "Разработка" в строке меню. Закройте настройки.


2. Нажмите на колесико и выберите Показать строку меню. Инструменты будут доступны в появившейся строке меню, в пункте Разработка. Opera В Opera работает горячая клавиша Ctrl+Shift+I. Можно включить и доступ через меню.

Для этого сначала нужно включить меню: Теперь в меню: Инструменты -> Дополнительно -> Opera Dragonfly. Вы на месте. IE<8 Для IE<8, основной инструмент разработки - это Microsoft Script Debugger. У него есть 4 варианта, самый лучший входит в поставку Microsoft Visual Studio (платной, не Express). При установке студии отключите все дополнительные опции, чтобы не ставить лишнего. Также есть Internet Explorer Developer Toolbar для работы с документом. Она вам понадобится для поддержки IE7 и, возможно, IE6.

Тестирование в старых браузерах

1. 2. 3. 4.

Internet Explorer Firefox Chrome Opera

Бывает, что ошибка возникает в старой версии популярного браузера, но она настолько важна, а


посетителей с этой версией так много, что её хорошо бы поправить. Особенно это касается IE, у которого между версиями самые существенные различия и, в меньшей степени, других браузеров. Здесь мы рассмотрим, как поставить несколько версий различных браузеров одновременно, для разработки и отладки. Internet Explorer Браузер Internet Explorer тесно интегрирован с системой. Особенно это касается IE<9. Идёт это всё с древних времён, когда Микрософт обвиняли в монополизме и пытались заставить изъять ряд компонент, в том числе браузер, из операционной системы. А Микрософт делать этого не хотела, аргументируя тем, что «браузер слишком сильно интегрирован в ОС, является её частью и изъять его ну никак нельзя». И, соответственно, старалась интегрировать Варианты разработки и тестирования под несколько версий IE: Режим эмуляции в IE8+ В IE8+ есть режим эмуляции, переключается в панели инструментов, но работает он неполноценно. По опыту, он на 100% надёжен в вёрстке, но примерно в 95% в JavaScript. Некоторые ошибки не воспроизводятся. IETester Один из лучших проектов по эмуляции старых IE. Содержит библиотеки от них, по возможности старается отделить один IE от другого, но это не всегда удаётся. Тоже хорошо работает с вёрсткой, но в JavaScript не все глюки и особенности IE воспроизводятся, и кроме того, появляются свои, дополнительные, ошибки. Виртуальная машина, например VirtualPC, VMware, VirtualBox, Parallels (Mac) В виртуальную машину ставится нужная версия Windows с соответствующим IE. Это — единственный полностью надёжный способ. Также существуют утилиты, позволяющие запускать приложение без явного входа в виртуальную машину, на одной панели задач с другими. Как будто оно установлено в основную ОС. Например ThinApp, Sandboxie. Можно ими пользоваться, но возможны неудобства, если к браузеру ставятся плагины, отладчик Visual Studio (IE6,7), профайлер (DynaTrace AJAX) и т.п. Можно использовать несколько подходов одновременно: режим эмуляции при разработке, а при необходимости или для 100%-й гарантии — запускать виртуальную машину с нужной версией IE. Firefox С браузером Firefox проще, но иногда и между его версиями бывают существенные различия. К счастью, Firefox можно поставить в нескольких экземплярах на одну машину. Проблем не будет. Обычно поддерживаются две версии Firefox: «текущая стабильная (или даже бета)» и «минимально допустимая». При этом в первой включено автообновление, а во второй — нет. Единственное, что следует иметь в виду — это «профиль», т.е. директорию, в которой браузер хранит свои настройки и расширения. Физически он хранится в домашней директории пользователя. Под разные Firefox они могут быть разными, поэтому нужно указывать профиль


явно. Разным версиям Firefox должны соответствовать разные профили. 1. Профиль нужно сначала создать. Для этого при запуске Firefox используйте ключ:

firefox.exe --profilemanager Запуск можно осуществлять из директории установки или по полному пути к Firefox, чтобы это точно была нужная версия. Firefox выведет список профилей и позволит создать новый. 2. В дальнейшем создаётся «ярлык» (или его аналог, bash-файл и т.п.), явно запускающий Firefox с нужным профилем:

C:\Program Files\FirefoxLatest\firefox.exe -P latest 3. Чтобы запускать несколько версий Firefox одновременно, нужно добавить опцию --noremote. Chrome Как правило, хватает одной — последней версии Chrome. Тем не менее, иногда хочется, например, иметь последнюю stable-версию и dev для тестирования самого нового функционала. Под MacOS/Linux — возможно, всё совсем просто (напишите в комментариях, как?), а под Windows я использую Chrome Portable. Opera Разные «мажорные», то есть отличающиеся главных цифрах (не после точки) версии Opera, как правило, могут жить на одной машине. В качестве альтернативы можно использовать Opera Portable.

Привет, мир!

1. Тег SCRIPT 2. Внешние скрипты В этой статье мы создадим простой скрипт и посмотрим, как он работает. Тег SCRIPT Программы на языке JavaScript можно вставить в любое место HTML при помощи тега SCRIPT. Например:

01 <!DOCTYPE HTML> 02 <html> 03 <head> 04 <!-- Тег meta для указания кодировки -->


05 <meta charset="utf-8"> 06 </head> 07 <body> 08 09 <p>Начало документа...</p> 10 11 <script> 12 alert('Привет, Мир!'); 13 </script> 14 15 <p>...Конец документа</p> 16 17 </body> 18 </html> Открыть код в новом окне Этот пример использует следующие элементы: <script> ... </script> Тег script содержит исполняемый код. Предыдущие стандарты HTML требовали обязательного указания атрибута type, но сейчас он уже не нужен. Достаточно просто <script>. Браузер, когда видит <script>:

1. Начинает отображать страницу, показывает часть документа до script 2. Встретив тег script, переключается в JavaScript-режим и не показывает, а исполняет его содержимое. 3. Закончив выполнение, возвращается обратно в HTML-режим и отображает оставшуюся часть документа. Попробуйте этот пример в действии, обратите внимание что пока браузер не выполнит скрипт - он не может отобразить часть страницы после него. alert(...) Отображает окно с сообщением и ждет, пока посетитель не нажмет «Ок»

Кодировка и тег META При попытке сделать такой же файл у себя на диске и запустить, вы можете столкнуться с проблемой — выводятся «кракозяблы», «квадратики» и «вопросики» вместо русского текста. Чтобы всё было хорошо, нужно:


1. Убедиться, что в HEAD есть строка <meta charset="utf-8">. Если вы будете открывать файл с диска, то именно он укажет браузеру кодировку.

2. Убедиться, что редактор сохранил файл в кодировке UTF-8, а не, скажем, вwindows-1251. На английском соответствующий параметр может называться «charset» или «encoding».

Указание кодировки — часть обычного HTML, к JavaScript не имеет отношения.

Очень важно не только читать, но и тестировать, пробовать писать что-то самому. Решите задачку, чтобы удостовериться, что вы все правильно поняли. 5

Сделайте страницу, которая выводит «Я - JavaScript!», т.е. работает вот так: tutorial/browser/script/alert/index.html. Создайте ее на диске, откройте в браузере, убедитесь, что все работает. Решение tutorial/browser/script/alert/index.html. [Открыть задачу в новом окне]

Современная разметка для тега SCRIPT В старых скриптах оформление тега SCRIPT было немного сложнее. В них можно встретить следующие элементы: Атрибут <script type=...>

В отличие от HTML5, стандарт HTML 4 требовал обязательного указания этого атрибута. Выглядел он так: type="text/javascript". Если вы укажете некорректные данные в атрибуте type, например<script type="text/html">, то содержимое тега не будет отображено. Но его можно получить средствами JavaScript. Этот хитрый способ используют для добавления служебной информации на страницу.


Атрибут <script language=...>

Этот атрибут ставить не обязательно, т.к. язык по умолчанию — JavaScript.

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

В старых руководствах и книгах иногда рекомендуют использовать HTMLкомментарии внутри SCRIPT, чтобы спрятать Javascript от браузеров, которые не поддерживают его.

Выглядит это примерно так:

<script type="text/javascript"><!-... //--></script>

Браузер, для которого предназначались такие трюки, очень старый Netscape, давно умер. Поэтому в этих комментариях нет нужды. Внешние скрипт ы Если JavaScript-кода много — его выносят в отдельный файл, который подключается в HTML:

<script src="/path/to/script.js"></script> Здесь /path/to/script.js - это абсолютный путь к файлу, содержащему скрипт (из корня сайта). Браузер сам скачает скрипт и выполнит. Например:

01 <html> 02 <head> 03 <meta charset="utf-8"> 04 <script src="/files/tutorial/browser/script/rabbits.js"></script> 05 </head> 06


07 <body> 08 <script> 09 count_rabbits(); 10 </script> 11 </body> 12 13 </html> Открыть код в новом окне Содержимое файла /files/tutorial/browser/script/rabbits.js:

1 function count_rabbits() { 2 for(var i=1; i<=3; i++) { 3 alert("Кролик номер "+i) 4 } 5} Открыть код в новом окне Можно указать и полный URL, например:

<script src="http://code.jquery.com/jquery.js"></script> Вы также можете использовать путь относительно текущей страницы, например src="script.js"если скрипт находится в том же каталоге, что и страница. Чтобы подключить несколько скриптов, используйте несколько тегов:

<script src="/js/script1.js"></script> <script src="/js/script2.js"></script> ... Как правило, в HTML пишут только самые простые скрипты, а сложные выносят в отдельный файл. Благодаря этому один и тот же скрипт, например, меню или библиотека функций, может использоваться на разных страницах.

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

Если указан атрибут src, то содержимое тега игнорируется. В одном теге SCRIPT нельзя одновременно подключить внешний скрипт и указать код.


Если указан атрибут src, то содержимое тега игнорируется. В одном теге SCRIPT нельзя одновременно подключить внешний скрипт и указать код. Вот так не cработает:

<script src="file.js"> alert(1); // если указан src, то внутренняя часть скрипта игнорируется </script> Нужно выбрать: либо SCRIPT идёт с src, либо содержит код. Тег выше следует разбить на два: один — с src, другой с кодом:

1 <script src="file.js"></script> 2 <script> 3 alert(1); 4

</script>

Структура кода

1. Команды 2. Комментарии В этой главе мы рассмотрим общую структуру кода, команды и их разделение. Команд ы Например, можно вместо одного вызова alert сделать два:

1 alert('Привет'); alert('Мир'); Как правило, новая команда занимает отдельную строку — так код лучше читается:

1 alert('Привет'); 2 alert('Мир'); Точку с запятой во многих случаях можно не ставить, если есть переход на новую строку. Так тоже будет работать:

1 alert('Привет') 2 alert('Мир')


В этом случае JavaScript интерпретирует переход на новую строчку как разделитель команд и автоматически вставляет «виртуальную» точку с запятой между ними. Однако, внутренние правила по вставке точки с запятой не идеальны. В примере выше они сработали, но в некоторых ситуациях JavaScript «забывает» вставить точку с запятой там, где она нужна. Таких ситуаций не так много, но они все же есть, и ошибки, которые при этом появляются, достаточно сложно исправлять. Поэтому рекомендуется точки с запятой ставить. Сейчас это, фактически, стандарт. Комментарии Со временем программа становится большой и сложной. Появляется необходимость добавитькомментарии, которые объясняют, что происходит и почему. Комментарии могут находиться в любом месте программы и никак не влияют на ее выполнение. Интерпретатор JavaScript попросту игнорирует их. Однострочные комментарии начинаются с двойного слэша //. Текст считается комментарием до конца строки:

1 // Команда ниже говорит "Привет" 2 alert('Привет'); 3 4 alert('Мир'); // Второе сообщение выводим отдельно Многострочные комментарии начинаются слешем-звездочкой "/*" и заканчиваются звездочкойслэшем "*/", вот так:

1 /* Пример с двумя сообщениями. 2 Это - многострочный комментарий. 3 */ 4 alert('Привет'); 5 alert('Мир'); Все содержимое комментария игнорируется. Если поместить код внутрь /* ... */ или после // — он не выполнится.

1 /* Закомментировали код 2 alert('Привет'); 3 */ 4 alert('Мир');

Вложенные комментарии не поддерживаются!


В этом коде будет ошибка:

1 /* 2 alert('Привет'); /* вложенный комментарий ?!? */ 3 */ 4 alert('Мир');

В многострочных комментариях всё очень просто — комментарий длится от открытия/* до закрытия */. Таким образом, код выше будет интерпретирован так:

Комментарий открывается /* и закрывается */:

/* alert('Привет'); /* вложенный комментарий ?!? */ Код (лишние символы сверху вызывают ошибку):

*/ alert('Мир');

Виды комментариев Существует три типа комментариев.

1. Первый тип отвечает на вопрос «Что делает эта часть кода?». Эти комментарии бывают особенно полезны, если используются неочевидные алгоритмы.

2. Второй тип комментариев отвечает на вопрос «Почему я выбрал этот вариант решения задачи?». И он гораздо важнее. При создании кода мы принимаем много решений, выбираем лучший вариант из нескольких возможных. Иногда для правильного выбора нужно многое изучить, посмотреть.


Когда вы остановились на чём-то — не выбрасывайте проделанную работу, укажите, хотя бы кратко, что вы посмотрели и почему остановились именно на этом варианте. Особенно это важно, если выбранный вариант не очевиден, а существует другое, более очевидное, но неправильное решение. Ведь в будущем, вернувшись к этому коду, мы можем захотеть переписать «сложное» решение на более «явное» или «оптимальное», тут-то и комментарий и поможет понять, что к чему. Например: «Я выбрал здесь анимацию при помощи JavaScript вместо CSS, поскольку IE именно в этом месте ведёт себя некорректно». 3. Третий тип комментариев возникает, когда мы в одном месте кода делаем вычисления или присвоения переменных, неочевидным образом использованные совсем в другом месте кода. Например: «Эти значения отформатированы именно так, чтобы их можно было передать на сервер».

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

1. Переменная 1. Аналогия из жизни 2. Копирование значений 2. Важность директивы var 3. Константы В зависимости от того, для чего вы делаете скрипт, понадобится работать с информацией. Если это электронный магазин - то это товары, корзина. Если чат - посетители, сообщения и так далее. Чтобы хранить информацию, используются переменные. Переменная Переменная состоит из имени и выделенной области памяти, которая ему соответствует. Для объявления или, другими словами, создания переменной используется ключевое слово var:

var message;


После объявления, можно записать в переменную данные:

var message; message = 'Привет'; // сохраним в переменной строку Эти данные будут сохранены в соответствующей области памяти и в дальнейшем доступны при обращении по имени:

1 var message; 2 message = 'Привет'; 3 4 alert(message); // выведет содержимое переменной Для краткости можно совместить объявление переменной и запись данных:

var message = 'Привет'; Аналогия из жизни Проще всего понять переменную, если представить ее как «коробку» для данных, с уникальным именем. Например, переменная message - это коробка, в которой хранится значение "Привет":

В коробку можно положить любое значение, а позже - поменять его. Значение в переменной можно изменять сколько угодно раз:

1 var message; 2 3 message = 'Привет'; 4 5 message = 'Мир'; // заменили значение 6 7 alert(message);


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

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

.

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

var num = 100500; var message = 'Привет'; Значение можно копировать из одной переменной в другую.

1 var num = 100500; 2 var message = 'Привет'; 3 4 message = num; Значение из num перезаписывает текущее в message. В «коробке» message меняется значение:


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

x = "value"; // переменная создана, если ее не было Технически, это не вызовет ошибки, но делать так все-таки не стоит. Всегда определяйте переменные через var. Это хороший тон в программировании и помогает избежать ошибок. Откройте пример в IE в новом окне:

01 <html> 02 <head> 03 <meta http-equiv="X-UA-Compatible" content="IE=8"> 04 </head> 05 <body> 06 <div id="test"></div> 07 08 <script> 09 test = 5; 10 alert(test); 11 </script> 12 13 </body> 14 </html> Открыть код в новом окне Значение не выведется, будет ошибка. Если в IE включена отладка или открыта панель разработки - вы увидите ее. Дело в том, что почти все современные браузеры создают переменные для элементов, у которых естьid.


Переменная test в них в любом случае существует, запустите, к примеру:

1 <div id="test"></div> 2 3 <script> 4 alert(test); // выведет элемент 5 </script> ..Но в IE<9 такую переменную изменять нельзя. Всё будет хорошо, если объявить test, используя var: Правильный код:

01 <html> 02 <body> 03 <div id="test"></div> 04 05 <script> 06 var test = 5; 07 alert(test); 08 </script> 09 10 </body> 11 </html> Самое «забавное» — то, что, эта ошибка будет только в IE<9, и только если на странице присутствует элемент с совпадающим id. Такие ошибки особенно весело исправлять и отлаживать. Есть и еще ситуации, когда отсутствие var может привести к ошибкам. Надеюсь, вы убедились в необходимости всегда ставить var. 2

1. 2. 3. 4.

Объявите две переменные: admin и name. Запишите в name строку "Василий". Скопируйте значение из name в admin. Выведите admin (должно вывести «Василий»).

Решение Каждая строчка решения соответствует одному шагу задачи:


1 var admin, name; // две переменных через запятую 2 3 name = "Василий"; 4 5 admin = name; 6 7 alert(admin); // "Василий" [Открыть задачу в новом окне] Констант ы Константа — это переменная, которая никогда не меняется. Как правило, их называют большими буквами, через подчёркивание. Например:

1 var COLOR_BLUE = "#00F"; 2 var COLOR_RED = "#0F0"; 3 var COLOR_GREEN = "#F00"; 4 var COLOR_ORANGE = "#FF7F00"; 5 6 alert(COLOR_RED); // #0F0 Технически, константа является обычной переменной, то есть её можно изменить. Но мы договариваемся этого не делать. Зачем нужны константы? Почему бы просто не использовать "#F00" или "#0F0"?

1. Во-первых, константа — это понятное имя, в отличие от строки "#FF7F00". 2. Во-вторых, опечатка в строке может быть не замечена, а в имени константы её упустить невозможно — будет ошибка при выполнении. Константы используют вместо строк и цифр, чтобы сделать программу понятнее и избежать ошибок. Имена переменных

1. Имена переменных 2. Зарезервированные имена 3. Правильный выбор имени Один из самых важных навыков программиста — умение называть переменные правильно. Имена переменн ых На имя переменной в JavaScript наложены всего два ограничения.

1. Имя может состоять из: букв, цифр, символов $ и _ 2. Первый символ не должен быть цифрой. Например:


var myName; var test123; Что здесь особенно интересно - доллар '$' и знак подчеркивания '_' являются такими же обычными символами, как буквы:

1 var $ = 5; // объявили переменную с именем '$' 2 var _ = 15; // переменная с именем '_' 3 4 alert($); А такие переменные были бы неправильными:

var 1a; // начало не может быть цифрой var my-name; // дефис '-' не является разрешенным символом

Регистр букв имеет значение Переменные apple и AppLE - две разные переменные.

Русские буквы допустимы, но не рекомендуются Можно использовать и русские буквы:

1 var имя = "Вася"; 2 alert(имя);

Технически, ошибки здесь нет, но на практике сложилась традиция использовать в именах только английские буквы. Зарезервированн ые имена Существует список зарезервированных слов, которые нельзя использовать при именовании переменных, так как они используются самим языком, например: var, class, return, implementsи др. Некоторые слова, например, class, не используются в современном JavaScript, но они заняты на будущее. Некоторые браузеры позволяют их использовать, но это может привести к ошибкам. Следующий пример будет работать во многих старых браузерах, которые допускают использование слова 'class' и не сработает в современных. Они выдадут синтаксическую


ошибку, попробуйте, запустите:

1 var class = 5; 2 alert(class); Правильный выбор имени Правильный выбор имени переменной - одна из самых важных и сложных вещей в программировании, которая отличает начинающего от гуру. Дело в том, что большинство времени мы тратим не на изначальное написание кода, а на его развитие. А что такое развитие? Это когда я вчера написал код, а сегодня (или спустя неделю) прихожу и хочу его поменять. Например, вывести сообщение не так, а эдак.. Обработать товары по-другому, добавить функционал.. А где у меня там сообщение хранится?.. Гораздо проще найти нужные данные, если они правильно помечены, т.е. переменная названаправильно.

• Правило 1. Никакого транслита. Только английский. Неприемлемы:

var moiTovari; var cena; var ssilka; Подойдут:

var myGoods; var price; var link; Если вы вдруг не знаете английский - самое время выучить. Все равно ведь придется…

• Правило 2. Использовать короткие имена только для переменных «местного значения». Называть переменные именами, не несущими смысловой нагрузки, например a, e, p, mg можно только в том случае, если они используются в небольшом фрагменте кода и их применение очевидно. Вообще же, название переменной должно быть понятным. Иногда для этого нужно использовать несколько слов.

• Правило 3. Переменные из нескольких слов пишутся вместеВотТак. Например:

var borderLeftWidth; Этот способ записи называется «верблюжьей нотацией» или, по-английски, «camelCase».


Существует альтернативный стандарт, когда несколько слов пишутся через знак подчеркивания'_':

var border_left_width; Преимущественно в JavaScript используется вариант borderLeftWidth, в частности во встроенных языковых и браузерных функциях. Поэтому целесообразно остановиться на нём. Ещё одна причина выбрать «верблюжью нотацию» — запись в ней немного короче, чем c подчеркиванием, т.к. не нужно вставлять '_'.

• Правило последнее, главное. Имя переменной должно максимально четко соответствовать хранимым в ней данным. Придумывание таких имен - одновременно коротких и точных, приходит с опытом, но только если сознательно стремиться к этому. Позвольте поделиться одним небольшим секретом, который позволит улучшить ваши названия переменных и сэкономит вам время. Бывает так, что вы написали код, через некоторое время к нему возвращаетесь, и вам надо чтото поправить. Например, изменить какую-то рамку «border…»

… И вы помните, что переменная, в которой хранится нужное вам значение, называется примерно так: borderLeftWidth. Вы ищете ее в коде, не находите, разбираетесь, и обнаруживаете, что на самом деле переменная называлась вот так: leftBorderWidth. После чего вносите нужные правки. В этом случае, самый лучший ход - это переименовать переменную на ту, которую вы искали изначально. То есть, у вас в коде leftBorderWidth, а вы ее переименовываете наborderLeftWidth. Зачем? Дело в том, что в следующий раз, когда вы захотите что-то поправить, то вы будете искать по тому же самому имени. Соответственно, это сэкономит вам время. Кроме того, поскольку именно это имя переменной пришло вам в голову - скорее всего, оно больше соответствует хранимым там данным, чем то, которое вы придумали изначально. Смысл имени переменной - это «имя на коробке», по которому мы сможем максимально быстро находить нужные нам данные. Не нужно бояться переименовывать переменные, если вы придумали имя получше. Современные редакторы позволяют делать это очень удобно. Это в конечном счете сэкономит вам время.


Храните в переменной то, что следует Бывают ленивые программисты, которые, вместо того чтобы объявить новую переменную, используют существующую. В результате получается, что такая переменная — как коробка, в которую кидают то одно, то другое, то третье, при этом не меняя название. Что в ней лежит сейчас? А кто его знает.. Нужно подойти, проверить. Сэкономит такой программист время на объявлении переменной — потеряет в два раза больше на отладке кода. «Лишняя» переменная — добро, а не зло.

3

1. Создайте переменную для названия нашей планеты со значением"Земля". Правильное имя выберите сами.

2. Создайте переменную для имени посетителя со значением "Петя". Имя также на ваш вкус. Решение Каждая строчка решения соответствует одному шагу задачи:

1 var ourPlanetName = "Земля"; // буквально "название нашей планеты" 2 3 var visitorName = "Петя"; // "имя посетителя" Названия переменных можно бы сократить, например, до planet и name, но тогда станет менее понятно, о чем речь. Насколько это допустимо - зависит от скрипта, его размера и сложности.

Введение в типы данных

1. Типы данных 2. Итого В JavaScript существует несколько основных типов данных. Тип ы данн ых

1. Число number: var n = 123; n = 12.345;


Единый тип число используется как для целых, так и для дробных чисел. Существуют специальные числовые значения Infinity (бесконечность) и NaN (ошибка вычислений). Они также принадлежат типу «число». Например, бесконечность Infinity получается при делении на ноль:

1 alert( 1 / 0 ); // Infinity Ошибка вычислений NaN будет результатом некорректной математической операции, например:

1 alert( "нечисло" * 2 ); // NaN, ошибка 2. Строка string: var str = "Мама мыла раму"; str = 'Одинарные кавычки тоже подойдут'; В JavaScript одинарные и двойные кавычки равноправны. Можно использовать или те или другие.

Тип символ не существует, есть только строка В некоторых языках программирования есть специальный тип данных для одного символа. Например, в языке С это char. В JavaScript есть только тип «строка» string. Что, надо сказать, вполне удобно..

3. Булевый (логический) тип boolean. У него всего два значения - true (истина) и false (ложь). Как правило, такой тип используется для хранения значения типа да/нет, например:

var checked = true; // поле формы помечено галочкой checked = false; // поле формы не содержит галочки О нём мы поговорим более подробно, когда будем обсуждать логические вычисления и условные операторы.

4. null — специальное значение. Оно имеет смысл «ничего». Значение null не относится ни к одному из типов выше, а образует свой отдельный тип, состоящий из единственного значенияnull:

var age = null; В JavaScript null не является «ссылкой на несуществующий объект» или «нулевым указателем», как в некоторых других языках. Это просто специальное значение, которое имеет смысл «ничего» или «значение неизвестно». В частности, код выше говорит о том, что возраст age неизвестен.

5. undefined — специальное значение, которое, как и null, образует свой собственный тип. Оно имеет смысл «значение не присвоено».


Если переменная объявлена, но в неё ничего не записано, то ее значение как раз и естьundefined:

1 var u; 2 alert(u); // выведет "undefined" Можно присвоить undefined и в явном виде, хотя это делается редко:

var x = 123; x = undefined; В явном виде undefined обычно не присваивают, так как это противоречит его смыслу. Для записи в переменную «пустого значения» используется null.

6. Объекты object. Первые 5 типов называют «примитивными». Особняком стоит шестой тип: «объекты». К нему относятся, например, даты, он используется для коллекций данных и для многого другого. Итого Есть 5 «примитивных» типов: number, string, boolean, null, undefined и объекты object. Основные операторы

1. Термины: «унарный», «бинарный», «операнд» 2. Арифметические операторы 1. Сложение строк, бинарный + 2. Унарный плюс + 3. Присваивание 4. Приоритет 5. Инкремент/декремент: ++, -6. Побитовые операторы 7. Вызов операторов с присваиванием 8. Оператор запятая Для работы с переменными, со значениями, JavaScript поддерживает все стандартные операторы, большинство которых есть и в других языках программирования. Термин ы: «унарн ый », «бинарн ый », «операнд» У операторов есть своя терминология, которая используется во всех языках программирования.

• Операнд — то, к чему применяется оператор. Например: 5 * 2 — оператор умножения с левым и правым операндами. Другое название: «аргумент оператора».

• Унарным называется оператор, который применяется к одному выражению. Например, оператор унарный минус "-" меняет знак числа на противоположный:

1 var x = 1; 2 alert( -x ); // -1, унарный минус 3 alert( -(x+2) ); // -3, унарный минус применён к результату


сложения x+2 4 alert( -(-3) );

// 3

• Бинарным называется оператор, который применяется к двум операндам. Тот же минус существует и в бинарной форме:

1 var x = 1, y = 3; 2 alert( y - x ); // 2, бинарный минус Работа унарного "+" и бинарного "+" в JavaScript существенно различается. Это действительно разные операторы. Бинарный плюс складывает операнды, а унарный — ничего не делает в арифметическом плане, но зато приводит операнд к числовому типу. Далее мы увидим примеры. Арифметические оператор ы Базовые арифметические операторы знакомы нам с детства: это плюс +, минус -, умножить *, поделить /. Например:

1 alert(2 + 2); // 4 Или чуть сложнее:

1 var i = 2; 2 3 i = (2 + i) * 3 / i; 4 5 alert(i); // 6 Более редкий арифметический оператор % интересен тем, что никакого отношения к процентам не имеет. Его результат a % b — это остаток от деления a на b. Например:

1 alert(5 % 2); // 1, остаток от деления 5 на 2 2 alert(8 % 3); // 2, остаток от деления 8 на 3 3 alert(6 % 3); // 0, остаток от деления 6 на 3 Сложение строк, бинарный + Если бинарный оператор + применить к строкам, то он их объединяет в одну:

var a = "моя" + "строка"; alert(a); // моястрока Если хотя бы один аргумент является строкой, то второй будет также преобразован к строке!


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

1 alert( '1' + 2 ); // "12" 2 alert( 2 + '1' ); // "21" Это приведение к строке — особенность бинарного оператора "+". Остальные арифметические операторы работают только с числами и всегда приводят аргументы к числу. Например:

1 alert( '1' - 2 ); // -1 2 alert( 6 / '2'); // 3 Унарный плюс + Унарный плюс как арифметический оператор ничего не делает:

1 alert( +1 ); // 1 2 alert( +(1-2) ); // -1 Как видно, плюс ничего не изменил в выражениях. Результат — такой же, как и без него. Тем не менее, он широко применяется, так как его «побочный эффект» — преобразование значения в число. Например, у нас есть два числа, в форме строк, и нужно их сложить. Бинарный плюс сложит их как строки, поэтому используем унарный плюс, чтобы преобразовать к числу:

1 var a = "2"; 2 var b = "3"; 3 4 alert( a + b ); // "23", так как бинарный плюс складывает строки 5 alert( +a + b ); // "23", второй операнд - всё ещё строка 6 alert( +a + +b); // 5, число, так как оба операнда предварительно 7 преобразованы в числа Присваивание Оператор присваивания выглядит как знак равенства =:

var i = 1 + 2; alert(i); // 3


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

1 var a = 1; 2 var b = 2; 3 4 a = b + a + 3; 5 6 alert(a); // 6

// (*)

В строке (*) сначала произойдет вычисление, использующее текущее значение a (т.е. 1), после чего результат перезапишет старое значение a. Возможно присваивание по цепочке:

1 var a, b, c; 2 3 a = b = c = 2 + 2; Такое присваивание работает справа-налево, то есть сначала вычислятся самое правое выражение2+2, присвоится в c, затем выполнится b = c и, наконец, a = b.

Оператор "=" возвращает значение Все операторы возвращают значение. Вызов x = выражение записывает выражение в x, а затем возвращает его. Благодаря этому присваивание можно использовать как часть более сложного выражения:

1 var a = 1; 2 var b = 2; 3 4 var c = 3 - (a = b + 1); 5 6 alert(a); // 3 7 alert(c); // 0 В примере выше результатом (a = b + 1) является значение, которое записывается в a (т.е. 3). Оно используется для вычисления c. Забавное применение присваивания, не так ли?


Знать, как это работает — стоит обязательно, а вот писать самому — только если вы уверены, что это сделает код более читаемым и понятным. Приоритет В том случае, если в выражении есть несколько операторов - порядок их выполнения определяетсяприоритетом. Из школы мы знаем, что умножение в выражении 2 * 2 + 1 выполнится раньше сложения, т.к. егоприоритет выше, а скобки явно задают порядок выполнения. Но в JavaScript — гораздо больше операторов, поэтому существует целая таблица приоритетов. Она содержит как уже пройденные операторы, так и те, которые мы еще не проходили. В ней каждому оператору задан числовой приоритет. Тот, у кого число меньше — выполнится раньше. Если приоритет одинаковый, то порядок выполнения — слева направо. Отрывок из таблицы:

… 5 5 6 6 17 …

… умножение деление сложение вычитание присвоение …

… * / + = …

Посмотрим на таблицу в действии. В выражении x = 2 * 2 + 1 приоритет умножения * равен 5, он самый высокий, поэтому выполнится раньше всех. Затем произойдёт сложение +, у которого приоритет 6, и после них — присвоение =, с приоритетом 17. Инкремент/ декремент : ++, -Одной из наиболее частых операций в JavaScript, как и во многих других языках программирования, является увеличение или уменьшение переменной на единицу. Для этого существуют даже специальные операторы:

• Инкремент ++ увеличивает на 1: 1 var i = 2; 2 i++; // более короткая запись для i = i + 1. 3 alert(i); // 3 • Декремент -- уменьшает на 1:


1 var i = 2; 2 i--; // более короткая запись для i = i - 1. 3 alert(i); // 1 Инкремент/декремент можно применить только к переменной. Код 5++ даст ошибку. Вызывать эти операторы можно не только после, но и перед переменной: i++ (называется «постфиксная форма») или ++i («префиксная форма»). Обе эти формы записи делают одно и то же: увеличивают на 1. Тем не менее, между ними существует разница. Она видна только в том случае, когда мы хотим не только увеличить/уменьшить переменную, но и использовать результат в том же выражении. Например:

1 var i = 1; 2 var a = ++i; // (*) 3 4 alert(a); // 2 В строке (*) вызов ++i увеличит переменную, а затем вернёт ее значение в a. То есть, в a попадёт значение i после увеличения. Постфиксная форма i++ отличается от префиксной ++i тем, что возвращает старое значение, бывшее до увеличения. В примере ниже в a попадёт старое значение i, равное 1:

1 var i = 1; 2 var a = i++; // (*) 3 4 alert(a); // 1 • Если результат оператора не используется, а нужно только увеличить/уменьшить переменную — без разницы, какую форму использовать:

1 var i = 0; 2 i++; 3 ++i; 4 alert(i); // 2 • Если хочется тут же использовать результат, то нужна префиксная форма:


1 var i = 0; 2 alert( ++i ); // 1 • Если нужно увеличить, но нужно значение переменной до увеличения — постфиксная форма:

1 var i = 0; 2 alert( i++ ); // 0 Инкремент/декремент можно использовать в любых выражениях. При этом он имеет более высокий приоритет и выполняется раньше, чем арифметические операции:

1 var i = 1; 2 alert( 2 * ++i ); // 4 1 var i = 1; 2 alert( 2 * i++ ); // 2, выполнился раньше но значение вернул старое При этом, нужно с осторожностью использовать такую запись, потому что при чтении кода зачастую неочевидно, что переменая увеличивается. Три строки — длиннее, зато нагляднее:

1 var i = 1; 2 alert( 2 * i ); 3 i++; 5

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

01 var a = 1, b = 1, c, d; 02 03 c = ++a; alert(c); // 2 04 d = b++; alert(d); // 1 05 06 c = (2+ ++a); alert(c); // 5 07 d = (2+ b++); alert(d); // 4 08 09 alert(a); // 3 10 alert(b); // 3 Решение

01 var a = 1, b = 1, c, d; 02


// префиксная форма сначала увеличивает a до 2, а потом возвращает 04 c = ++a; alert(c); // 2 05 06 // постфиксная форма увеличивает, но возвращает старое значение 07 d = b++; alert(d); // 1 08 09 // сначала увеличили a до 3, потом использовали в арифметике 10 c = (2+ ++a); alert(c); // 5 11 12 // увеличили b до 3, но в этом выражении оставили старое значение 13 d = (2+ b++); alert(d); // 4 14 15 // каждую переменную увеличили по 2 раза 16 alert(a); // 3 17 alert(b); // 3 03

[Открыть задачу в новом окне] Побитов ые оператор ы Побитовые операторы рассматривают аргументы как 32-разрядные целые числа и работают на уровне их внутреннего двоичного представления. Эти операторы не являются чем-то специфичным для JavaScript, они поддерживаются в большинстве языков программирования. Поддерживаются следующие побитовые операторы:

• • • • • • •

AND(и) ( & ) OR(или) ( | ) XOR(побитовое исключающее или) ( ^ ) NOT(не) ( ~ ) LEFT SHIFT(левый сдвиг) ( << ) RIGHT SHIFT(правый сдвиг) ( >> ) ZERO-FILL RIGHT SHIFT(правый сдвиг с заполнением нулями) ( >>> )

Вы можете более подробно почитать о них в отдельной статье Побитовые операторы. В ызов операторов с присваиванием Часто нужно применить оператор к переменной и сохранить результат в ней же, например:

n = n + 5; d = d * 2; Эту запись можно укоротить при помощи совмещённых операторов:+=, -=, *=, /=, >>=, <<=,


>>>=, &=, |=, ^=, вот так:

1 var n = 2; 2 n += 5; // теперь n=7 (работает как n = n + 5) 3 n *= 2; // теперь n=14 (работает как n = n * 2) 4 5 alert(n); // 14 Все эти операторы имеют в точности такой же приоритет, как обычное присваивание, то есть выполняются после большинства других операций. 3

Чему будет равен x в примере ниже?

var a = 2; var x = 1 + (a *= 2); Решение Ответ: x = 5. Оператор присваивания возвращает значение, которое будет записано в переменную, например:

1 var a = 2; 2 alert( a *= 2 ); // 4 Отсюда x = 1 + 4 = 5. [Открыть задачу в новом окне] Оператор запятая Запятая тоже является оператором. Ее можно вызвать явным образом, например:

1 a = (5, 6); 2 3 alert(a); Запятая позволяет перечислять выражения, разделяя их запятой ','. Каждое из них — вычисляется и отбрасывается, за исключением последнего, которое возвращается. Запятая — единственный оператор, приоритет которого ниже присваивания. В выражении a = (5,6)для явного задания приоритета использованы скобки, иначе оператор '=' выполнился бы


до запятой',', получилось бы (a=5), 6. Зачем же нужен такой странный оператор, который отбрасывает значения всех перечисленных выражений, кроме последнего? Обычно он используется в составе более сложных конструкций, чтобы сделать несколько действий в одной строке. Например:

1 // три операции в одной строке 2 for (a = 1, b = 3, c = a*b; a < 10; a++) { 3 ... 4} Такие трюки используются во многих JavaScript-фреймворках для укорачивания кода. Операторы сравнения и логические значения

1. 2. 3. 4. 5. 6.

Логические значения Сравнение строк Сравнение разных типов Строгое равенство Сравнение с null и undefined Итого

В этом разделе мы познакомимся с операторами сравнения и с логическими значениями, которые такие операторы возвращают. Многие операторы сравнения знакомы нам со школы:

• Больше/меньше: a > b, a < b. • Больше/меньше или равно: a >= b, a <= b. • Равно a == b. Для сравнения используется два символа равенства '='. Один символ a = b означал бы присваивание.

• «Не равно». В школе он пишется как ≠, в JavaScript — знак равенства с восклицательным знаком перед ним !=. Логические значения Как и другие операторы, сравнение возвращает значение. Это значение имеет специальныйлогический тип. Существует всего два логических значения:

• true — имеет смысл «да», «верно», «истина». • false — означает «нет», «неверно», «ложь». Например:

1 alert( 2 > 1 ); // true, верно 2 alert( 2 == 1 ); // false, неверно 3 alert( 2 != 1 ); // true


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

1 var a = true; // присвоили явно 2 var b = 3 > 4; // false 3 4 alert( b ); // false 5 6 alert( a == b ); // (true == false) неверно, результат false Сравнение строк Строки сравниваются побуквенно:

1 alert( 'Б' > 'А' ); // true Буквы сравниваются в алфавитном порядке. Какая буква в алфавите позже — та и больше.

Кодировка Unicode Аналогом «алфавита» во внутреннем представлении строк служит кодировка, у каждого символа — свой номер (код). JavaScript использует кодировку Unicode. При этом сравниваются численные коды символов. В кодировке Unicode обычно код у строчной буквы больше, чем у прописной, поэтому:

1 alert('а' > 'Я'); // true, строчные буквы больше больших Для корректного сравнения символы должны быть в одинаковом регистре.

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


1 alert( 'Банан' > 'Аят' ); • Если одинаковы — сравнение идёт дальше. Здесь оно дойдёт до третьей буквы:

1 alert( 'Вася' > 'Ваня' ); // true, т.к. 'с' > 'н' • При этом любая буква больше отсутствия буквы:

1

alert( 'Привет' > 'Прив' ); // true, так как 'е' больше чем "ничего".

Такое сравнение называется лексикографическим.

Обычно мы получаем значения от посетителя в виде строк. Например, promptвозвращает строку, которую ввел посетитель. Числа, полученные таким образом, в виде строк сравнивать нельзя, результат будет неверен. Например:

1 alert( "2" > "14" ); // true, неверно, ведь 2 не больше 14 В примере выше 2 оказалось больше 14, потому что строки сравниваются посимвольно, а первый символ '2' больше '1'. Правильно было бы преобразовать их к числу явным образом. Например, поставив перед ними +:

1

alert( +"2" > +"14" ); // false, теперь правильно

Сравнение разных типов

При сравнении значения преобразуются к числам. Исключение: когда оба значения — строки, тогда не преобразуются. Например:

1 alert( '2' > 1 ); // true 2 alert( '01' == 1 ); //true 3 alert( false == 0 ); // true, false становится 0, а true 1. Тема преобразований типов будет продолжена далее, в главе Преобразование объектов: toString


и valueOf. Строгое равенство Обычное равенство не может отличить 0 от false:

1 alert(0 == false); // true, т.к. false преобразуется к 0 Что же делать, если все же нужно отличить 0 от false? Для проверки равенства без преобразования типов используются операторы строгого равенства === (тройное равно) и !==. Они сравнивают без приведения типов. Если тип разный, то такие значения всегда неравны (строго):

1 alert(0 === false); // false, т.к. типы различны Сравнение с null и undefined Проблемы со специальными значениями возможны, когда к переменной применяется операция сравнения > < <= >=, а у неё может быть как численное значение, так и null/undefined. Интуитивно кажется, что null/undefined эквивалентны нулю, но это не так! Они ведут себя подругому.

1. Значения null и undefined равны == друг другу и не равны чему бы то ни было ещё. Это жёсткое правило буквально прописано в спецификации языка.

2. При преобразовании в число null становится 0, а undefined становится NaN. Посмотрим забавные следствия.

Некорректный результат сравнения null с 0 Сравним null с нулём:

1 alert(null > 0); // false 2 alert(null == 0); // false Итак, мы получили, что null не больше и не равен нулю. А теперь…

1 alert(null >= 0); // true


Как такое возможно? Если нечто «больше или равно нулю», то резонно полагать, что оно либо больше, либо равно. Но здесь это не так. Дело в том, что алгоритмы проверки равенства == и сравнения >= > < <= работают по-разному. Сравнение честно приводит к числу, получается ноль. А при проверке равенства значения null и undefined обрабатываются особым образом: они равны друг другу, но не равны чему-то ещё. В результате получается странная с точки зрения здравого смысла ситуация, которую мы видели в примере выше.

Несравнимый undefined Значение undefined вообще нельзя сравнивать:

1 alert(undefined > 0); // false (1) 2 alert(undefined < 0); // false (2) 3 alert(undefined == 0); // false (3) • Сравнения (1) и (2) дают false потому, что undefined при преобразовании к числу даёт NaN. А значение NaN по стандарту устроено так, что любые сравнения с ним возвращают false.

• Проверка равенства (3) даёт false, потому что в стандарте явно прописано, что undefined равно лишь null и ничему другому. Вывод: любые сравнения с undefined/null, кроме точного ===, следует делать с осторожностью. Желательно не использовать сравнения >= > < <=, во избежание ошибок в коде. Итого

• В JavaScript есть логические значения true (истина) и false (ложь). Операторы сравнения возвращают их. • Строки сравниваются побуквенно.

• Значения разных типов приводятся к числу при сравнении, за исключением строгого равенства=== (!==).

• Значения null и undefined равны == друг другу и не равны ничему другому. В других сравнениях (с участием >,<) их лучше не использовать, так как они ведут себя не как 0. Побитовые операторы

1. Формат 32-битного целого числа со знаком


2. Список операторов 3. Описание работы операторов 1. & (побитовое И) 2. | (Побитовое ИЛИ) 3. ^ (Исключающее ИЛИ) 4. ~ (Побитовое НЕ) 5. Операторы битового сдвига 1. << (Левый сдвиг) 2. >> (Правый сдвиг, переносящий знак) 3. >>> (Правый сдвиг с заполнением нулями) 4. Применение побитовых операторов 1. Маска 2. Описание доступов 3. Проверка доступов 4. Маски в функциях 5. Округление 6. Проверка на -1 7. Умножение и деление на степени 2 5. Итого Побитовые операторы интерпретируют операнды как последовательность из 32 битов (нулей и единиц). Они производят операции, используя двоичное представление числа, и возвращают новую последовательность из 32 бит (число) в качестве результата. Эта глава сложная, требует дополнительных знаний в программировании и не очень важная, вы можете пропустить её. Формат 32-битного целого числа со знаком Побитовые операторы в JavaScript работают с 32-битными целыми числами в их двоичном представлении. Это представление называется «32-битное целое со знаком, старшим битом слева и дополнением до двойки». Разберём, как устроены числа внутри подробнее, это необходимо знать для битовых операций с ними.

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

• Старший бит слева — это научное название для самого обычного порядка записи цифр (от большего разряда к меньшему). При этом, если больший разряд отсутствует, то соответствующий бит равен нулю. Примеры представления чисел в двоичной системе:

1a 2a 3a 4a 5a

= = = = =

0; // 1; // 2; // 3; // 255;//

00000000000000000000000000000000 00000000000000000000000000000001 00000000000000000000000000000010 00000000000000000000000000000011 00000000000000000000000011111111

Обратите внимание, каждое число состоит ровно из 32-битов.


Младший бит слева Несмотря на то, что нам такой способ записи чисел кажется совсем обычным, бывают языки и технологии, использующие способ записи «младший бит слева», когда биты пишутся наоборот, от меньшего разряда к большему. Именно поэтому спецификация EcmaScript явно говорит «старший бит слева».

• Дополнение до двойки — это название способа поддержки отрицательных чисел. Двоичный вид числа, обратного данному (например, 5 и -5) получается путём обращения всех битов с прибавлением 1. То есть, нули заменяются на единицы, единицы — на нули и к числу прибавляется 1. Получается внутреннее представление того же числа, но со знаком минус. Например, вот число 314:

00000000000000000000000100111010 Чтобы получить -314, первый шаг — обратить биты числа: заменить 0 на 1, а 1 на 0:

11111111111111111111111011000101 Второй шаг — к полученному двоичному числу приплюсовать единицу, обычным двоичным сложением:11111111111111111111111011000101 + 1 = 11111111111111111111111011000110. Итак, мы получили:

-314 = 11111111111111111111111011000110 Принцип дополнения до двойки делит все двоичные представления на два множества: если крайний-левый бит равен 0 — число положительное, если 1 — число отрицательное. Поэтому этот бит называется знаковым битом. Список операторов В следующей таблице перечислены все побитовые операторы. Далее операторы разобраны более подробно.

Оператор

Использование Описание Ставит 1 на бит результата, для которого Побитовое И (AND) a & b соответствующие биты операндов равны 1. Ставит 1 на бит результата, для которого хотя бы Побитовое ИЛИ (OR) a | b один из соответствующих битов операндов равен 1. a ^ b Побитовое Ставит 1 на бит результата, для которого только один


исключающее ИЛИ (XOR) Побитовое НЕ (NOT) ~a Левый сдвиг

a << b

Правый сдвиг, переносящий знак

a >> b

Правый сдвиг с a >>> b заполнением нулями

из соответствующих битов операндов равен 1 (но не оба). Заменяет каждый бит операнда на противоположный. Сдвигает двоичное представление a на b битов влево, добавляя справа нули. Сдвигает двоичное представление a на b битов вправо, отбрасывая сдвигаемые биты. Сдвигает двоичное представление a на b битов вправо, отбрасывая сдвигаемые биты и добавляя нули слева.

Описание работы операторов Побитовые операторы работают следующим образом: 1. Операнды преобразуются в 32-битные целые числа, представленные последовательностью битов. Дробная часть, если она есть, отбрасывается. 2. Для бинарных операторов — каждый бит в первом операнде рассматривается вместе с соответствующим битом второго операнда: первый бит с первым, второй со вторым и т.п. Оператор применяется к каждой паре бит, давая соответствующий бит результата. 3. Получившаяся в результате последовательность бит интерпретируется как обычное число. Посмотрим, как работают операторы, на примерах. & (побитовое И) Выполняет операцию И над каждой парой бит. Результат a & b равен единице только когда оба бита a и b равны единице. Таблица истинности для &:

a 0 0 1 1

b 0 1 0 1

a & b 0 0 0 1

Пример:

1 9 (по осн. 10) 2 = 00000000000000000000000000001001 (по осн. 2) 3 14 (по осн. 10) 4 = 00000000000000000000000000001110 (по осн. 2) 5 -------------------------------6 14 & 9 (по осн. 10) 7 = 00000000000000000000000000001000 (по осн. 2) 8 = 8 (по осн. 10)


| (Побитовое ИЛИ) Выполняет операцию ИЛИ над каждой парой бит. Результат a | b равен 1, если хотя бы один бит изa,b равен 1. Таблица истинности для |:

a 0 0 1 1

b 0 1 0 1

a | b 0 1 1 1

Пример:

1 9 (по осн. 10) 2 = 00000000000000000000000000001001 (по осн. 2) 3 14 (по осн. 10) 4 = 00000000000000000000000000001110 (по осн. 2) 5 -------------------------------6 14 | 9 (по осн. 10) 7 = 00000000000000000000000000001111 (по осн. 2) 8 = 15 (по осн. 10) ^ (Исключающее ИЛИ) Выполняет операцию «Исключающее ИЛИ» над каждой парой бит. a Исключающее ИЛИ b равно 1, если только a=1 или только b=1, но не оба одновременно a=b=1. Таблица истинности для исключающего ИЛИ:

a 0 0 1 1

b 0 1 0 1

a ^ b 0 1 1 0

Как видно, оно даёт 1, если ЛИБО слева 1, ЛИБО справа 1, но не одновременно. Поэтому его и называют «исключающее ИЛИ». Пример:

1 9 (по осн. 10) 2 = 00000000000000000000000000001001 (по осн. 2) 3 14 (по осн. 10) 4 = 00000000000000000000000000001110 (по осн. 2) 5 -------------------------------6 14 ^ 9 (по осн. 10)


7 8

= 00000000000000000000000000000111 (по осн. 2) = 7 (по осн. 10)

Исключающее ИЛИ в шифровании Исключающее или можно использовать для шифрования, так как эта операция полностью обратима. Двойное применение исключающего ИЛИ с тем же аргументом даёт исходное число. Иначе говоря, верна формула: a ^ b ^ b == a. Пускай Вася хочет передать Пете секретную информацию data. Эта информация заранее превращена в число, например строка интерпретируется как последовательность кодов символов. Вася и Петя заранее договариваются о числовом ключе шифрования key. Алгоритм:

• Вася берёт двоичное представление data и делает операцию data ^ key. При необходимости data бьётся на части, равные по длине key, чтобы можно было провести побитовое ИЛИ ^ по всей длине. JavaScript позволяет делать операцию ^ с 32-битными целыми числами, так что data нужно разбить на последовательность таких чисел.

• Результат data ^ key отправляется Пете, это шифровка. Например, пусть в data очередное число равно 9, а ключ key равен 1220461917.

Данные: 9 в двоичном виде 00000000000000000000000000001001 Ключ: 1220461917 в двоичном виде 01001000101111101100010101011101 Результат операции 9 ^ key: 01001000101111101100010101010100 Результат в 10-ной системе (шифровка): 1220461908 • Петя, получив очередное число шифровки 1220461908, применяет к нему такую же операцию ^ key.

• Результатом будет исходное число data.


В нашем случае:

Полученная шифровка в двоичной системе: 9 ^ key = 1220461908 01001000101111101100010101010100 Ключ: 1220461917 в двоичном виде: 01001000101111101100010101011101 Результат операции 1220461917 ^ key: 00000000000000000000000000001001 Результат в 10-ной системе (исходное сообщение): 9 Конечно, такое шифрование поддаётся частотному анализу и другим методам дешифровки, поэтому современные алгоритмы используют XOR как одну из важных частей более сложной многоступенчатой схемы. ~ (Побитовое НЕ) Производит операцию НЕ над каждым битом, заменяя его на обратный ему. Таблица истинности для НЕ:

a ~a 0 1 1 0 Пример:

1 9 (по осн. 10) 2 = 00000000000000000000000000001001 (по осн. 2) 3 -------------------------------4 ~9 (по осн. 10) 5 = 11111111111111111111111111110110 (по осн. 2) 6 = -10 (по осн. 10) Из-за внутреннего представления отрицательных чисел получается так, что ~n == -(n+1). Например:

1 alert(~3); // -4 2 alert(~-1); // 0


Операторы битового сдвига Операторы битового сдвига принимают два операнда. Первый — это число для сдвига, а второй — количество битов, которые нужно сдвинуть в первом операнде. Направление сдвига — то же, что и направление стрелок в операторе. << (Левый сдвиг) Этот оператор сдвигает первый операнд на указанное число битов влево. Лишние биты отбрасываются, справа добавляются нулевые биты. Например, 9 << 2 даст 36:

19 2 3 49 5 6

(по осн.10) = 00000000000000000000000000001001 (по осн.2) -------------------------------<< 2 (по осн.10) = 00000000000000000000000000100100 (по осн.2) = 36 (по осн.10)

>> (Правый сдвиг, переносящий знак) Этот оператор сдвигает биты вправо, отбрасывая лишние. Копии крайнего-левого бита добавляются слева. Так как итоговый крайний-левый бит имеет то же значение, что и исходный, знак числа (представленный крайним-левым битом) не изменяется. Поэтому он назван «переносящим знак». Например, 9 >> 2 даст 2:

19 2 3 49 5 6

(по осн.10) = 00000000000000000000000000001001 (по осн.2) ------------------------------->> 2 (по осн.10) = 00000000000000000000000000000010 (по осн.2) = 2 (по осн.10)

Аналогично, -9 >> 2 даст -3, так как знак сохранен:

1 -9 (по осн.10) 2 = 11111111111111111111111111110111 (по осн.2) 3 -------------------------------4 -9 >> 2 (по осн.10) 5 = 11111111111111111111111111111101 (по осн.2) = -3 (по осн.10) >>> (Правый сдвиг с заполнением нулями) Этот оператор сдвигает биты первого операнда вправо. Лишние биты справа отбрасываются. Слева добавляются нулевые биты.


Знаковый бит становится равным 0, поэтому результат всегда положителен. Для неотрицательных чисел правый сдвиг с заполнением нулями и правый сдвиг с переносом знака дадут одинаковый результат, т.к в обоих случаях слева добавятся нули. Для отрицательных чисел — результат работы разный. Например, -9 >>> 2 даст 1073741821, в отличие от -9 >> 2 (дает -3):

1 -9 (по осн.10) 2 = 11111111111111111111111111110111 (по осн.2) 3 -------------------------------4 -9 >>> 2 (по осн.10) 5 = 00111111111111111111111111111101 (по осн.2) 6 = 1073741821 (по осн.10) Применение побитовых операторов Случаи применения побитовых операторов, которые мы здесь разберём, составляют порядка 99% всех использований в JavaScript. Маска Для этого примера представим, что наш скрипт работает с пользователями:

• Гость — читатель сайта. • Петя — редактор сайта. • Админ — администратор сайта. У каждого из них есть ряд доступов, которые можно свести в таблицу:

Пользователь Гость Петя Админ

Просмотр статей Да Да Да

Изменение статей Нет Да Да

Просмотр товаров Да Да Да

Изменение товаров Нет Да Да

Общее администрирование Нет Нет Да

Если вместо «Да» поставить 1, а вместо «Нет» — 0, то каждый набор доступов описывается числом:

В Просмотр Изменение Просмотр Изменение Общее Пользователь десятичной статей статей товаров товаров администрирование системе Гость 1 0 1 0 0 = 20 Петя 1 1 1 1 0 = 30 Админ 1 1 1 1 1 = 31 Мы «упаковали» много информации в одно число. Это экономит память. Но, кроме этого, по нему очень легко проверить, имеет ли посетитель заданную комбинацию доступов. Для этого посмотрим, как в 2-ной системе представляется каждый доступ в отдельности.

• Доступ, соответствующий только общему администрированию: 00001 (=1) (все нули


кроме 1 на позиции, соответствующей этому доступу).

• • • •

Доступ, соответствующий только изменению товаров: 00010 (=2). Доступ, соответствующий только просмотру товаров: 00100 (=4). Доступ, соответствующий только изменению статей: 01000 (=8). Доступ, соответствующий только просмотру статей: 10000 (=16).

Например, просматривать и изменять статьи позволит доступ access = 11000: Описание доступов Работать с двоичными числами из JavaScript не очень-то удобно. Хорошо уже то, что можно получить двоичное представление числа через n.toString(2) и преобразовать двоичную строку в число черезparseInt(..., 2). Например:

1 var access = parseInt("11000", 2); // 24 в 10-ной системе 2 3 alert(access); // 24 4 5 var access2 = access.toString(2); 6 7 alert(access2); // 11000, в виде строки …Поэтому обычно нужные доступы задаются в виде констант:

1 var 2 var 3 var 4 var 5 var

ACCESS_ADMIN = 1; ACCESS_GOODS_CHANGE = 2; ACCESS_GOODS_VIEW = 4; ACCESS_ARTICLE_CHANGE = 8; ACCESS_ARTICLE_VIEW = 16;

// // // // //

00001 00010 00100 01000 10000

Чтобы получить маску, задающую определённые доступы, нужно составить число с 1 на нужной битовой позиции. Это может сделать оператор ИЛИ (|) . Как мы помним, он ставит 1 в результате, если хотя бы у одного операнда на этой позиции 1. Поэтому если сделать ИЛИ любого числа с какой-либо из переменныхACCESS_* выше, то число останется неизменным там, где в ACCESS_* стоит 0, а там где 1 — станет 1. В результате будет добавлен доступ. Например, создадим маску из двух доступов:

1 var access = 0; // изначально никаких прав 2 3 access = access | ACCESS_GOODS_VIEW; // добавили один доступ 4 access = access | ACCESS_GOODS_CHANGE; // добавили другой доступ


5 6 alert(access.toString(2)); // "00110" в бинарном виде 7 alert(access); // число 6 Можно поступить и проще:

var access = ACCESS_GOODS_VIEW | ACCESS_GOODS_CHANGE; Проверка доступов Для того, чтобы понять, есть ли в доступе access нужный доступ, например право администрирования — достаточно применить к нему побитовый оператор И (&) с соответствующей маской. Например:

1 var access = parseInt("11111", 2); // 31, все доступы включены 2 alert(access & ACCESS_ADMIN); // не 0, значит есть доступ к общему 3 администрированию А теперь та же проверка для посетителя с другими правами:

1 var access = parseInt("10100"); // 20, нет 1 в конце 2 alert(access & ACCESS_ADMIN); // 0, нет доступа к общему 3 администрированию Такая проверка работает, потому что оператор И ставит 1 на те позиции результата, на которых в обоих операндах стоит 1. Так что access & 1 для любого числа access поставит все биты в ноль, кроме самого правого. А самый правый станет 1 только если он равен 1 в access. Для полноты картины также проверим, даёт ли доступ 11111 право на изменение товаров. Для этого нужно применить к доступу оператор И (&) с 00010 (=2 в 10-ной системе).**

1 var adminAccess = 31; // 11111 2 alert(adminAccess & ACCESS_GOODS_CHANGE); // не 0, есть доступ к 3 изменению товаров Можно проверить один из нескольких доступов. Например, проверим, есть ли права на просмотр ИЛИ изменение товаров. Соответствующие права задаются битом 1 на втором и третьем месте с конца, что даёт число 00110 (=6 в 10-ной системе).


1 var check = ACCESS_GOODS_VIEW | ACCESS_GOODS_CHANGE; // 6, 00110 2 3 var access = 30; // 11110; 4 alert(access & check); // не 0, значит есть доступ к просмотру ИЛИ 5 изменению 6 7 access = parseInt("11100", 2); 8 9 alert(access & check); // не 0, есть доступ к просмотру ИЛИ изменению Как видно из примера выше, если в аргументе check стоит ИЛИ из нескольких доступов ACCESS_*, то и результат проверки скажет, есть ли хотя бы один из них. А какой — нужно смотреть отдельной проверкой, если это важно. Итак, маска даёт возможность удобно «паковать» много битовых значений в одно число при помощи ИЛИ |, а также, при помощи оператора И (&), проверять маску на комбинацию установленных битов. Маски в функциях Зачастую маски используют в функциях, чтобы одним параметром передать несколько «флагов», т.е. однобитных значений. Например:

// найти пользователей с правами на изменение товаров или администраторов findUsers(ACCESS_GOODS_CHANGE | ACCESS_ADMIN); Округление Так как битовые операции отбрасывают десятичную часть, то их можно использовать для округления. Достаточно взять любую операцию, которая не меняет значение числа. Например, двойное НЕ (~):

1 alert( ~~12.345 ); // 12 Подойдёт и Исключающее ИЛИ (^) с нулём:

1 alert( 12.345^0 ); // 12 Последнее даже более удобно, поскольку отлично читается:

1 alert( 12.3 * 14.5 ^ 0); // (=178) "12.3 умножить на 14.5 и округлить" У побитовых операторов достаточно низкий приоритет, он меньше чем у остальной арифметики:


1 alert( 1.1 + 1.2 ^ 0 ); // 2, сложение выполнится раньше округления Проверка на -1 Внутренний формат чисел устроен так, что для смены знака нужно все биты заменить на противоположные («обратить») и прибавить 1. Обращение битов — это побитовое НЕ (~). То есть, при таком формате представления числа-n = ~n + 1. Или, если перенести единицу: ~n = -(n+1). Как видно из последнего равенства, ~n == 0 только если n == -1. Поэтому можно легко проверить равенство n == -1:

1 var n = 5; 2 3 if (~n) { // сработает, т.к. ~n = -(5+1) = -6 4 alert("n не -1"); // выведет! 5} 1 var n = -1; 2 3 if (~n) { // не сработает, т.к. ~n = -(-1+1) = 0 4 alert("....ничего не выведет..."); 5} Проверка на -1 пригождается, например, при поиске символа в строке. Вызовstr.indexOf("подстрока") возвращает позицию подстроки в str, или -1 если не нашёл.

1 var str = "Проверка"; 2 if (~str.indexOf("верка")) { // Сочетание "if (~...indexOf)" читается 3 как "если найдено" 4 alert('найдено!'); 5} Умножение и деление на степени 2 Оператор a << b, сдвигая биты, по сути умножает a на 2b. Например:

1 alert( 1 << 2 ); // 1*(2*2) = 4 2 alert( 1 << 3 ); // 1*(2*2*2) = 8 3 alert( 3 << 3 ); // 3*(2*2*2) = 24 Оператор a >> b, сдвигая биты, производит целочисленное деление a на 2b.


1 alert( 8 >> 2 ); // 2 = 8/4, убрали 2 нуля в двоичном представлении alert( 11 >> 2 ); // 2, целочисленное деление (менее значимые биты 2 просто отброшены) Итого

• Бинарные побитовые операторы: & | ^ << >> >>>. • Унарный побитовый оператор один: ~. Как правило, битовое представление числа используется для:

• Упаковки нескольких битововых значений («флагов») в одно значение. Это экономит память и позволяет проверять наличие комбинации флагов одним оператором &. Кроме того, такое упакованное значение будет для функции всего одним параметром, это тоже удобно.

• Округления числа: (12.34^0) = 12. • Проверки на равенство -1: if (~n) { n не -1 }. 5

Почему побитовые операции в примерах ниже не меняют число? Что они делают внутри?

1 alert( 123 ^ 0 ); // 123 2 alert( 0 ^ 123 ); // 123 3 alert( ~~123 ); // 123 Решение

1. Операция a^b ставит бит результата в 1, если на соответствующей битовой позиции в a или b (но не одновременно) стоит 1. Так как в 0 везде стоят нули, то биты берутся в точности как во втором аргументе.

2. Первое побитовое НЕ ~ превращает 0 в 1, а 1 в 0. А второе НЕ превращает ещё раз, в итоге получается как было. [Открыть задачу в новом окне] 3

Напишите функцию isInteger(num), которая возвращает true, еслиnum — целое число, иначе false. Например:

alert( isInteger(1) ); // true


alert( isInteger(1.5) ); // false alert( isInteger(-0.5) ); // false Решение Один из вариантов такой функции:

1 function isInteger(num) { 2 return (num ^ 0) === num; 3} 4 5 alert( isInteger(1) ); // true 6 alert( isInteger(1.5) ); // false 7 alert( isInteger(-0.5) ); // false Обратите внимание: num^0 — в скобках! Это потому, что приоритет операции ^очень низкий. Если не поставить скобку, то === сработает раньше. Получитсяnum ^ (0 === num), а это уже совсем другое дело. [Открыть задачу в новом окне] 5

Верно ли, что для любых a и b выполняются равенства ниже?

• a ^ b == b ^ a • a & b == b & a • a | b == b | a Иными словами, при перемене мест — всегда ли результат остаётся тем же? Решение Операция над числами, в конечном итоге, сводится к битам. Посмотрим, можно ли поменять местами биты слева и справа. Например, таблица истинности для ^:

a b результат 0 0 0 0 1 1 1 0 1 1 1 0


Случаи 0^0 и 1^1 заведомо не изменятся при перемене мест, поэтому нас не интересуют. А вот 0^1 и 1^0 эквивалентны и равны 1. Аналогично можно увидеть, что и другие операторы симметричны. Ответ: да. [Открыть задачу в новом окне] 5

Почему результат alert'ов ниже разный?

1 alert( 123456789 ^ 0 ); // 123456789 2 alert( 12345678912345 ^ 0 ); // 1942903641 Решение Результат разный, потому что обычно число в JavaScript имеет 64-битный формат с плавающей точкой. При этом часть битов (52) отведены под цифры, часть (11) отведены под хранение номера позиции, на которой стоит десятичная точка, и один бит — знак числа. Это означает, что максимальное целое, которое можно хранить, занимает 52 бита. Побитовые операции преобразуют число в 32-битовое целое. При этом старшие из этих 52 битов будут отброшены. Если число изначально занимало больше чем 31бита (еще один бит хранит не цифру, а знак) — оно изменится. Вот ещё пример:

01 // в двоичном виде 1000000000000000000000000000000 02 alert( Math.pow(2, 30) ); // 1073741824 alert( Math.pow(2, 30) ^ 0 ); // 1073741824, всё ок, длины 03 хватает 04 05 // в двоичном виде 100000000000000000000000000000000 06 alert( Math.pow(2, 32) ); // 4294967296 07 alert( Math.pow(2, 32) ^ 0 ); // 0, отброшены старшие биты! 08 09 // пограничный случай 10 // в двоичном виде 10000000000000000000000000000000 11 alert( Math.pow(2, 31) ); // 2147483648


alert( Math.pow(2, 31) ^ 0 ); // -2147483648, старший бит стал знаковым

12

Взаимодействие с пользователем: alert, prompt, confirm

1. 2. 3. 4. 5.

alert prompt confirm Особенности встроенных функций Резюме

В этом разделе мы рассмотрим базовые UI операции: alert, prompt и confirm, которые позволяют работать с данными, полученными от пользователя. alert Синтаксис:

alert(сообщение) alert выводит на экран окно с сообщением и приостанавливает выполнение скрипта, пока пользователь не нажмет «ОК».

1 alert("Привет"); Окно сообщения, которое выводится, является модальным окном. Слово «модальное» означает, что посетитель не может взаимодействовать со страницей, нажимать другие кнопки и т.п., пока не разберется с окном. В данном случае - пока не нажмет на «OK». prompt Функция prompt принимает два аргумента:

result = prompt(title, default); Она выводит модальное окно с заголовком title, полем для ввода текста, заполненным строкой по умолчанию default и кнопками OK/CANCEL. Пользователь должен либо что-то ввести и нажать OK, либо отменить ввод кликом на CANCEL или нажатием ESC на клавиатуре. Вызов prompt возвращает то, что ввел посетитель - строку или специальное значение null, если ввод отменен. Как и в случае с alert, окно prompt модальное.

1 var years = prompt('Сколько вам лет?', 100);


2 3 alert('Вам ' + years + ' лет!')

Всегда указывайте default Вообще, второй default может отсутствовать. Однако при этом IE вставит в диалог значение по умолчанию "undefined". Запустите этот код в IE, чтобы понять о чем речь:

1 var test = prompt("Тест");

Поэтому рекомендуется всегда указывать второй аргумент:

1

var test = prompt("Тест", ''); // <-так лучше

confirm

Синтаксис:

result = confirm(question); confirm выводит окно с вопросом question с двумя кнопками: OK и CANCEL. Результатом будет true при нажатии OK и false - при CANCEL(Esc). Например:

1 var isAdmin = confirm("Вы - администратор?"); 2 3 alert(isAdmin); Особенности встроенных функций Место, где выводится модальное окно с вопросом, и внешний вид окна выбирает браузер. Разработчик не может на это влиять. С одной стороны — это недостаток, т.к. нельзя вывести окно в своем дизайне.


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

Создайте страницу, которая спрашивает имя и выводит его. Работать должно так: tutorial/intro/basic.html. Решение Решение: tutorial/intro/basic.html. [Открыть задачу в новом окне] Резюме

• alert выводит сообщение. • prompt выводит сообщение и ждет, пока пользователь введет текст, а затем возвращает введенное значение или null, если ввод отменен (CANCEL/Esc).

• confirm выводит сообщение и ждет, пока пользователь нажмет «OK» или «CANCEL» и возвращает true/false. Условные операторы: if, '?'

1. 2. 3. 4. 5. 6. 7.

Оператор if Преобразование к логическому типу Неверное условие, else Несколько условий, else if Оператор вопросительный знак '?' Несколько операторов '?' Нетрадиционное использование '?'

Иногда, в зависимости от условия, нужно выполнить различные действия. Для этого используется оператор if. Например:

1

var year = prompt('В каком году появилась спецификация ECMA-262 5.1?', '');

2 3 if (year != 2011) alert('А вот и неправильно!');


Оператор if Оператор if («если») получает условие, в примере выше это year != 2011. Он вычисляет его, и если результат — true, то выполняет команду. Если нужно выполнить более одной команды — они оформляются блоком кода в фигурных скобках:

1 if (year != 2011) { 2 alert('А вот..'); 3 alert('..и неправильно!'); 4} Рекомендуется использовать фигурные скобки всегда, даже когда команда одна. Это улучшает читаемость кода. Преобразование к логическому типу Оператор if (...) вычисляет и преобразует выражение в скобках к логическому типу. В логическом контексте число 0, пустая строка "", null и undefined, а также NaN являютсяfalse, остальные значения — true. Например, такое условие никогда не выполнится:

if (0) { // 0 преобразуется к false ... } … А такое — выполнится всегда:

if (1) { // 1 преобразуется к true ... } Вычисление условия в проверке if (year != 2011) может быть вынесено в отдельную переменную:

1 var cond = (year != 2011); 2 3 if (cond) { 4 ... 5} 5 Выведется ли alert?

if ("0") {

// true/false


alert('Привет'); } Решение Да, выведется, т.к. внутри if стоит строка "0". Любая строка, кроме пустой (а здесь она не пустая), в логическом контексте является true. Можно запустить и проверить:

1 if ("0") { 2 alert('Привет'); 3} [Открыть задачу в новом окне] Неверное условие, else Необязательный блок else («иначе») выполняется, если условие неверно:

1 var year = prompt('Введите год ECMA-262 5.1', ''); 2 3 if (year == 2011) { 4 alert('Да вы знаток!'); 5 } else { 6 alert('А вот и неправильно!'); // любое значение, кроме 2011 7} Несколько условий, else if Бывает нужно проверить несколько вариантов условия. Для этого используется блок else if .... Например:

1

var year = prompt('В каком году появилась спецификация ECMA-262 5.1?', '');

2 3 if (year < 2011) { 4 alert('Это слишком рано..'); 5 } else if (year > 2011) { 6 alert('Это поздновато..'); 7 } else { 8 alert('Да, точно в этом году!'); 9}


В примере выше JavaScript сначала проверит первое условие, если оно ложно — перейдет ко второму — и так далее, до последнего else. 2

Напишите код, который будет спрашивать: «Каково «официальное» название JavaScript?». Если посетитель вводит «EcmaScript», то выводить «Верно!», если что-то другое — выводить «Не знаете? «EcmaScript»!». Блок-схема:

Результат в действии: tutorial/intro/ifelse_task2.html Решение Решение: tutorial/intro/ifelse_task2.html. [Открыть задачу в новом окне] 2

Напишите код, который получает значение prompt, а затем выводитalert:

• 1, если значение больше нуля, • -1, если значение меньше нуля, • 0, если значение равно нулю. Можно посмотреть в действии: tutorial/intro/if_sign.html Решение tutorial/intro/if_sign.html


[Открыть задачу в новом окне] 3

Напишите код, который будет спрашивать логин (prompt). Если посетитель вводит «Админ», то спрашивать пароль, если нажал отмена (escape) — выводить «Вход отменён», если вводит что-то другое — «Я вас не знаю». Пароль проверять так. Если введён пароль «Чёрный Властелин», то выводить «Добро пожаловать!», иначе — «Пароль неверен», при отмене — «Вход отменён». Блок-схема:

Для решения используйте вложенные блоки if. Обращайте внимание на стиль и читаемость кода. Результат в действии: tutorial/intro/ifelse_task.html Решение Решение: tutorial/intro/ifelse_task.html.


Обратите внимание на дополнительные вертикальные отступы внутри if. Они полезны для лучшей читаемости кода. [Открыть задачу в новом окне] Оператор вопросительн ый знак '?' Иногда нужно в зависимости от условия присвоить переменную. Например:

01 var access; 02 var age = prompt('Сколько вам лет?', ''); 03 04 if (age > 14) { 05 access = true; 06 } else { 07 access = false; 08 } 09 10 alert(access); Оператор вопросительный знак '?' позволяет делать это короче и проще. Он состоит из трех частей:

условие ? значение1 : значение2 Проверяется условие, затем если оно верно — возвращается значение1 , если неверно — значение2, например:

access = (age > 14) ? true : false; Оператор '?' выполняется позже большинства других, в частности — позже сравнений, поэтому скобки можно не ставить:

access = age > 14 ? true : false; .. Но когда скобки есть — код лучше читается. Так что рекомендуется их писать. В данном случае можно было бы обойтись и без оператора '?', т.к. сравнение само по себе уже возвращает true/false:

access = age > 14;


«Тернарный оператор» Вопросительный знак — единственный оператор, у которого есть аж три аргумента, в то время как у обычных операторов их один-два. Поэтому его называют «тернарный оператор». 5

Перепишите if с использованием оператора '?':

1 if (a + b < 4) { 2 result = 'Мало'; 3 } else { 4 result = 'Много'; 5} Решение

result = (a + b < 4) ? 'Мало' : 'Много'; [Открыть задачу в новом окне] Несколько операторов '?' Несколько операторов if..else можно заменить последовательностью операторов '?'. Например:

1 var a = prompt('a?', 1); 2 3 var res = (a == 1) ? 'значение1' : 4 (a == 2) ? 'значение2' : 5 (a > 2) ? 'значение3' : 6 'значение4'; 7 8 alert(res); Поначалу может быть сложно понять, что происходит. Однако, внимательно приглядевшись, мы замечаем, что это обычный if..else! Вопросительный знак проверяет сначала a == 1, если верно — возвращает значение1, если нет — идет проверять a == 2. Если это верно — возвращает значение2, иначе проверка a > 2 изначение3.. Наконец, если ничего не верно, то значение4. Альтернативный вариант с if..else:

01 var res;


02 03 if (a == 1) { 04 res = 'значение1'; 05 } else if (a == 2) { 06 res = 'значение2'; 07 } else if (a > 2) { 08 res = 'значение3'; 09 } else { 10 res = 'значение4'; 11 } 5 Перепишите if..else с использованием нескольких операторов '?'. Для читаемости — оформляйте код в несколько строк.

01 var message; 02 03 if (login == 'Вася') { 04 message = 'Привет'; 05 } else if (login == 'Директор') { 06 message = 'Здравствуйте'; 07 } else if (login == '') { 08 message = 'Нет логина'; 09 } else { 10 message = ''; 11 } Решение

1 var message = (login == 'Вася') ? 'Привет' : 2 (login == 'Директор') ? 'Здравствуйте' : 3 (login == '') ? 'Нет логина' : 4 ''; [Открыть задачу в новом окне] Нетрадиционное использование '?' Иногда оператор вопросительный знак '?' используют как замену if:

1 var company = prompt('Какая компания создала JavaScript?', ''); 2 3 (company == 'Netscape') ? 4 alert('Да, верно') : alert('Неправильно');


Работает это так: в зависимости от условия, будет выполнена либо первая, либо вторая часть после'?'. Результат выполнения не присваивается в переменную, так что пропадёт (впрочем, alert ничего не возвращает). Рекомендуется не использовать вопросительный знак таким образом. Несмотря на то, что с виду такая запись короче if, она является существенно менее читаемой. Вот, для сравнения, то же самое с if:

1 var company = prompt('Какая компания создала JavaScript?', ''); 2 3 if (company == 'Netscape') { 4 alert('Да, верно'); 5 } else { 6 alert('Неправильно'); 7} Логические операторы

1. 2. 3. 4. 5.

|| (ИЛИ) Короткий цикл вычислений Значение ИЛИ && (И) ! (НЕ)

В JavaScript поддерживаются операторы || (ИЛИ), && (И) и ! (НЕ). Они называются «логическими», но в JavaScript могут применяться к значениям любого типа и возвращают также значения любого типа. || (ИЛИ) Оператор ИЛИ выглядит как двойной символ вертикальной черты:

result = a || b; Логическое ИЛИ в классическом программировании работает следующим образом: «еслихотя бы один из аргументов true, то возвращает true, иначе — false». Получается следующая таблица результатов:

1 alert( 2 alert( 3 alert( 4 alert(

true false true false

|| || || ||

true ); true ); false); false);

// // // //

true true true false


При вычислении ИЛИ в JavaScript можно использовать любые значения. В этом случае они будут интерпретироваться как логические. Например, число 1 будет воспринято как true, а 0 — как false:

1 if ( 1 || 0 ) { // сработает как if( true || false ) 2 alert('верно'); 3} Обычно оператор ИЛИ используется в if, чтобы проверить, выполняется ли хотя бы одно из условий, например:

1 var hour = 9; 2 3 if (hour < 10 || hour > 18) { 4 alert('Офис до 10 или после 18 закрыт'); 5} Можно передать и больше условий:

1 var hour = 12, isWeekend = true; 2 3 if (hour < 10 || hour > 18 || isWeekend) { 4 alert('Офис до 10 или после 18 или в выходной закрыт'); 5} Короткий цикл вычислений JavaScript вычисляет несколько ИЛИ слева направо. При этом, чтобы экономить ресурсы, используется так называемый «короткий цикл вычисления». Допустим, вычисляются несколько ИЛИ подряд: a || b || c || .... Если первый аргумент — true, то результат заведомо будет true (хотя бы одно из значений — true), и остальные значения игнорируются. Это особенно заметно, когда выражение, переданное в качестве второго аргумента, имеет сторонний эффект — например, присваивает переменную. При запуске примера ниже присвоение x не произойдёт:

1 var x; 2 3 true || (x = 1); // просто вычислим ИЛИ, без if 4 5 alert(x); // undefined, x не присвоен


…А в примере ниже первый аргумент — false, так что ИЛИ попытается вычислить второй, запустив тем самым присваивание:

1 var x; 2 3 false || (x = 1); 4 alert(x); // 1 Значение ИЛИ Итак, как мы видим, оператор ИЛИ вычисляет ровно столько значений, столько необходимо — до первого true. Оператор ИЛИ возвращает то значение, на котором остановились вычисления. Примеры:

1 alert( 2 alert( 3 4 alert( 5 alert(

1 || 0 ); // 1 true || 'неважно что'); // true null || 1 ); // 1 undefined || 0 ); // 0

Это используют, в частности, чтобы выбрать первое «истинное» значение из списка:

1 var undef; // переменная не присвоена, т.е. равна undefined 2 var zero = 0; 3 var emptyStr = ""; 4 var msg = "Привет!"; 5 6 var result = undef || zero || emptyStr || msg || 0; 7 alert(result) // выведет "Привет!" - первое значение, которое является 8 true 3

Что выведет код ниже?

alert( alert(1) || alert(2) || 3 || alert(4) ); Решение Ответ: 1, 2, 3.


1 alert( alert(1) || alert(2) || 3 || alert(4) ); Вызов alert не возвращает значения, или, иначе говоря, возвращает undefined.

1. Оператор ИЛИ выполнит первый alert, получит undefined и пойдёт дальше.

2. Оператор ИЛИ выполнит второй alert (опять undefined) и пойдёт дальше.

3. Оператор ИЛИ далее вычисляет undefined || 3, результат: 3. Вычисления завершены. [Открыть задачу в новом окне] && (И) Оператор И пишется как два амперсанда &&:

result = a && b; В классическом программировании И возвращает true, если оба аргумента истинны, а иначе — false

1 alert( 2 alert( 3 alert( 4 alert(

true false true false

&& && && &&

true ); true ); false); false);

// // // //

true false false false

Пример:

1 var hour = 12, minute = 30; 2 3 if (hour == 12 && minute == 30) { 4 alert('Время 12:30'); 5} Как и в ИЛИ, допустимы любые значения:

1 if ( 1 && 0 ) { // вычислится как true && false 2 alert('не сработает, т.к. условие ложно'); 3} К И применим тот же принцип «короткого цикла вычислений», но немного по-другому, чем к ИЛИ. Если левый аргумент — false, оператор И возвращает его и заканчивает вычисления. Иначе — вычисляет и возвращает правый аргумент.


Например:

1 // Первый аргумент - true, 2 // Поэтому возвращается второй аргумент 3 alert(1 && 0); // 0 4 alert(1 && 5); // 5 5 6 // Первый аргумент - false, 7 // Он и возвращается, а второй аргумент игнорируется 8 alert(null && 5); // null 9 alert(0 && "не важно"); // 0 3

Что выведет код ниже?

alert( alert(1) && alert(2) ); Решение Ответ: 1, undefined.

1 alert( alert(1) && alert(2) ); Вызов alert не возвращает значения, или, иначе говоря, возвращает undefined. Поэтому до правого alert дело не дойдёт, вычисления закончатся на левом. [Открыть задачу в новом окне] Приоритет оператора И && больше, чем ИЛИ ||, т.е. он выполняется раньше. Поэтому в следующем коде сначала будет вычислено правое И: 1 && 0 = 0, а уже потом — ИЛИ.

1 alert(5 || 1 && 0); // 5

Не используйте && вместо if Оператор && в простых случаях можно использовать вместо if, например:

1 var x = 1;


2 3 (x > 0) && alert('Больше'); Действие в правой части && выполнится только в том случае, если до него дойдут вычисления. То есть, если в левой части будет true. Получился аналог:

1 var x = 1; 2 3 if (x > 0) { 4 alert('Больше'); 5} Однако, как правило, if лучше читается и воспринимается. Он более очевиден, поэтому лучше использовать его. Это, впрочем, относится и к другим неочевидным применениям возможностей языка. ! (НЕ) Оператор НЕ — самый простой. Он получает один аргумент. Синтаксис:

var result = !value; Действия !:

1. Сначала приводит аргумент к логическому типу true/false. 2. Затем возвращает противоположное значение. Например:

1 alert( !true ) // false 2 alert( !0 ) // true В частности, двойное НЕ используются для преобразования значений к логическому типу:

1 alert( !!"строка" ) // true 2 alert( !!null ) // false 3 Напишите условие if для проверки того факта, что переменная ageнаходится между 14 и 90 включительно.


«Включительно» означает, что концы промежутка включены, то есть age может быть равна 14 или 90. Решение

if (age >= 14 && age <= 90) [Открыть задачу в новом окне] 3

Напишите условие if для проверки того факта, что age НЕ находится между 14 и 90 включительно. Сделайте два варианта условия: первый с использованием оператора НЕ !, второй - без этого оператора. Решение Первый вариант:

if ( !(age >= 14 && age <= 90) ) Второй вариант:

if (age < 14 || age > 90) [Открыть задачу в новом окне] 5

Какие из этих if верны, т.е. выполнятся? Какие значения будут результатами выражений в условиях if?

if (-1 || 0) alert('первое'); if (-1 && 0) alert('второе'); if (null || -1 && 1) alert('третье'); Пример решения:

// 1. Да, выполнится // 2. Выражение внутри false || 1 будет равно 1 if (false || 1) alert('тест'); Решение


Ответ: первое и третье выполнятся. Детали:

01 // Да, выполнится, т.к. -1 в логическом контексте true 02 // -1 || 0 = -1 03 if (-1 || 0) alert('первое'); 04 // Не выполнится, т.к. -1 интерпретируется как true, а 0 как 05 false 06 // -1 && 0 = 0 07 if (-1 && 0) alert('второе'); 08 09 // Да, выполнится 10 // оператор && имеет больший приоритет, чем || 11 // так что -1 && 1 выполнится раньше, будет null || 1 = 1 12 // получится null || -1 && 1 -> null || 1 -> 1 13 if (null || -1 && 1) alert('третье'); [Открыть задачу в новом окне] Циклы while, for

1. Цикл while 2. Цикл do..while 3. Цикл for При написании скриптов зачастую встает задача сделать однотипное действие много раз. Например, вывести товары из списка один за другим. Или просто перебрать все числа от 1 до 10 и для каждого выполнить одинаковый код. Для многократного повторения одного участка кода - предусмотрены циклы. Цикл while Цикл while имеет вид:

while (условие) { // код, тело цикла } Пока условие верно — выполняется код из тела цикла. Например, цикл ниже выводит i пока i < 3:

1 var i = 0; 2 while (i < 3) { 3 alert(i);


4 i++; 5} Повторение цикла по-научному называется «итерация». Цикл в примере выше совершает три итерации. Если бы i++ в коде выше не было, то цикл выполнялся бы (в теории) вечно. На практике, браузер выведет сообщение о «зависшем» скрипте и посетитель его остановит. Бесконечный цикл можно сделать и проще:

while (true) { // ... } Условие в скобках интерпретируется как логическое значение, поэтому вместо while (i! =0) обычно пишут while (i):

1 var i = 3; while (i) { // при i=0 значение в скобках будет false и цикл 2 остановится 3 alert(i); 4 i--; 5} Цикл do..while Проверку условия можно поставить под телом цикла, используя специальный синтаксис do..while:

do { // тело цикла } while (условие); Цикл, описанный, таким образом, сначала выполняет тело, а затем проверяет условие. Например:

1 var i = 0; 2 do { 3 alert(i); 4 i++; 5 } while (i < 3); Синтаксис do..while редко используется, т.к. обычный while нагляднее — в нём не приходится искать глазами условие и ломать голову, почему оно проверяется именно в конце. 3


Какое последнее значение выведет этот код? Почему?

1 var i = 3; 2 3 while(i) { 4 alert(i--); 5} Решение Ответ: 1.

1 var i = 3; 2 3 while(i) { 4 alert(i--); 5} Каждое выполнение цикла уменьшает i. Проверка while(i) даст сигнал «стоп» приi = 0. Соответственно, шаги цикла:

1 var i = 3 2 alert(i--); // выведет 3, затем уменьшит i до 2 3 4 alert(i--) // выведет 2, затем уменьшит i до 1 5 6 alert(i--) // выведет 1, затем уменьшит i до 0 7 8 // все, проверка while(i) не даст выполняться циклу дальше [Открыть задачу в новом окне] Цикл for Чаще всего применяется цикл for. Выглядит он так:

for (начало; условие; шаг) { // ... тело цикла ... } Например, цикл ниже выводит значения от 0 до 3 (не включая 3):

1 var i;


2 3 for (i=0; i<3; i++) { 4 alert(i); 5} • Начало i=0 выполняется при заходе в цикл. • Условие i<3 проверяется перед каждой итерацией. • Шаг i++ выполняется после каждой итерации, но перед проверкой условия. В цикле также можно определить переменную:

for (var i=0; i<3; i++) { ... } Любая часть for может быть пропущена. Например, можно убрать начало:

var i = 0; for (; i<3; i++) ... Можно убрать и шаг:

1 var i = 0; 2 for (; i<3; ) { 3 // цикл превратился в аналог while (i<3) 4} А можно и вообще убрать все, получив бесконечный цикл:

for (;;) { // будет выполняться вечно } При этом сами точки с запятой ; обязательно должны присутствовать, иначе будет ошибка синтаксиса.

for..in Существует также специальная конструкция for..in для перебора свойств объекта. Мы познакомимся с ней позже, когда будем говорить об объектах.

4


Для каждого цикла запишите, какие значения он выведет. Потом сравните с ответом. 1. Префиксный вариант

var i = 0; while (++i < 5) alert(i); 2. Постфиксный вариант

var i = 0; while (i++ < 5) alert(i); Решение

1. От 1 до 4 1 var i = 0; 2 while (++i < 5) alert(i); Первое значение: i=1, так как операция ++i сначала увеличит i, а потом уже произойдёт сравнение и выполнение alert. Далее 2,3,4.. Значения выводятся одно за другим. Для каждого значения сначала происходит увеличение, а потом — сравнение, так как ++ стоит перед переменной. При i=4 произойдет увеличение i до 5, а потом сравнение while(5 < 5) — это неверно. Поэтому на этом цикл остановится, и значение 5 выведено не будет.

2. От 1 до 5 1 var i = 0; 2 while (i++ < 5) alert(i); Первое значение: i=1. Остановимся на нём подробнее. Оператор i+ +увеличивает i, возвращая старое значение, так что в сравнении i++ < 5 будет участвовать старое i=0. Но последующий вызов alert уже не относится к этому выражению, так что получит новый i=1. Далее 2,3,4.. Для каждого значения сначала происходит сравнение, а потом — увеличение, и затем срабатывание alert.


Окончание цикла: при i=4 произойдет сравнение while(4 < 5) — верно, после этого сработает i++, увеличив i до 5, так что значение 5 будет выведено. Оно станет последним. [Открыть задачу в новом окне] 4

Для каждого цикла запишите, какие значения он выведет. Потом сравните с ответом. 1. Постфиксная форма:

for(var i=0; i<5; i++) alert(i); 2. Префиксная форма:

for(var i=0; i<5; ++i) alert(i); Решение Ответ: от 0 до 4 в обоих случаях.

1 for(var i=0; i<5; ++i) alert(i); 2 3 for(var i=0; i<5; i++) alert(i); Такой результат обусловлен алгоритмом работы for:

1. Выполнить присвоение i=0 2. Проверить i<5 3. Если верно - выполнить тело цикла alert(i), затем выполнить i++ Увеличение i++ выполняется отдельно от проверки условия (2), значение i при этом не используется, поэтому нет никакой разницы между i++ и ++i. [Открыть задачу в новом окне] 1 Посмотрите страницу tutorial/intro/source/loop.html. Перепишите код, заменив цикл for на while, без изменения поведения цикла. Решение


tutorial/intro/loop.html. [Открыть задачу в новом окне] 5

Напишите цикл, который предлагает prompt ввести число, большее 100. Если посетитель ввел другое число — попросить ввести еще раз, и так далее. Цикл должен спрашивать число пока либо посетитель не введет число, большее 100, либо не нажмет кнопку Cancel (ESC). Предполагается, что посетитель вводит только числа. Пример работы. Решение Решение:

1 var num; 2 3 do { 4 num = prompt("Введите число больше 100?", 0); 5 } while(num <= 100 && num != null); В действии: tutorial/intro/endless_loop.html Цикл do..while повторяется, пока верны две проверки:

1. Проверка num <= 100 — то есть, введённое число всё еще меньше 100. 2. Проверка num != null — значение null означает, что посетитель нажал «Отмена», в этом случае цикл тоже нужно прекратить. Кстати, сравнение num <= 100 при вводе null даст true, так что вторая проверка необходима. [Открыть задачу в новом окне] Директивы break и continue

1. Выход: break 2. Следующая итерация: continue 3. Метки Для более гибкого управления циклом используются директивы break и continue.


В ыход: break Выйти из цикла можно не только при проверке условия но и, вообще, в любой момент. Эту возможность обеспечивает директива break. Например, бесконечный цикл в примере прекратит выполнение при i==5:

01 var i=0; 02 03 while(1) { 04 i++; 05 06 if (i==5) break; 07 08 alert(i); 09 } 10 11 alert('Последняя i = '+ i ); // 5 (*) Выполнение продолжится со строки (*), следующей за циклом. Следу ющая итерация: continue Директива continue прекращает выполнение текущей итерации цикла. Например, цикл ниже не выводит четные значения:

1 for (var i = 0; i < 10; i++) { 2 3 if (i % 2 == 0) continue; 4 5 alert(i); 6} Для четных i срабатывает continue, выполнение блока прекращается и управление передается наfor.

Совет по стилю Как правило, continue и используют, чтобы не обрабатывать определенные значения в цикле. Цикл, который обрабатывает только часть значений, мог бы выглядеть так:

01 for (var i = 0; i < 10; i++) { 02


03 if ( checkValue(i) ) { 04 // функция checkValue проверяет, подходит ли i 05 06 // ... 07 // ... обработка 08 // ... этого 09 // ... значения 10 // ... цикла 11 // ... 12 13 } 14 } Все хорошо, но мы получили дополнительный уровень вложенности фигурных скобок, без которого можно и нужно обойтись. Гораздо лучше здесь использовать continue:

01 for (var i = 0; i < 10; i++) { 02 03 if ( !checkValue(i) ) continue; 04 05 // здесь мы точно знаем, что i подходит 06 07 // ... 08 // ... обработка 09 // ... этого 10 // ... значения 11 // ... цикла 12 // ... 13 14

}

Нельзя использовать break/continue справа от оператора ‘?’ Обычно мы можем заменить if на оператор вопросительный знак '?'. То есть, запись:


1 if (условие) { 2 a(); 3 } else { 4 b(); 5}

..Аналогична записи:

условие ? a() : b(); В обоих случаях в зависимости от условия выполняется либо a() либо b(). Но разница состоит в том, что оператор вопросительный знак '?', использованный во второй записи, возвращает значение. Синтаксические конструкции, которые не возвращают значений, нельзя использовать в операторе '?'. К таким относятся большинство конструкций и, в частности, break/continue. Поэтому такой код приведёт к ошибке:

(i > 5) ? alert(i) : continue;

Метки

Бывает нужно выйти одновременно из нескольких уровней цикла. Представим, что нужно ввести значения точек. У каждой точки есть две координаты (i, j). Цикл для ввода значений i,j = 0..2 может выглядеть так:

01 for (var i = 0; i < 3; i++) { 02 03 for (var j = 0; j < 3; j++) { 04 05 var input = prompt("Значение в координатах " + i + "," + j, ""); 06 07 if (input == null) break; // (*) 08


09 } 10 } 11 alert('Готово!'); Здесь break используется, чтобы прервать ввод, если посетитель нажал на Отмена. Но обычный вызовbreak в строке (*) не может прервать два цикла сразу. Как же прервать ввод полностью? Один из способов — поставить метку. Метка имеет вид "имя:", имя должно быть уникальным. Она ставится перед циклом, вот так:

outer: for (var i = 0; i < 3; i++) { ... } Можно также выносить ее на отдельную строку. Вызов break outer прерывает управление цикла с такой меткой, вот так:

01 outer: 02 for (var i = 0; i < 3; i++) { 03 04 for (var j = 0; j < 3; j++) { 05 06 var input = prompt('Значение в координатах '+i+','+j, ''); 07 08 if (input == null) break outer; // (*) 09 10 } 11 } 12 alert('Готово!'); Директива continue также может быть использована с меткой. Управление перепрыгнет на следующую итерацию цикла с меткой. Метки можно ставить в том числе на блок, без цикла:

01 my: { 02 03 for (;;) { 04 for (i=0; i<10; i++) { 05 if (i>4) break my; 06 } 07 } 08 09 some_code; // произвольный участок кода 10 11 } 12 alert("После my"); // (*)


В примере выше, break перепрыгнет через some_code, выполнение продолжится сразу после блокаmy, со строки (*). Возможность ставить метку на блоке используется редко. Обычно метки ставятся перед циклом.

Goto? В некоторых языках программирования есть оператор goto, который может передавать управление на любой участок программы. Операторы break/continue более ограниченны. Они работают только внутри циклов, и метка должна быть не где угодно, а выше по уровню вложенности. В JavaScript нет goto. 4

Натуральное число, большее 1, называется простым, если оно ни на что не делится, кроме себя и 1. Другими словами, n>1 - простое, если при делении на любое число от 2 до n1 есть остаток. Создайте код, который выводит все простые числа из интервала от 2 до 10.Результат должен быть: 2,3,5,7. P.S. Код также должен легко модифицироваться для любых других интервалов. Решение Схема решения

1 Для всех i от 1 до 10 { 2 проверить, делится ли число i на какое-либо из чисел до него 3 если делится, то это i не подходит, берем следующее 4 если не делится, то i - простое число 5} Решение Решение с использованием метки:

1 nextPrime: 2 for(var i=2; i<10; i++) { 3


4 for(var j=2; j<i; j++) { 5 if ( i % j == 0) continue nextPrime; 6 } 7 8 alert(i); // простое 9} Конечно же, его можно оптимизировать с точки зрения производительности. Например, проверять все j не от 2 до i, а от 2 до квадратного корня из i. А для очень больших чисел — существуют более эффективные специализированные алгоритмы проверки простоты числа, например квадратичное решето и решето числового поля. [Открыть задачу в новом окне] Конструкция switch

1. 2. 3. 4.

Синтаксис Пример работы Группировка case Тип имеет значение

Конструкция switch заменяет собой сразу несколько if. Это — более наглядный способ сравнить выражение сразу с несколькими вариантами. Синтаксис Выглядит она так:

01 switch(x) { 02 case 'value1': // if (x === 'value1') 03 ... 04 [break] 05 06 case 'value2': // if (x === 'value2') 07 ... 08 [break] 09 10 default: 11 ... 12 [break] 13 } • Переменная x проверяется на строгое равенство первому значению value1, затем второмуvalue2 и так далее.

• Если соответствие установлено — switch начинает выполняться от соответствующей директивыcase и далее, до ближайшего break (или до конца switch). При этом case называют вариантами switch.


• Если ни один case не совпал — выполняетcя (если есть) вариант default. Пример работ ы Пример использования switch (сработавший код выделен):

01 var a = 2+2; 02 03 switch (a) { 04 case 3: 05 alert('Маловато'); 06 break; 07 case 4: 08 alert('В точку!'); 09 break; 10 case 5: 11 alert('Перебор'); 12 break; 13 default: 14 alert('Я таких значений не знаю'); 15 } Будет выведено только одно значение, соответствующее 4. После чего break прервёт выполнение. Если его не прервать — оно пойдёт далее, при этом остальные проверки игнорируются. Например:

01 var a = 2+2; 02 03 switch (a) { 04 case 3: 05 alert('Маловато'); 06 case 4: 07 alert('В точку!'); 08 case 5: 09 alert('Перебор'); 10 default: 11 alert('Я таких значений не знаю'); 12 } В примере выше последовательно выполнятся три alert.

alert('В точку!');


alert('Перебор'); alert('Я таких значений не знаю'); В case могут быть любые выражения, в том числе включающие в себя переменные и функции. Например:

01 var a = 1; 02 var b = 0; 03 04 switch(a) { 05 case b+1: 06 alert(1); 07 break; 08 09 default: 10 alert('нет-нет, выполнится вариант выше') 11 } Группировка case Несколько значений case можно группировать. В примере ниже case 3 и case 5 выполняют один и тот же код:

01 var a = 2+2; 02 03 switch (a) { 04 case 4: 05 alert('Верно!'); 06 break; 07 08 case 3: // (*) 09 case 5: // (**) 10 alert('Неверно!'); 11 break; 12 13 default: 14 alert('Я таких значений не знаю'); 15 } При case 3 выполнение идёт со строки (3) и идёт вниз до ближайшего break, таким образом проходя и то, что предназначено для case 5. 5


Напишите if..else, соответствующий следующему switch:

01 switch (browser) { 02 case 'IE': 03 alert('О, да у вас IE!'); 04 break; 05 06 case 'Chrome': 07 case 'Firefox': 08 case 'Safari': 09 case 'Opera': 10 alert('Да, и эти браузеры мы поддерживаем'); 11 break; 12 13 default: 14 alert('Мы надеемся, что и в вашем браузере все ок!'); 15 } Решение Если совсем точно следовать условию, то сравнение должно быть строгим '==='. В реальном случае, скорее всего, подойдёт обычное сравнение '=='.

1 if(browser == 'IE') { 2 alert('О, да у вас IE!'); 3 } else if (browser == 'Chrome' || browser == 'Firefox' 4 || browser == 'Safari' || browser == 'Opera') { 5 alert('Да, и эти браузеры мы поддерживаем'); 6 } else { 7 alert('Мы надеемся, что и в вашем браузере все ок!'); 8} Как видно, эта запись существенно хуже читается, чем конструкция switch. [Открыть задачу в новом окне] Тип имеет значение Следующий пример принимает значение от посетителя.

01 var arg = prompt("Введите arg?") 02 switch(arg) { 03 case '0':


04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 }

case '1': alert('Один или ноль'); case '2': alert('Два'); break; case 3: alert('Никогда не выполнится'); case null: alert('Отмена'); break; default: alert('Неизвестное значение: ' + arg)

Что оно выведет при вводе чисел 0, 1, 2, 3? Подумайте и потом читайте дальше…

• При вводе 0 или 1 выполнится первый alert, далее выполнение продолжится вниз до первогоbreak и выведет второй alert('Два').

• При вводе 2, switch перейдет к case '2' и выведет Два. • При вводе 3, switch перейдет на default. Это потому, что prompt возвращает строку '3', а не число. Типы разные. Switch использует строгое равенство ===, так что совпадения не будет.

• При отмене сработает case null. 4

Перепишите код с использованием одной конструкции switch:

01 var a = +prompt('a?', ''); 02 03 if (a == 0) { 04 alert(0); 05 } 06 if (a == 1) { 07 alert(1); 08 } 09 10 if (a == 2 || a == 3) { 11 alert('2,3'); 12 }


Решение Первые две проверки — обычный case, третья разделена на два case:

01 var a = +prompt('a?', ''); 02 03 switch(a) { 04 case 0: 05 alert(0); 06 break; 07 08 case 1: 09 alert(1); 10 break; 11 12 case 2: 13 case 3: 14 alert('2,3'); 15 break; 16 } Обратите внимание: break внизу не обязателен, но ставится по «правилам хорошего тона». Если он не стоит, то при дописывании в конец нового case, к примеру case 4, мы, скорее всего, забудем его поставить. В результате выполнение case 2/case 3продолжится на case 4 и будет ошибка. [Открыть задачу в новом окне] Функции

1. 2. 3. 4. 5. 6. 7. 8. 9.

Объявление Локальные переменные Внешние переменные Параметры Аргументы по умолчанию Стиль объявления функций Возврат значения Выбор имени Итого

Зачастую нам надо повторять одно и то же действие во многих частях программы. Например, красиво вывести сообщение необходимо при приветствии посетителя, при выходе посетителя с сайта, еще где-нибудь.


Чтобы не повторять один и тот же код во многих местах, придуманы функции. Функции являются основными «строительными блоками» программы. Примеры встроенных функций вы уже видели — это alert(message), prompt(message, default) иconfirm(question). Но можно создавать и свои. Объявление Пример объявления функции:

function showMessage() { alert('Привет всем присутствующим!'); } Вначале идет ключевое слово function, после него имя функции, затем список параметров в скобках (в примере выше он пустой) и тело функции — код, который вызывается при её вызове. Объявленная функция доступна по имени, например:

1 function showMessage() { 2 alert('Привет всем присутствующим!'); 3} 4 5 showMessage(); 6 showMessage(); Этот код выведет сообщение два раза. Уже здесь видна главная цель создания функций: избавление от дублирования кода. Если понадобится поменять сообщение или способ его вывода — достаточно изменить его в одном месте: в функции, которая его выводит. Локальн ые переменн ые Функция может содержать локальные переменные, объявленные через var. Такие переменные видны только внутри функции:

1 function showMessage() { 2 var message = 'Привет, я - Вася!'; // локальная переменная 3 4 alert(message); 5} 6 7 showMessage(); // 'Привет, я - Вася!' 8 alert(message); // <-- будет ошибка, т.к. переменная видна только 9 внутри


Блоки if/else, switch, for, while, do..while не влияют на область видимости переменных. При объявлении переменной в таких блоках, она всё равно будет видна во всей функции. Например:

1 function count() { 2 for (var i=0; i<3; i++) { 3 var j = i * 2; 4 } 5 6 alert(i); // i=3, на этом значении цикл остановился alert(j); // j=4, последнее значение, на котором цикл сработал, было 7 i=2 Неважно, где именно в функции и сколько раз объявляется переменная. Любое объявление срабатывает один раз и распространяется на всю функцию. Объявления переменных в примере выше можно передвинуть вверх, это ни на что не повлияет:

1 function count() { 2 var i, j; // передвинули объявления var в начало 3 for (i=0; i<3; i++) { 4 j = i * 2; 5 } 6 7 alert(i); // i=3 8 alert(j); // j=4 9} Внешние переменные Функция может обратиться ко внешней переменной, например:

1 var userName = 'Вася'; 2 3 function showMessage() { 4 var message = 'Привет, я ' + userName; 5 alert(message); 6} 7 8 showMessage(); // Привет, я Вася Доступ возможен не только на чтение, но и на запись. При этом, так как переменная внешняя, то изменения будут видны и снаружи функции:


01 var userName = 'Вася'; 02 03 function showMessage() { 04 userName = 'Петя'; // (1) присвоение во внешнюю переменную 05 var message = 'Привет, я ' + userName; 06 alert(message); 07 } 08 09 showMessage(); 10 alert(userName); // Петя, значение внешней переменной изменено 11 функцией Конечно, если бы внутри функции, в строке (1), была бы объявлена своя локальная переменнаяvar userName, то все обращения использовали бы её, и внешняя переменная осталась бы неизменной. Переменные, объявленные на уровне всего скрипта, называют «глобальными переменными». Делайте глобальными только те переменные, которые действительно имеют общее значение для вашего проекта. Пусть каждая функция работает «в своей песочнице».

Внимание: неявное объявление глобальных переменных! В старом стандарте JavaScript существовала возможность неявного объявления переменных присвоением значения. Например:

1 function showMessage() { 2 message = 'Привет'; // без var! 3} 4 5 showMessage(); 6 7 alert(message); // Привет В коде выше переменная message нигде не объявлена, а сразу присваивается. Скорее всего, программист просто забыл поставить var.


В современном стандарте JavaScript такое присвоение запрещено, а в старом, который работает в браузерах по умолчанию, переменная будет создана автоматически, причём в примере выше она создаётся не в функции, а на уровне всего скрипта. Избегайте этого. Здесь опасность даже не в автоматическом создании переменной, а в том, что глобальные переменные должны использоваться тогда, когда действительно нужны «общескриптовые» параметры. Забыли var в одном месте, потом в другом — в результате две функции неожиданно друг для друга поменяли одну и ту же глобальную переменную.

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

1 function showMessage(from, text) { // параметры from, text 2 from = "** " + from + " **"; // здесь может быть сложный код 3 оформления 4 alert(from + '\n\n' + text); 5} 6 7 showMessage('Маша', 'Привет!'); 8 showMessage('Маша', 'Как дела?'); Параметры копируются в локальные переменные функции. В примере ниже изменение from в строке (1) не отразится на значении внешней переменной from(2), т.к. изменена была копия значения:

1 function showMessage(from, text) { 2 from = '**' + from + '**'; // (1), красиво оформили from 3 alert(from + '\n\n' + text); 4} 5


6 var from = 'Маша', msg = 'Привет!'; // (2) 7 8 showMessage(from, msg); // значения будут скопированы в параметры alert(from); // перезапись в строке (1) не повлияет на внешнюю 9 переменную Аргументы по умолчанию Функцию можно вызвать с любым количеством аргументов. Например, функцию показа сообщения showMessage(from, text) можно вызвать с одним аргументом:

showMessage("Маша"); Если параметр не передан при вызове — он считается равным undefined. Такую ситуацию можно отловить и назначить значение «по умолчанию»:

01 function showMessage(from, text) { 02 if (text === undefined) { 03 text = 'текст не передан'; 04 } 05 06 alert(from + ": " + text); 07 } 08 09 showMessage("Маша", "Привет!"); // Маша: Привет! 10 showMessage("Маша"); // Маша: текст не передан При объявлении функции необязательные аргументы, как правило, располагают в конце списка. Для указания значения «по умолчанию», то есть, такого, которое используется, если аргумент не указан, используется два способа:

1. Можно проверить, равен ли аргумент undefined, и если да — то записать в него значение по умолчанию. Этот способ продемонстрирован в примере выше.

2. Использовать оператор ||: 1 function showMessage(from, text) { 2 text = text || 'текст не передан'; 3 4 ... 5} Второй способ считает, что аргумент отсутствует, если передана пустая строка, 0, или вообще любое значение, которое в булевом виде является false.


Если аргументов передано больше, чем надо, напримерshowMessage("Маша", "привет", 1, 2, 3), то ошибки не будет. Но так как для «лишних» аргументов не предусмотрены параметры, то доступ к ним можно будет получить только через специальный объект arguments, который мы рассмотрим в главе Псевдо-массив arguments. Стиль объявления функций В объявлении функции есть правила для расстановки пробелов. Они отмечены стрелочками:

Конечно, вы можете ставить пробелы и по-другому, но эти правила используются в большинстве JavaScript-фреймворков. Возврат значения Функция может возвратить результат, который будет передан в вызвавший её код. Например, создадим функцию calcD, которая будет возвращать дискриминант квадратного уравнения по формуле b2 - 4ac:

1 function calcD(a, b, c) { 2 return b*b - 4*a*c; 3} 4 5 var test = calcD(-4, 2, 1); 6 alert(test); // 20 Для возврата значения используется директива return. Она может находиться в любом месте функции. Как только до нее доходит управление — функция завершается и значение передается обратно. Вызовов return может быть и несколько, например:

01 function checkAge(age) {


02 if (age > 18) { 03 return true; 04 } else { 05 return confirm('Родители разрешили?'); 06 } 07 } 08 09 var age = prompt('Ваш возраст?'); 10 11 if (checkAge(age)) { 12 alert('Доступ разрешен'); 13 } else { 14 alert('В доступе отказано'); 15 } 4 Следующая функция возвращает true, если параметр age больше 18. В ином случае она задает вопрос confirm и возвращает его результат.

1 function checkAge(age) { 2 if (age > 18) { 3 return true; 4 } else { 5 // ... 6 return confirm('Родители разрешили?'); 7 } 8} Будет ли эта функция работать как-то иначе, если убрать else?

1 function checkAge(age) { 2 if (age > 18) { 3 return true; 4 } 5 // ... 6 return confirm('Родители разрешили?'); 7} Есть ли хоть одно отличие в поведении этого варианта? Решение


Оба варианта функции работают одинаково, отличий нет. [Открыть задачу в новом окне] 4

Следующая функция возвращает true, если параметр age больше 18. В ином случае она задает вопрос confirm и возвращает его результат.

1 function checkAge(age) { 2 if (age > 18) { 3 return true; 4 } else { 5 return confirm('Родители разрешили?'); 6 } 7} Перепишите функцию, чтобы она делала то же самое, но без if, в одну строку. Сделайте два варианта функции checkAge:

1. Используя оператор '?' 2. Используя оператор || Решение Используя оператор '?':

function checkAge(age) { return (age > 18) ? true : confirm('Родители разрешили?'); } Используя оператор || (самый короткий вариант):

function checkAge(age) { return (age > 18) || confirm('Родители разрешили?'); } [Открыть задачу в новом окне] Директива return может также использоваться без значения, чтобы прекратить выполнение и выйти из функции. Например:

1 function showMovie(age) {


2 3 4 5 6 7 8}

if (!checkAge(age)) { return; } alert("Фильм не для всех"); // (*) // ...

В коде выше, если сработал if, то строка (*) и весь код под ней никогда не выполнится, так какreturn завершает выполнение функции.

Значение функции без return и с пустым return В случае, когда функция не вернула значение или return был без аргументов, считается что она вернула undefined:

1 function doNothing() { /* пусто */ } 2 3 alert( doNothing() ); // undefined Обратите внимание, никакой ошибки нет. Просто возвращается undefined. Ещё пример, на этот раз с return без аргумента:

1 function doNothing() { 2 return; 3} 4

5

alert( doNothing() === undefined ); // true

Выбор имени

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


Функции, которые начинаются с "show" — что-то показывают:

showMessage(..)

// префикс show, "показать" сообщение

Функции, начинающиеся с "get" — получают, и т.п.:

1 getAge(..) 2 calcD(..) 3 createForm(..) checkPermission(..) 4 true/false

// // // //

get, "получает" возраст calc, "вычисляет" дискриминант create, "создает" форму check, "проверяет" разрешение, возвращает

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

Одна функция — одно действие Функция должна делать только то, что явно подразумевается её названием. И это должно быть одно действие. Если оно сложное и подразумевает поддействия — может быть имеет смысл выделить их в отдельные функции? Зачастую это имеет смысл, чтобы лучше структурировать код.

…Но самое главное — в функции не должно быть ничего, кроме самого действия и поддействий, неразрывно связанных с ним. Например, функция проверки данных (скажем, "validate") не должна показывать сообщение об ошибке. Её действие — проверить.

Сверхкороткие имена функций Имена функций, которые используются очень часто, иногда делают сверхкороткими. Например, во фреймворке jQuery есть функция $, во фреймворке Prototype — функция $$, а в библиотеке Underscore очень активно используется функция с названием из одного символа подчеркивания _. Итого Объявление функции:

function имя(параметры, через, запятую) { код функции


} • Передаваемые значения копируются в параметры функции и становятся локальными переменными. • Параметры функции являются её локальными переменными.

• • • •

Можно объявить новые локальные переменые при помощи var. Значение возвращается оператором return .... Вызов return тут же прекращает функцию. Если return; вызван без значения, или функция завершилась без return, то её результат равен undefined.

При обращении к необъявленной переменной функция будет искать внешнюю переменную с таким именем. По возможности, рекомендуется использовать локальные переменные и параметры: • Это делает очевидным общий поток выполнения — что передаётся в функцию и какой получаем результат. • Это предотвращает возможные конфликты доступа, когда две функции, возможно написанные в разное время или разными людьми, неожиданно используют одну и ту же внешнюю переменную. Именование функций: • Имя функции должно понятно и чётко отражать, что она делает. Увидев её вызов в коде, вы должны тут же понимать, что она делает. • Функция — это действие, поэтому для имён функций, как правило, используются глаголы. Функции являются основными строительными блоками скриптов. Мы будем неоднократно возвращаться к ним и изучать все более и более глубоко. 1

Задача «Hello World» для функций Напишите функцию min(a,b), которая возвращает меньшее из чисел a,b. Пример вызовов:

min(2, 5) == 2 min(3, -1) == -1 min(1, 1) == 1 Решение Код с if:

1 function min(a, b) { 2 if (a < b) {


3 return a; 4 } else { 5 return b; 6 } 7} Код с оператором '?':

function min(a, b) { return a < b ? a : b; } P.S. Случай равенства a == b отдельно не рассматривается, так как при этом неважно, что возвращать. [Открыть задачу в новом окне] 4

Напишите функцию pow(x,n), которая возвращает x в степени n. Иначе говоря, умножает x на себя n раз и возвращает результат.

pow(3, 2) = 3*3 = 9 pow(3, 3) = 3*3*3 = 27 pow(1, 100) = 1*1*...*1 = 1 Создайте страницу, которая запрашивает x и n, а затем выводит результат pow(x,n). Демо: tutorial/intro/pow.html P.S. В этой задаче функция обязана поддерживать только натуральные значения n, т.е. целые от 1 и выше. Решение Решение: tutorial/intro/pow.html [Открыть задачу в новом окне] Рекурсия, стек

1. Реализация pow(x, n) через рекурсию 2. Контекст выполнения, стек 3. Задачи на рекурсию В коде функции могут быть вызваны другие функции для выполнения подзадач. Частный случай


подвызова — когда функция вызывает сама себя. Это называется рекурсией. В этой главе мы рассмотрим, как рекурсия устроена изнутри, и как её можно использовать. Реализация pow(x, n) через рекурсию Чтобы возвести x в натуральную степень n — можно умножить его на себя n раз в цикле:

1 function pow(x, n) { 2 var result = x; 3 for(var i=1; i<n; i++) { 4 result *= x; 5 } 6 7 return result; 8} А можно поступить проще. Ведь xn = x * xn-1, т.е. можно вынести один x из-под степени. Таким образом, значение функцииpow(x,n) получается из pow(x, n-1) умножением на x. Этот процесс можно продолжить. Например, вычислим pow(2, 4):

1 pow(2, 2 pow(2, 3 pow(2, 4 pow(2,

4) 3) 2) 1)

= = = =

2 * pow(2, 3); 2 * pow(2, 2); 2 * pow(2, 1); 2;

…То есть, для степени pow(2, n) мы получаем результат как 2 * pow(2, n-1), затем уменьшаем nещё на единицу и так далее. Этот процесс останавливается на n==1, так как очевидно, чтоpow(x,1) == x. Код для такого вычисления:

1 function pow(x, n) { 2 // пока n!=1, сводить вычисление pow(..,n) к pow(..,n-1) 3 return (n != 1) ? x*pow(x, n-1) : x; 4} 5 6 alert( pow(2, 3) ); // 8 Говорят, что «функция pow рекурсивно вызывает сама себя». Значение, на котором рекурсия заканчивается называют базисом рекурсии. В примере выше базисом является 1. Общее количество вложенных вызовов называют глубиной рекурсии. В случае со степенью,


всего будет n вызовов. Максимальная глубина рекурсии ограничена и составляет около 10000, но это число зависит от браузера и может быть в 10 раз меньше. Рекурсию используют, когда вычисление функции можно свести к её более простому вызову, а его — еще к более простому, и так далее, пока значение не станет очевидно. Контекст в ыполнения, стек Теперь мы посмотрим, как работают рекурсивные вызовы. У каждого вызова функции есть свой «контекст выполнения» (execution context). Контекст выполнения включает в себя локальные переменные функции и другую служебную информацию, необходимую для её текущего выполнения. При любом вызове функции интерпретатор переключает контекст на новый. Этот процесс состоит из нескольких шагов: 1. Текущая функция приостанавливается. 2. Информация о её выполнении, то есть текущий контекст заносится в специальную внутреннюю структуру данных: «стек контекстов». 3. Запускается новая функция, для нее создаётся свой контекст. 4. По завершении подвызова предыдущий контекст достаётся из стека, выполнение в нём возобновляется. Например, для вызова:

1 function pow(x, n) { 2 return (n != 1) ? x*pow(x, n-1) : x; 3} 4 5 alert( pow(2, 3) ); Просходит следующее: pow(2, 3) Запускается функция pow, с аргументами x=2, n=3. Эти переменные хранятся в контексте выполнения, схематично изображённом ниже: • Контекст: { x: 2, n: 3 } pow(2, 2) В строке 2 происходит вызов pow, с аргументами x=2, n=2. Для этой функции создаётся новый текущий контекст (выделен красным), а предыдущий сохраняется в «стеке»: • Контекст: { x: 2, n: 3 } • Контекст: { x: 2, n: 2 } pow(2, 1) Опять вложенный вызов в строке 2, на этот раз — с аргументами x=2, n=1. Создаётся


новый текущий контекст, предыдущий добавляется в стек: • Контекст: { x: 2, n: 3 } • Контекст: { x: 2, n: 2 } • Контекст: { x: 2, n: 1 } Выход из pow(2, 1). При вызове pow(2, 1) вложенных вызовов нет. Функция для n==1 тут же заканчивает свою работу, возвращая 2. Текущий контекст больше не нужен и удаляется из памяти, из стека восстанавливается предыдущий: • Контекст: { x: 2, n: 3 } • Контекст: { x: 2, n: 2 } Возобновляется обработка внешнего вызова pow(2, 2). Выход из pow(2, 2).

…И теперь уже pow(2, 2) может закончить свою работу, вернув 4. Восстанавливается контекст предыдущего вызова: • Контекст: { x: 2, n: 3 } Возобновляется обработка внешнего вызова pow(2, 3). Выход из pow(2, 3). Самый внешний вызов заканчивает свою работу, его результат: pow(2, 3) = 8. Глубина рекурсии в данном случае составила: 3. Как видно из иллюстраций выше, глубина рекурсии равна максимальному числу контекстов, одновременно хранимых в стеке.

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

Обратим внимание на требования к памяти. Фактически, рекурсия приводит к хранению всех данных для внешних вызовов в стеке. То есть, возведение в степень n хранит в памяти n различных контекстов. Реализация степени через цикл гораздо более экономна:

1 function pow(x, n) { 2 var result = x; 3 for(var i=1; i<n; i++) { 4 result *= x; 5 } 6 return result;


7} У такой функции pow будет один контекст, в котором будут последовательно меняться значения i иresult. Любая рекурсия может быть переделана в цикл. Как правило, вариант с циклом будет эффективнее.

…Но зачем тогда нужна рекурсия? Да просто затем, что рекурсивный код может быть гораздо проще и понятнее! Переделка в цикл может быть нетривиальной, особенно когда в функции, в зависимости от условий, используются разные рекурсивные подвызовы. В программировании мы в первую очередь стремимся сделать сложное простым, а повышенная производительность нужна… Лишь там, где она действительно нужна. Поэтому красивое рекурсивное решение во многих случаях лучше. Недостатки и преимущества рекурсии: • Требования к памяти. • Ограничена максимальная глубина стека. • Краткость и простота кода. Задачи на рекурсию 5

Напишите функцию sumTo(n), которая для данного n вычисляет сумму чисел от 1 до n, например:

1 sumTo(1) = 2 sumTo(2) = 3 sumTo(3) = 4 sumTo(4) = 5 ... 6 sumTo(100)

1 2 + 1 = 3 3 + 2 + 1 = 6 4 + 3 + 2 + 1 = 10 = 100 + 99 + ... + 2 + 1 = 5050

Сделайте три варианта решения: 1. С использованием цикла.

2. Через рекурсию, т.к. sumTo(n) = n + sumTo(n-1) для n > 1. 3. С использованием формулы для суммы арифметической прогрессии. Пример работы вашей функции:

function sumTo(n) { /*... ваш код ... */ }


alert( sumTo(100) ); // 5050 Какой вариант решения самый быстрый? Самый медленный? Почему? Можно ли при помощи рекурсии посчитать sumTo(100000)? Если нет, то почему? Решение Решение с использованием цикла:

1 function sumTo(n) { 2 var sum = 0; 3 for(var i=1; i<=n; i++) { 4 sum += i; 5 } 6 return sum; 7} 8 9 alert( sumTo(100) ); Решение через рекурсию:

1 function sumTo(n) { 2 if (n == 1) return 1; 3 return n + sumTo(n-1); 4} 5 6 alert( sumTo(100) ); Решение по формуле: sumTo(n) = n*(n+1)/2:

1 function sumTo(n) { 2 return n*(n+1)/2; 3} 4 5 alert( sumTo(100) ); P.S. Надо ли говорить, что решение по формуле работает быстрее всех? Оно использует всего три операции для любого n, а цикл и рекурсия требуют как


минимум n операций сложения. Вариант с циклом — второй по скорости. Он быстрее рекурсии, так как операции по сути те же, но нет накладных расходов на вложенный вызов функции. Рекурсия в данном случае работает медленнее всех. P.P.S. Существует ограничение глубины вложенных вызовов, поэтому рекурсивный вызов sumTo(100000) выдаст ошибку. [Открыть задачу в новом окне] 4

Факториа́л числа — это число, умноженное на «себя минус один», затем на «себя минус два» и так далее, до единицы. Обозначается n! Определение факториала можно записать как:

n! = n*(n-1)*(n-2)*...*1 Примеры значений для разных n:

1 1! 2 2! 3 3! 4 4! 5 5!

= = = = =

1 2*1 = 2 3*2*1 = 6 4*3*2*1 = 24 5*4*3*2*1 = 120 Задача — написать функцию factorial(n), которая возвращает факториал числаn!, используя рекурсивный вызов.

alert( factorial(5) ); // 120

Подсказка: обратите внимание, что n! можно записать как n * (n-1)!, например3! = 3*2! = 3*2*1! = 6

Решение По свойствам факториала, как описано в условии, n! можно записать какn * (n-1)!.


То есть, результат функции для n можно получить как n, умноженное на результат функции для n-1, и так далее до 1!:

1 function factorial(n) { 2 return (n!=1) ? n*factorial(n-1) : 1; 3} 4 5 alert( factorial(5) ); // 120 Базисом рекурсии является значение 1. А можно было бы сделать базисом и 0. Тогда код станет чуть короче:

1 function factorial(n) { 2 return n ? n*factorial(n-1) : 1; 3} 4 5 alert( factorial(5) ); // 120 В этом случае вызов factorial(1) сведется к 1*factorial(0), будет дополнительный шаг рекурсии. [Открыть задачу в новом окне] 5 Последовательность чисел Фибоначчи имеет формулуFn = Fn-1 + Fn-2. То есть, следующее число получается как сумма двух предыдущих. Первые два числа равны 1, затем 2(1+1), затем 3(1+2), 5(2+3) и так далее:1, 1, 2, 3, 5, 8, 13, 21.... Числа Фибоначчи тесно связаны с золотым сечением и множеством природных явлений вокруг нас. Напишите функцию fib(n), которая возвращает n-е число Фибоначчи. Пример работы:

1 function fib(n) { /* ваш код */ } 2 3 alert( fib(3) ); // 2 4 alert( fib(7) ); // 13 5 alert( fib(77)); // 5527939700884757


Все запуски функций из примера выше должны срабатывать быстро. Решение Вычисление рекурсией (медленное) Решение по формуле, используя рекурсию:

1 function fib(n) { 2 return n <= 1 ? n : fib(n-1) + fib(n-2); 3} 4 5 alert( fib(3) ); // 2 6 alert( fib(7) ); // 13 7 // fib(77); // не запускаем, подвесит браузер При больших значениях n оно будет работать очень медленно. Например, fib(77)уже будет вычисляться очень долго. Это потому, что функция порождает обширное дерево вложенных вызовов. При этом ряд значений вычисляются много раз. Например, fib(n-2) будет вычислено как дляfib(n), так и для fib(n-1). Можно это оптимизировать, запоминая уже вычисленные значения.. А можно просто отказаться от рекурсии, а вместо этого пойти по формуле слева-направо в цикле, вычисляя 1е, 2е, 3е и так далее числа до нужного. Попробуйте. Не получится — откройте решение ниже. Последовательное вычисление (идея) Будем идти по формуле слева-направо:

1 var a = 1, b = 1; // начальные значения 2 var c = a + b; // 2 3 4 /* переменные на начальном шаге: 5a b c 6 1, 1, 2 7 */ Теперь следующий шаг, присвоим a и b текущие 2 числа и получим новое следующее в c:


1 a = b, b = c; 2 c = a + b; 3 4 /* стало так (еще число): 5 a b c 6 1, 1, 2, 3 7 */ Следующий шаг даст нам еще одно число последовательности:

1 a = b, b = c; 2 c = a + b; 3 4 /* стало так (еще число): 5 a b c 6 1, 1, 2, 3, 5 7 */ Повторять в цикле до тех пор, пока не получим нужное значение. Это гораздо быстрее, чем рекурсия, хотя бы потому что ни одно из чисел не вычисляется дважды. P.S. Этот подход к вычислению называется динамическое программирование снизу-вверх. Последовательное вычисление (код)

01 function fib(n) { 02 var a = 1, b = 1; 03 for (var i = 3; i 04 var c = a + b; 05 a = b; 06 b = c; 07 } 08 return b; 09 } 10 11 alert( fib(3) ); // 12 alert( fib(7) ); // 13 alert( fib(77)); //

<= n; i++) {

2 13 5527939700884757

Цикл здесь начинается с i=3, так как первое и второе числа Фибоначчи заранее записаны в переменные a=1, b=1.


[Открыть задачу в новом окне] Существуют много областей применения рекурсивных вызовов, в частности — работа со структурами данных, такими как дерево документа. Мы обязательно встретимся с ними. Методы и свойства

1. Пример: str.length, str.toUpperCase() 2. Пример: num.toFixed Все значения в JavaScript, за исключением null и undefined, содержат набор вспомогательных функций и значений, доступных «через точку». Такие функции называют «методами», а значения — «свойствами». Посмотрим на примеры. Пример: str.length, str.toUpperCase() У строки есть свойство length, содержащее длину:

1 alert( "Привет, мир!".length ); // 12 Еще у строк есть метод toUpperCase(), который возвращает строку в верхнем регистре:

1 var hello = "Привет, мир!"; 2 3 alert( hello.toUpperCase() ); // "ПРИВЕТ, МИР!" Если через точку вызывается функция (toUpperCase()) — это называют «вызов метода», если просто читаем значение (length) — «получение свойства». Пример: num.toFixed У чисел есть метод num.toFixed(n). Он округляет число num до n знаков после запятой, при необходимости добивает нулями до данной длины и возвращает в виде строки (удобно для форматированного вывода):

1 var n = 12.345; 2 3 alert( n.toFixed(2) ); // "12.35" 4 alert( n.toFixed(0) ); // "12" 5 alert( n.toFixed(5) ); // "12.34500" Детали работы toFixed разобраны в главе Числа.

Обращение к методам чисел


К методу числа можно обратиться и напрямую:

1 alert( 12.34.toFixed(1) ); // 12.3 ..Но если число целое, то будет проблема:

1 alert( 12.toFixed(1) ); // ошибка!

Ошибка произойдёт потому, что JavaScript ожидает десятичную дробь после точки.

Это — особенность синтаксиса JavaScript. Вот так — будет работать:

1

alert( 12..toFixed(1) ) ; // 12.0

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

1 var hello = "Привет"; 2 3 alert( hello.toUpperCase ); // function... Этот код выводит значение свойства toUpperCase, которое является встроенной в язык функцией. Как правило браузер выводит его как-то так:"function toUpperCase() { [native code] }".


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

1 var hello = "Привет"; 2

3

alert( hello.toUpperCase() ); // "ПРИВЕТ" (результат вызова)

Мы еще встретимся со строками и числами в последующих главах и глубже познакомимся со средствами для работы с ними.

Всё вместе: особенности JavaScript

1. Структура кода 2. Переменные и типы 3. Взаимодействие с посетителем 4. Особенности операторов 5. Логические операторы 6. Циклы 7. Конструкция switch 8. Функции 9. Методы и свойства 10.Итого В этой главе приводятся основные особенности JavaScript, на уровне базовых конструкций, типов, синтаксиса. Она будет особенно полезна, если ранее вы программировали на другом языке, ну или как повторение важных моментов раздела. Все очень компактно, со ссылками на развёрнутые описания. Структура кода Операторы разделяются точкой с запятой:

1 alert('Привет'); alert('Мир'); Как правило, перевод строки тоже подразумевает точку с запятой. Так тоже будет работать:

1 alert('Привет') 2 alert('Мир') ..Однако, иногда JavaScript не вставляет точку с запятой. Например:


1 var a = 2 2 +3 3 4 alert(a); // 5 Бывают случаи, когда это ведёт к ошибкам, которые достаточно трудно найти и исправить. Поэтому рекомендуется точки с запятой ставить. Сейчас это, фактически, стандарт. Поддерживаются однострочные комментарии // ... и многострочные /* ... */: Подробнее: Структура кода. Переменн ые и тип ы

• Объявляются директивой var. Могут хранить любое значение: var x = 5; x = "Петя"; • Есть 5 «примитивных» типов и объекты:

1 x = 1; x = "Тест"; 2 двойные 3 x = true; 4 x = null; 5 x = undefined;

// число // строка, кавычки могут быть одинарные или // булево значение true/false // спец. значение (само себе тип) // спец. значение (само себе тип)

Также есть специальные числовые значения Infinity (бесконечность) и NaN. Значение NaN обозначает ошибку и является результатом числовой операции, если она некорректна.

• Значение null не является «ссылкой на нулевой адрес/объект» или чем-то подобным. Это просто специальное значение. Оно присваивается, если мы хотим указать, что значение переменной неизвестно. Например:

var age = null; // возраст неизвестен • Значение undefined означает «переменная не присвоена». Например:

var x; alert( x ); // undefined Можно присвоить его и явным образом: x = undefined, но так делать не рекомендуется. Про объекты мы поговорим в главе Объекты как ассоциативные массивы, они в JavaScript


сильно отличаются от большинства других языков.

• В имени переменной могут быть использованы любые буквы или цифры, но цифра не может быть первой. Символы доллар $ и подчёркивание _ допускаются наравне с буквами. Подробнее: Переменные, Введение в типы данных. Взаимодействие с посетителем Простейшие функции для взаимодействия с посетителем в браузере: prompt(вопрос[, по_умолчанию]) Задать вопрос и возвратить введённую строку, либо null, если посетитель нажал «Отмена». confirm(вопрос) Задать вопрос и предложить кнопки «Ок», «Отмена». Возвращает, соответственно, true/false. alert(сообщение) Вывести сообщение на экран. Все эти функции являются модальными, т.е. не позволяют посетителю взаимодействовать со страницей до ответа. Например:

1 var userName = prompt("Введите имя?", "Василий"); 2 var smokes = confirm("Вы хотите чаю?"); 3 4 alert( "Посетитель: " + userName); 5 alert( "Чай: " + smokes); Подробнее: Взаимодействие с пользователем: alert, prompt, confirm. Особенности операторов

• Для сложения строк используется оператор +. Если хоть один аргумент — строка, то другой тоже приводится к строке:

1 alert( 1 + 2 ); // 3, число 2 alert( '1' + 2 ); // '12', строка 3 alert( 1 + '2' ); // '12', строка • Сравнение === проверяет точное равенство, включая одинаковый тип. Это самый очевидный и надёжный способ сравнения. Остальные сравнения == < <= > >= осуществляют числовое приведение типа:


1 alert( 0 == false ); // true 2 alert( true > 0 ); // true Исключение — сравнение двух строк (см. далее). Исключение: значения null и undefined ведут себя в сравнениях не как ноль.

• Они равны null == undefined друг другу и не равны ничему ещё. В частности, не равны нулю.

• В других сравнениях (кроме ===) значение null преобразуется к нулю, а undefined — становится NaN («ошибка»). Такое поведение может привести к неочевидным результатам, поэтому лучше всего использовать для сравнения с ними ===. Оператор == тоже можно, если не хотите отличатьnull от undefined. Например, забавное следствие этих правил для null:

1 alert( null > 0 ); // false, т.к. null преобразовано к 0 2 alert( null >= 0 ); // true, т.к. null преобразовано к 0 alert( null == 0 ); // false, в стандарте явно указано, что null 3 равен лишь undefined С точки зрения здравого смысла такое не возможно. Значение null не равно нулю и не больше, но при этом null >= 0 возвращает true!

• Сравнение строк — лексикографическое, символы сравниваются по своим unicode-кодам. Поэтому получается, что строчные буквы всегда больше, чем прописные:

1 alert('а' > 'Я'); // true Подробнее: Основные операторы, Операторы сравнения и логические значения. Логические оператор ы В JavaScript есть логические операторы: И (обозначается &&), ИЛИ (обозначается ||) и НЕ (обозначается !). Они интерпретируют любое значение как логическое. Не стоит путать их с побитовыми операторами И, ИЛИ, НЕ, которые тоже есть в JavaScript и работают с числами на уровне битов. Как и в большинстве других языков, в логических операторах используется «короткий цикл» вычислений. Например, вычисление выражения 1 && 0 && 2 остановится после первого И &&, т.к. понятно что результат будет ложным (ноль интерпретируется как false). Результатом логического оператора служит последнее значение в коротком цикле вычислений.


Можно сказать и по-другому: значения хоть и интерпретируются как логические, но то, которое в итоге определяет результат, возвращается без преобразования. Например:

1 alert( 0 && 1 ); // 0 2 alert( 1 && 2 && 3 ); // 3 3 alert( null || 1 || 2 ); // 1 Подробнее: Логические операторы. Цикл ы • Поддерживаются три вида циклов:

01 // 1 02 while (условие) { 03 ... 04 } 05 06 // 2 07 do { 08 ... 09 } while(условие); 10 11 // 3 12 for (var i = 0; i < 10; i++) { 13 ... 14 } • Переменную можно объявлять прямо в цикле, но видна она будет и за его пределами.

• Поддерживаются директивы break/continue для выхода из цикла/перехода на следующую итерацию. Для выхода одновременно из нескольких уровней цикла можно задать метку. Синтаксис: «имя_метки:», ставится она только перед циклами и блоками, например:

1 outer: 2 for(;;) { 3 ... 4 for(;;) { 5 ... 6 break outer; 7 } 8} Переход на метку возможен только изнутри цикла, и только на внешний блок по


отношению к данному циклу. В произвольное место программы перейти нельзя. Подробнее: Директивы break и continue. Конструкция switch При сравнениях в конструкции switch используется оператор ===. Например:

01 var age = prompt('Ваш возраст', 18); 02 03 switch (age) { 04 case 18: alert('Никогда не сработает'); // результат prompt - строка, а не 05 число 06 07 case "18": // вот так - сработает! 08 alert('Вам 18 лет!'); 09 break; 10 11 default: 12 alert('Любое значение, не совпавшее с case'); 13 } Подробнее: Конструкция switch. Функции Синтаксис функций в JavaScript:

1 // function имя(список параметров) { тело } 2 function sum(a, b) { 3 var result = a + b; 4 5 return result; 6} • sum — имя функции, ограничение на имя функции — те же, что и на имя переменной. • Переменные, объявленные через var, видны только внутри этой функции. • Параметры передаются «по значению», т.е. копируются в локальные переменные a, b, за исключением объектов, которые передаются «по ссылке», их мы подробно обсудим в главеОбъекты как ассоциативные массивы.

• Функция без return считается возвращающей undefined. Вызов return без значения также возвращает undefined:

1 function f() { }


2 alert( f() ); // undefined Подробнее: Функции. Метод ы и свойства Все значения в JavaScript, за исключением null и undefined, содержат набор вспомогательных функций и значений, доступных «через точку». Такие функции называют «методами», а значения — «свойствами». Например:

1 alert( "Привет, мир!".length ); // 12 Еще у строк есть метод toUpperCase(), который возвращает строку в верхнем регистре:

1 var hello = "Привет, мир!"; 2 3 alert( hello.toUpperCase() ); // "ПРИВЕТ, МИР!" Подробнее: Методы и свойства. Итого В этой главе мы повторили основные особенности JavaScript, знание которых необходимо для обхода большинства «граблей», да и просто для написания хорошего кода. Строгий режим, "use strict"

1. "use strict" 2. На уровне функции 3. Использовать ли use strict? Современная спецификация языка содержит ряд несовместимых изменений, по сравнению со старым стандартом. Чтобы не ломать существующий код, они, в основном, включаются при наличии специальной директивы use strict. Эта директива не поддерживается IE9-. "use strict" Директива выглядит как строка "use strict"; или 'use strict';, и может стоять в начале скрипта, либо в начале функции, например:

1 "use strict"; 2 3 // этот код будет работать по стандарту ES5 4 ... Например, присвоение переменной без объявления в старом стандарте было допустимо, а в


современном — нет. Поэтому следующий код выдаст ошибку:

1 "use strict"; 2 3 x = 5; // error: x is not defined Директиву нужно указывать до кода, иначе она не сработает:

1 var a; 2 3 "use strict"; 4 5 x = 5; // ошибки не будет, строгий режим не включен На уровне функции Допустим, код работает только в старом режиме, но мы хотим написать новые функции, используя современный стандарт. Директиву "use strict"; можно указать в начале функции, тогда она будет действовать только в ней. Например, в коде ниже используются переменные без объявления. Но ошибка будет выведена только при запуске функции, так как именно она работает в строгом режиме:

01 function sayHi(person) { 02 "use strict"; 03 04 message = "Привет, " + person; // error: message is not defined 05 //... 06 } 07 08 person = "Вася"; 09 10 sayHi(person); Отменить действие "use strict" никак нельзя. Если директива указана на уровне скрипта, то действует и на все функции. Использовать ли use strict? Строгий режим даёт две вещи:


1. Там, где в старом стандарте был «кривой код», в новом будет ошибка. 2. Некоторые возможности языка работают по-другому. Более корректно, но по-другому. По ходу учебника мы увидим много конкретных примеров этих различий. Всё это хорошо. Но основная проблема при использовании этой директивы — поддержка браузеров IE9-, которые игнорируют use strict и работают только в старом стандарте. Действительно, предположим, что мы, используя эту директиву, разработали код в Chrome. Всё работает… Однако, вероятность ошибок при этом в IE9- выросла! Так как там поддерживается только старый стандарт, возможны ошибки совместимости. А отлаживать код, чтобы найти эти ошибки, в IE9- намного менее приятно, чем в Chrome. Тем не менее, строгий режим — это наше будущее. Поэтому его лучше использовать. Но при этом нужно очень хорошо знать отличия в работе JavaScript между старым и новым режимом, чтобы код, который вы напишете, не «ломался» при запуске в режиме старого стандарта. Стиль кода

1. Синтаксис 1. Фигурные скобки 2. Длина строки 3. Отступы 4. Точка с запятой 2. Именование 3. Уровни вложенности 4. Функции = Комментарии 5. Функции — под кодом 6. Комментарии 7. Руководства по стилю 1. Автоматизированные средства проверки 8. Итого Код должен быть максимально читаемым и понятным. Для этого нужен хороший стиль написания кода. В этой главе мы рассмотрим компоненты такого стиля. Синтаксис Шпаргалка с правилами синтаксиса:


Пробелов может быть и больше. Главное, не меньше Разберём основные моменты. Фигурные скобки Пишутся на той же строке, так называемый «египетский» стиль. Перед скобкой — пробел.


Если у вас уже есть опыт в разработке и вы привыкли делать скобку на отдельной строке — это тоже вариант. В конце концов, решать вам. Но в основных JavaScript-фреймворках (jQuery, Dojo, Google Closure Library, Mootools, Ext.JS, YUI…) стиль именно такой. Если условие и код достаточно короткие, например if (cond) return null;, то запись в одну строку вполне читаема… Но, как правило, отдельная строка всё равно воспринимается лучше. Длина строки Максимальную длину строки согласовывают в команде. Как правило, это либо 80, либо 120 символов, в зависимости от того, какие мониторы у разработчиков. Более длинные строки необходимо разбивать. Если этого не сделать, то перевод очень длинной строки сделает редактор, и это может быть менее красиво и читаемо. Отступы Отступы нужны двух типов:

• Горизонтальный, при вложенности — два(или четыре) пробела. Как правило, используются именно пробелы, т.к. они позволяют сделать более гибкие «конфигурации отступов», чем символ «Tab». Например:

1 function removeClass(obj, cls) { 2 var targetObj = obj, targetClass = cls, // <-- пробельный отступ 3 выравнен! 4 className = obj.className; // <-5 ... 6} Переменные здесь объявлены по вертикали, т.к. вообще человеческий глаз лучше


воспринимает («сканирует») вертикально выравненную информацию, нежели по горизонтали. Это известный факт в мире дизайнеров.

• Вертикальный, для лучшей разбивки кода — перевод строки. Используется, чтобы разделить логические блоки внутри одной функции. В примере ниже разделены функция pow, получение данных x,n и их обработка if.

01 function pow(..) { 02 .. 03 } 04 // <-05 x = ... 06 n = ... 07 // <-08 if (n <= 1) { 09 ... 10 } 11 .. Вставляйте дополнительный перевод строки туда, где это сделает код более читаемым. Не должно быть более 9 строк кода подряд без вертикального отступа. Точка с запятой Точка с запятой ставится везде, включая динамическое объявление функции:

var func = function(a) { alert(a); }; // <-Есть языки, в которых точка с запятой не обязательна, и её там никто не ставит. В JavaScript она тоже не обязательна, но ставить нужно. В чём же разница? Она в том, что в JavaScript без точки с запятой возможны трудноуловимые ошибки. Такая вот особенность синтаксиса. Поэтому рекомендуется её всегда ставить. Именование Общее правило: • Имя переменной — существительное. • Имя функции — глагол, или начинается с глагола. Бывает, что имена для краткости делают существительными, но глаголы понятнее. Для имён используется английский язык (не транслит) и верблюжья нотация. Более подробно — читайте про имена функций и имена переменных.


Уровни вложенности Уровней вложенности должно быть немного. Например, проверки в циклах лучше делать через «continue», чтобы не было дополнительного уровняif(..) { ... }: Вместо:

1 for (var i=0; i<10; i++) { 2 if (i подходит) { 3 ... // <- уровень вложенности 2 4 } 5} Используйте:

1 for (var i=0; i<10; i++) { 2 if (i не подходит) continue; 3 ... // <- уровень вложенности 1 4} Аналогичная ситуация — с if/else и return. Следующие две конструкции идентичны. Первая:

1 function isEven(n) { // проверка чётности 2 if (n % 2 == 0) { 3 return true; 4 } else { 5 return false; 6 } 7} Вторая:

1 function isEven(n) { // проверка чётности 2 if (n % 2 == 0) { 3 return true; 4 } 5 6 return false; 7} Если в блоке if идёт return, то else за ним не нужен. Лучше быстро обработать простые случаи, вернуть результат, а дальше разбираться со сложным, без дополнительного уровня вложенности.


В случае с функцией isEven можно было бы поступить и проще:

function isEven(n) { // проверка чётности return n % 2 == 0; } ..Казалось бы, можно пойти дальше, есть ещё более короткий вариант:

function isEven(n) { // проверка чётности return !(n % 2); } …Однако, код !(n % 2) менее очевиден чем n % 2 == 0. Поэтому, на самом деле, последний вариант хуже. Главное для нас — не краткость кода, а его простота и читаемость. На эту тему иногда говорят, что «хороший программист — это тот, кто может делать сложные вещи простым кодом». Функции = Комментарии Функции должны быть небольшими. Если функция большая — желательно разбить её на несколько. Этому правилу бывает сложно следовать, но оно стоит того. При чем же здесь комментарии? Вызов отдельной небольшой функции не только легче отлаживать и тестировать — сам факт его наличия является отличным комментарием. Сравните, например, две функции showPrimes(n) для вывода простых чисел до n. Первый вариант:

01 function showPrimes(n) { 02 nextPrime: 03 for (var i=2; i<n; i++) { 04 05 for (var j=2; j<i; j++) { 06 if ( i % j == 0) continue nextPrime; 07 } 08 09 alert(i); // простое 10 } 11 } Второй вариант, вынесена подфункция isPrime(n) для проверки на простоту:

01 function showPrimes(n) { 02 03 for (var i=2; i<n; i++) {


04 if (!isPrime(i)) continue; 05 06 alert(i); // простое 07 } 08 } 09 10 function isPrime(n) { 11 for (var i=2; i<n; i++) { 12 if ( n % i == 0) return false; 13 } 14 return true; 15 } Второй вариант проще и понятнее, не правда ли? Вместо участка кода мы видим описание действия, которое там совершается (проверка isPrime). Функции — под кодом Есть два способа расположить функции, необходимые для выполнения кода. 1. Функции над кодом, который их использует:

01 // объявить функции 02 function createElement() { 03 ... 04 } 05 06 function setHandler(elem) { 07 ... 08 } 09 10 function walkAround() { 11 ... 12 } 13 14 // код, использующий функции 15 var elem = createElement(); 16 setHandler(elem); 17 walkAround(); 2. Сначала код, а функции внизу:

01 // код, использующий функции 02 var elem = createElement(); 03 setHandler(elem); 04 walkAround(); 05


06 // --- функции --07 08 function createElement() { 09 ... 10 } 11 12 function setHandler(elem) { 13 ... 14 } 15 16 function walkAround() { 17 ... 18 } …На самом деле существует еще третий «стиль», при котором функции хаотично разбросаны по коду

, но это ведь не наш метод, да?

Как правило, лучше располагать функции под кодом, который их использует. То есть, это 2й способ. Дело в том, что при чтении такого кода мы хотим знать в первую очередь, что он делает, а уже затемкакие функции ему помогают. Если первым идёт код, то это как раз дает необходимую информацию. Что же касается функций, то вполне возможно нам и не понадобится их читать, особенно если они названы адекватно и то, что они делают, понятно. У первого способа, впрочем, есть то преимущество, что на момент чтения мы уже знаем, какие функции существуют. Таким образом, если над названиями функций никто не думает — может быть, это будет лучшим выбором

. Попробуйте оба варианта, но по практике автора предпочтителен всё же

второй. Комментарии В коде нужны комментарии.

1. Справочный комментарий перед функцией — о том, что именно она делает, какие параметры принимает и что возвращает. Для таких комментариев существует синтаксис JSDoc.

01 /** 02 * Возвращает x в степени n, только для натуральных n 03 * 04 * @param {number} x Число для возведения в степень.


05 * @param {number} n Показатель степени, натуральное число. 06 * @return {number} x в степени n. 07 */ 08 function pow(x, n) { 09 ... 10 } Такие комментарии обрабатываются многими редакторами, например Aptana и редакторами отJetBrains. Они учитывают их при автодополнении.

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

3. Есть несколько способов решения задачи. Почему выбран именно этот? Как правило, из кода можно понять, что он делает. Бывает, конечно, всякое, но, в конце концов, вы этот код видите. Гораздо важнее может быть то, чего вы не видите! Почему это сделано именно так? На это сам код ответа не даёт. Например, пробовали решить задачу по-другому, но не получилось — напишите об этом. Почему вы выбрали именно этот способ решения? Особенно это важно в тех случаях, когда используется не первый приходящий в голову способ, а какой-то другой. Без этого возможна, например, такая ситуация: • Вы открываете код, который был написан какое-то время назад, и видите, что он «неоптимален». • Думаете: «Какой я был дурак», и переписываете под «более очевидный и правильный» вариант.

• …Порыв, конечно, хороший, да только этот вариант вы уже обдумали раньше. И отказались, а почему — забыли. В процессе переписывания вспомнили, конечно (к счастью), но результат - потеря времени на повторное обдумывание. Комментарии, которые объясняют поведение кода, очень важны. Они помогают понять происходящее и принять правильное решение о развитии кода.

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

5. Архитектурный комментарий — «как оно, вообще, устроено». Применённые технологии, поток взаимодействия. Для этого, в частности, используется UML, но можно и без него. Главное — чтобы понятно. Что интересно, в коде начинающих разработчиков большинство комментариев обычно типа 2. Но на самом деле именно эти комментарии являются самыми ненужными. А всё дело в том, что людям лень придумывать правильные имена и структурировать функции. Ну ничего. Жизнь


заставит :/ Руководства по стилю Когда написанием проекта занимается целая команда, то должен существовать один стандарт кода, описывающий где и когда ставить пробелы, запятые, переносы строк и т.п. Сейчас, когда есть столько готовых проектов, нет смысла придумывать целиком своё руководство. Можно взять уже готовое, и которому, по желанию, всегда можно что-то добавить. Большинство есть на английском, сообщите мне, если найдёте хороший перевод:

• • • •

Google JavaScript Style Guide JQuery Core Style Guidelines Idiomatic.JS (есть перевод) Dojo Style Guide

Для того, чтобы начать разработку, вполне хватит элементов стилей, обозначенных в этой главе. В дальнейшем, посмотрите на эти руководства, найдите «свой» стиль Автоматизированные средства проверки Существуют онлайн-сервисы, проверяющие стиль кода. Самые известные — это:

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

• JSHint — ещё один вариант JSLint, ослабляющий требования в ряде мест. • Closure Linter — проверка на соответствие Google JavaScript Style Guide. Все они также доступны в виде программ, которые можно скачать. Итого Описанные принципы оформления кода уместны в большинстве проектов. Есть и другие полезные соглашения. Следуя (или не следуя) им, необходимо помнить, что любые советы по стилю хороши лишь тогда, когда делают код читаемее, понятнее, проще в поддержке. 4

Какие недостатки вы видите в стиле этого примера?

01 function pow(x,n) 02 { 03 var result=x;


04 for(var i=1;i<n;i++) {result*=x;} 05 return result; 06 } 07 08 x=prompt("x?",'') 09 n=prompt("n?",'') 10 if (n<=0) 11 { alert('Степень '+n+'не поддерживается, введите целую степень, 12 большую 0'); 13 } 14 else 15 { 16 alert(pow(x,n)) 17 } Решение Вы могли заметить следующие недостатки:

1. Отсутствуют пробелы — между параметрами, вокруг операторов, при вложенном вызове alert(pow(...)).

2. Переменные x и n присвоены без var. 3. Фигурные скобки расположены на отдельной строке.

4. Логически разные фрагменты кода: ввод данных prompt и их обработка if не разделены вертикальным пробелом.

5. Строка с alert слишком длинная, лучше разбить её на две. 6. Не везде есть точки с запятой. [Открыть задачу в новом окне] 4

Какие недостатки вы видите в стиле этого примера?

01 function pow(x, n) { 02 if (n < 1 || n != Math.round(n)) { 03 return NaN; // проверка: n должно быть целым, 1 или больше 04 } 05 06 return result(x, n); 07 08 // ---09 10 function result(x, n) {


11 return (n == 1) ? x : x*result(x, n-1); 12 } 13 14 } Решение Основная ошибка — неверный выбор имени функции result. 1. Во-первых, это существительное, а значит функцией быть не может (разве что какая-то особая договорённость о наименованиях).

2. Во-вторых, переменная result традиционно используется для хранения «текущего результата функции». Здесь эта традиция нарушена. Как назвать функцию правильно? Один из вариантов — префикс do, т.е. doPow, он означает что эта функция как раз и делает реальную работу. Также не лучший вариант — проверка с комментарием. Обычно код становится более читабельным, если выносить неочевидные действия в новую функцию. В этом случае имя этой функции послужит комментарием. P.S. Рекурсия при возведении в степень — не лучший выбор, обычный цикл будет быстрее, но это скорее недочёт логики, а не ошибка стиля. Исправленный код:

01 function pow(x, n) { 02 if (!isNatural(n)) { 03 return NaN; 04 } 05 06 return doPow(x, n); 07 08 // ---09 function isNatural(n) { 10 return n >= 1 && n == Math.round(n); 11 } 12 13 function doPow(x, n) { 14 return (n == 1) ? x : x*doPow(x, n-1); 15 } 16 17 } [Открыть задачу в новом окне]


Как писать неподдерживаемый код?

1. Соглашения 2. Краткость — сестра таланта! 3. Именование 1. Однобуквенные переменные 2. Не используйте i для цикла 3. Русские слова и сокращения 4. Будьте абстрактны при выборе имени 5. Похожие имена 6. А.К.Р.О.Н.И.М 7. Хитрые синонимы 8. Словарь терминов — это еда! 9. Повторно используйте имена 10.Добавляйте подчеркивания 11.Покажите вашу любовь к разработке 12.Перекрывайте внешние переменные 4. Мощные функции! 5. Внимание.. Сюр-при-из! 6. Заключение Предлагаю вашему вниманию советы мастеров древности, следование которым создаст дополнительные рабочие места для JavaScript-разработчиков. Код, который вы напишете, будет так сложен в поддержке, что у JavaScript’еров, которые придут после вас, даже простейшее изменение займет годы оплачиваемого труда! Более того, внимательно следуя этим правилам, вы сохраните и своё рабочее место, так как все будут бояться вашего кода и бежать от него…

…Впрочем, всему своя мера. Код не должен выглядеть сложным в поддержке — подобное напишет любой дурак. Код должен быть таковым. Иначе это заметят, и код будет переписан с нуля. Вы не можете такого допустить. Эти советы учитывают такую возможность. Да здравствует дзен. Статья представляет собой вольный перевод How To Write Unmaintainable Code с дополнениями для JavaScript. Соглашения Рабочий-чистильщик: "...Вот только жук у вас необычный... И чтобы с ним справиться, я должен жить как жук, стать жуком, думать как жук." (грызёт стол Симпсонов) Сериал "Симпсоны", серия Helter Shelter Чтобы помешать другому программисту исправить ваш код, вы должны понять путь его мыслей. Представьте, перед ним — ваш большой скрипт. И ему нужно поправить его. У него нет ни времени ни желания, чтобы читать его целиком, а тем более — досконально разбирать. Он хотел бы по-быстрому найти нужное место, сделать изменение и убраться восвояси без появления побочных эффектов. Он рассматривает ваш код как бы через трубочку из туалетной бумаги. Это не даёт ему общей картины, он ищет тот небольшой фрагмент, который ему необходимо изменить. По крайней мере, он надеется, что этот фрагмент будет небольшим.


На что он попытается опереться в этом поиске — так это на соглашения, принятые в программировании, об именах переменных, названиях функций и методов… Как затруднить задачу? Можно везде нарушать соглашения — это помешает ему, но такое могут заметить, и код будет переписан. Как поступил бы ниндзя на вашем месте?

…Правильно! Следуйте соглашениям «в общем», но иногда — нарушайте их. Тщательно разбросанные по коду нарушения соглашений с одной стороны не делают код явно плохим при первом взгляде, а с другой — имеют в точности тот же, и даже лучший эффект, чем явное неследование им! Например, в jQuery есть метод wrap, который обёртывает один элемент вокруг другого:

1 var img = $('<img/>'); 2 var div = $('<div/>'); 3 4 div.wrap(img); // обернуть img в div Результат кода выше:

<div> <img/> </div> (div обернулся вокруг img) А теперь, когда все расслабились и насладились этим замечательным методом…

…Самое время ниндзя нанести свой удар! Как вы думаете, что будет, если добавить к коду выше строку:

5 div.append('<span/>'); Возможно, вы полагаете, что <span/> добавится в конец div, сразу после img? А вот и нет! А вот и нет!.. Такое трудно себе представить, но вызов div.wrap(img) внутри клонирует div. И оборачивает вокругimg не сам div, а его злой клон. При этом исходный div остаётся пустым. После применения к нему append получается два div'а: один обёрнут вокруг span, а в другом — только img.

Переменная div <div> <span/> </div>

Клон div, созданный wrap (не присвоен никакой переменной) <div> <img/> </div>

Соглашение в данном случае — в том, что большинство методов jQuery не клонируют элементы.


А вызов wrap — клонирует. Код его истинный ниндзя писал! Краткость — сестра таланта! Пишите «как короче», а не как понятнее. «Меньше букв» — уважительная причина для нарушения любых соглашений. Ваш верный помощник — возможности языка, использованные неочевидным образом. Обратите внимание на оператор вопросительный знак '?', например:

// код из jQuery i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; Разработчик, встретивший эту строку и попытавшийся понять, чему же всё-таки равно i, скорее всего придёт к вам за разъяснениями. Смело скажите ему, что короче — это всегда лучше. Посвятите и его в пути ниндзя. Не забудьте вручить Дао дэ цзин. Именование Существенную часть науки о создании неподдерживаемого кода занимает искусство выбора имён. Однобуквенные переменные Называйте переменные коротко: a, b или c. В этом случае никто не сможет найти её, используя фунцию «Поиск» текстового редактора. Более того, даже найдя — никто не сможет «расшифровать» её и догадаться, что она означает. Не используйте i для цикла В тех местах, где однобуквенные переменные общеприняты, например, в счетчике цикла — ни в коем случае не используйте стандартные названия i, j, k. Где угодно, только не здесь! Остановите свой взыскательный взгляд на чём-нибудь более экзотическом. Например, x или y. Эффективность этого подхода особенно заметна, если тело цикла занимает одну-две страницы. В этом случае заметить, что переменная — счетчик цикла, без пролистывания вверх, невозможно. Русские слова и сокращения Если вам приходится использовать длинные, понятные имена переменных — что поделать.. Но и здесь есть простор для творчества! Назовите переменные «калькой» с русского языка или как-то «улучшите» английское слово.


В одном месте напишите var ssilka, в другом var ssylka, в третьем var link, в четвёртом — var lnk… Это действительно великолепно работает и очень креативно! Количество ошибок при поддержке такого кода увеличивается во много раз. Будьте абстрактны при выборе имени Лучший кувшин лепят всю жизнь. Высокая музыка неподвластна слуху. Великий образ не имеет формы. Лао-цзы При выборе имени старайтесь применить максимально абстрактное слово, например obj, data,value, item, elem и т.п.

• Идеальное имя для переменной: data. Используйте это имя везде, где можно. В конце концов, каждая переменная содержит данные, не правда ли? Но что делать, если имя data уже занято? Попробуйте value, оно не менее универсально. Ведь каждая переменная содержит значение. Занято и это? Есть и другой вариант.

• Называйте переменную по типу: obj, num, arr… Насколько это усложнит разработку? Как ни странно, намного! Казалось бы, название переменной содержит информацию, говорит о том, что в переменной — число, объект или массив… С другой стороны, когда непосвящённый будет разбирать этот код — он с удивлением обнаружит, что информации нет! Ведь как раз тип легко понять, запустив отладчик и посмотрев, что внутри. Но в чём смысл этой переменной? Что за массив/объект/число в ней хранится? Без долгой медитации над кодом тут не обойтись!

• Что делать, если и эти имена кончились? Просто добавьте цифру:item1, item2, elem5, data1.. Похожие имена Только истинно внимательный программист достоин понять ваш код. Но как проверить, достоин ли читающий? Один из способов — использовать похожие имена переменных, например data и date. Бегло прочитать такой код почти невозможно. А уж заметить опечатку и поправить её… Ммммм… Мы здесь надолго, время попить чайку. А.К.Р.О.Н.И.М Используйте сокращения, чтобы сделать код короче. Например ie (Inner Element), mc (Money Counter) и другие. Если вы обнаружите, что путаетесь в них сами — героически страдайте, но не переписывайте код. Вы знали, на что шли.


Хитрые синонимы Очень трудно найти чёрную кошку в тёмной комнате, особенно когда её там нет. Конфуций Чтобы было не скучно — используйте похожие названия для обозначения одинаковых действий. Например, если метод показывает что-то на экране — начните его название с display.. (скажем,displayElement), а в другом месте объявите аналогичный метод как show.. (showFrame). Как бы намекните этим, что существует тонкое различие между способами показа в этих методах, хотя на самом деле его нет. По возможности, договоритесь с членами своей команды. Если Вася в своих классах используетdisplay.., то Валера — обязательно render.., а Петя — paint...

…И напротив, если есть две функции с важными отличиями — используйте одно и то же слово для их описания! Например, с print... можно начать метод печати на принтере printPage, а также — метод добавления текста на страницу printText. А теперь, пусть читающий код думает: «Куда же выводит сообщение printMessage?». Особый шик — добавить элемент неожиданности. Пусть printMessage выводит не туда, куда все, а в новое окно. Словарь терминов — это еда! Ни в коем случае не поддавайтесь требованиям написать словарь терминов для проекта. Если же он уже есть — не следуйте ему, а лучше проглотите и скажите, что так и былО! Пусть читающий ваш код программист напрасно ищет различия в helloUser и welcomeVisitor и пытается понять, когда что использовать. Вы-то знаете, что на самом деле различий нет, но искать их можно о-очень долго. Для обозначения посетителя в одном месте используйте user, а в другом visitor, в третьем — просто u. Выбирайте одно имя или другое, в зависимости от функции и настроения. Это воплотит сразу два ключевых принципа ниндзя-дизайна — сокрытие информации и подмена понятий! Повторно используйте имена По возможности, повторно используйте имена переменных, функций и свойств. Просто записывайте в них новые значения. Добавляйте новое имя только если это абсолютно необходимо. В функции старайтесь обойтись только теми переменными, которые были переданы как параметры.


Это не только затруднит идентификацию того, что сейчас находится в переменной, но и сделает почти невозможным поиск места, в котором конкретное значение было присвоено. Цель — максимально усложнить отладку и заставить читающего код программиста построчно анализировать код и конспектировать изменения переменных для каждой ветки исполнения. Продвинутый вариант этого подхода — незаметно (!) подменить переменную на нечто похожее, например:

1 function ninjaFunction(elem) { 2 // 20 строк кода, работающего с elem 3 4 elem = elem.cloneNode(true); 5 6 // еще 20 строк кода, работающего с elem 7} Программист, пожелавший добавить действия с elem во вторую часть функции, будет удивлён. Лишь во время отладки, посмотрев весь код, он с удивлением обнаружит, что оказывается имел дело с клоном! Регулярные встречи с этим приемом на практике говорят: защититься невозможно. Эффективно даже против опытного ниндзи. Добавляйте подчеркивания Добавляйте подчеркивания _ и __ к именам переменных. Желательно, чтобы их смысл был известен только вам, а лучше — вообще без явной причины. Этим вы достигните двух целей. Во-первых, код станет длиннее и менее читаемым, а во-вторых, другой программист будет долго искать смысл в подчёркиваниях. Особенно хорошо сработает и внесет сумятицу в его мысли, если в некоторых частях проекта подчеркивания будут, а в некоторых — нет. В процессе развития кода вы, скорее всего, будете путаться и смешивать стили: добавлять имена с подчеркиваниями там, где обычно подчеркиваний нет, и наоборот. Это нормально и полностью соответствует третьей цели — увеличить количество ошибок при внесении исправлений. Покажите вашу любовь к разработке Пусть все видят, какими замечательными сущностями вы оперируете! Имена superElement,megaFrame и niceItem при благоприятном положении звёзд могут привести к просветлению читающего. Действительно, с одной стороны, кое-что написано: super.., mega.., nice.. С другой — это не несёт никакой конкретики. Читающий может решить поискать в этом глубинный смысл и замедитировать на часок-другой оплаченного рабочего времени.


Перекрывайте внешние переменные Находясь на свету, нельзя ничего увидеть в темноте. Пребывая же в темноте, увидишь все, что находится на свету. Гуань Инь-цзы Почему бы не использовать одинаковые переменные внутри и снаружи функции? Это просто и не требует придумывать новых имён.

1 var user = authenticateUser(); 2 3 function render() { 4 var user = ... 5 ... 6 ... 7 ... // <-- программист захочет внести исправления сюда, и.. 8 ... 9} Зашедший в середину метода render программист, скорее всего, не заметит, что переменная user«уже не та» и использует её… Ловушка захлопнулась! Здравствуй, отладчик. Мощн ые функции! Не ограничивайте действия функции тем, что написано в её названии. Будьте шире. Например, функция validateEmail(email) может, кроме проверки e-mail на правильность, выводить сообщение об ошибке и просить заново ввести e-mail. Выберите хотя бы пару дополнительных действий, кроме основного назначения функции.Главное — они должны быть неочевидны из названия функции. Истинный ниндзя-девелопер сделает так, что они будут неочевидны и из кода тоже. Объединение нескольких смежных действий в одну функцию защитит ваш код от повторного использования. Представьте, что другому разработчику нужно только проверить адрес, а сообщение — не выводить. Ваша функция validateEmail(email), которая делает и то и другое, ему не подойдёт. Работодатель будет вынужден оплатить создание новой. Внимание.. Сюр- при - из ! Есть функции, название которых говорит о том, что они ничего не меняют. Например, isReady,checkPermission, findTags.. В трактатах это называется «отсутствие сторонних эффектов». По-настоящему красивый приём — делать в таких функциях что-нибудь полезное, заодно с процессом проверки. Что именно — совершенно неважно.


Удивление и ошеломление, которое возникнет у вашего коллеги, когда он увидит, что функция с названием на is.., check.. или find... что-то меняет — несомненно, расширит его границы разумного! Ещё одна вариация такого подхода — возвращать нестандартное значение. Ведь общеизвестно, что is.. и check.. обычно возвращают true/false. Продемонстрируйте оригинальное мышление. Пусть вызов checkPermission возвращает не результат true/false, а объект с результатами проверки! А что, полезно. Те же, кто попытается написать проверку if (checkPermission(..)), будут удивлены результатом. Ответьте им: «надо читать документацию!». И перешлите эту статью. Заключение Все советы выше пришли из реального кода… И в том числе от разработчиков с большим опытом. Возможно, даже больше вашего, так что не судите опрометчиво • Следуйте нескольким из них — и ваш код станет полон сюрпризов. • Следуйте многим — и ваш код станет истинно вашим, никто не захочет изменять его. • Следуйте всем — и ваш код станет ценным уроком для молодых разработчиков, ищущих просветления. Отладка в браузере Chrome

1. 2. 3. 4. 5. 6. 7.

Общий вид панели Sources Точки остановки Остановиться и осмотреться Управление выполнением Консоль Ошибки Итого

Перед тем, как двигаться дальше, поговорим об отладке скриптов. Все современные браузеры поддерживают для этого «инструменты разработчика». Исправление ошибок с их помощью намного проще и быстрее. На текущий момент самые многофункциональные инструменты — в браузере Chrome. Также очень хорош Firebug (для Firefox). Общий вид панели Sources Зайдите на страницу tutorial/debugging/pow/index.html браузером Chrome. Откройте инструменты разработчика: F12 или в меню Инструменты > Инструменты Разработчика. Выберите сверху Sources:


Кнопки, которые мы будем использовать: 1. Раскрывает список скриптов и стилей, подключённых к странице. 2. Включает-выключает консоль (ESC). 3. Двойное нажатие на эту кнопку заставляет отладчик останавливаться на ошибках JavaScript (кнопка поменяет цвет на фиолетовый). 4. Код можно отлаживать в отформатированном виде, если включить эту кнопку. 5. Панель управления потоком исполнения в режиме отладки. Нам она скоро понадобится. Точки остановки Зашли на страницу? Раскройте список скриптов, нажав на кнопку 1, и выберите pow.js. Кликните на 6й строке, прямо на цифре 6. Поздравляю! Вы поставили свой первый «брейкпойнт». Слово Брейкпойнт (breakpoint) — часто используемый английский жаргонизм. В русскоязычных пособиях используется термин «точка остановки». Это то место в коде, где отладчик будет останавливать выполнение JavaScript, как только оно до него дойдёт. В остановленном коде можно посмотреть любые переменные, выполнить команды и т.п.


Справа-снизу находится вкладка Breakpoints, в ней можно: • Просматривать точки остановки. • Выключать их кликом на чекбокс. • При клике на точку остановки происходит переход на соответствующее место кода (удобно, когда их много).

Дополнительные возможности

• Остановку можно инициировать и напрямую из кода скрипта, командойdebugger:

1 function pow(x, n) { 2 ... 3 debugger; // <-- отладчик остановится тут 4 ... 5} • Правый клик на цифру строки позволяет создать условную точку остановки (conditional breakpoint), т.е. задать условие, при котором точка остановки сработает. Это удобно, если остановка нужна только при определённом значении переменной или параметра функции. Остановиться и осмотреться Наша функция выполняется сразу при загрузке страницы, так что самый простой способ активировать JavaScript — перезагрузить её. Итак, нажимаем F5. Если вы сделали всё, как описано выше, то выполнение прервётся как раз на 6й строке.


Обратите внимание на информационные вкладки справа (отмечены стрелками). В них мы можем посмотреть текущее состояние:

1. Watch Expressions — показывает текущие значения любых выражений. Можно раскрыть эту вкладку, нажать мышью + на ней и ввести любое выражение или имя переменной. Отладчик будет отображать его значение.

2. Call Stack — вложенные вызовы (иначе называют «стек вызовов»). На текущий момент видно, отладчик находится в функции pow (pow.js, строка 6), вызванной из анонимного кода (index.html, строка 13).

3. Scope Variables — переменные. На текущий момент строка 6 ещё не выполнилась, поэтому result равен undefined. В Local показываются переменные функции: объявленные через var и параметры. Вы также можете там видеть переменную this, она создаётся автоматически. Если вы не знаете, что она означает — ничего страшного, мы это обсудим в главе Контекст this в деталях. В Global — глобальные переменные, которые находятся вне функций. Управление в ыполнением Пришло время погонять скрипт и «оттрейсить» (от англ. trace, отслеживать) его работу. Обратим внимание на панель управления в ней есть 4 основные кнопки:


— продолжить выполнение (F8). Если скрипт не встретит новых точек остановки, то на этом работа в отладчике закончится. Нажмите на эту кнопку. Вы увидите, что отладчик остался на той же строке, но в Call Stack появился новый вызов. Это произошло потому, что в 6й строке находится рекурсивный вызов функции pow, т.е. управление перешло в неё опять, но с другими аргументами. Походите по стеку вверх-вниз — вы увидите, что действительно аргументы разные.

— сделать шаг, не заходя внутрь функции (F10). Выполняет одну строку скрипта. Если в ней есть вызов функции — то отладчик обходит его стороной, т.е. не переходит на код внутри. Нажмите на эту кнопку. Отладчик перейдёт на строку 7. Всё правильно, хотя есть и тонкость. Дело в том, что во вложенном вызове pow есть брейкпойнт, а на включённых брейкпойнтах отладчик останавливается всегда. Даже если вложенный вызов и нажата эта кнопка.

…Но в данном случае вложенный вызов будет с n=1, поэтому сработает if и до строки 6 управление не дойдёт. Поэтому и остановки нет.

— сделать шаг (F11). Переходит на следующую строку кода. Если есть вложенный вызов, то внутрь функции.

— выполнять до выхода из текущей функции (Shift + F11). Как только текущая функция завершилась, отладчик тут же останавливает скрипт. Удобно в случае, если мы нечаянно вошли во вложенный вызов, который нам совсем не интересен — чтобы быстро из него выйти.

— отключить/включить все точки остановки. Эта кнопка никак не двигает нас по коду, она позволяет временно отключить все точки остановки в файле. Процесс отладки заключается в том, что мы останавливаем скрипт, смотрим, что с переменными, переходим дальше и ищем, где поведение отклоняется от правильного.

Дополнительные возможности Правый клик на номер строки позволяет запустить выполнение кода до неё (Continue to here).


Это очень удобно, если промежуточные строки нас не интересуют. Консоль При отладке, кроме просмотра переменных, бывает полезно запускать команды JavaScript. Для этого нужна консоль. В неё можно перейти, нажав кнопку «Console» вверху-справа, а можно и открыть в дополнение к отладчику, нажав на кнопку

или клавишей ESC.

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

1 // результат будет виден в консоли 2 for(var i=0; i<5; i++) { 3 console.log("значение", i); 4} Полную информацию по специальным командам консоли вы можете получить на страницеhttp://firebug.ru/commandline.html. Эти команды также действуют в Firebug (отладчик для браузера Firefox). Консоль поддерживают все браузеры, но IE<10 поддерживает не все функции. Логирование работает везде, пользуйтесь им вместо alert. Ошибки Ошибки JavaScript выводятся в консоли. Например, откройте страницу tutorial/debugging/pow-error/index.html. Предыдущую отладку можно прекратить (очистить брейкпойнты В консоли вы увидите что-то подобное:

и затем продолжить

).


Да, действительно, это потому что в этом скрипте есть ошибка. Но в чём же дело? Давайте посмотрим, какие были значения переменных на момент отладки. Для этого заставим отладчик остановиться при ошибке, нажав два раза на

(до фиолетового цвета).

Кстати, чтобы увидеть эту кнопку, нужно быть именно во вкладке Sources, не в Console. Теперь перезагрузите страницу. Отладчик остановится на строке с ошибкой:

Можно посмотреть значения переменных. Поставить брейкпойнты раньше по коду и посмотреть, что привело к такой печальной картине. В данном случае-то всё просто: опечатка в имени переменной y вместо x. Этот тип ошибки


называется ReferenceError. Итого Отладчик позволяет:

• Останавливаться на отмеченном месте (breakpoint) или по команде debugger. • Выполнять код — по одной строке или до определённого места. • Смотреть переменные, выполнять команды в консоли и т.п. В этой статье кратко описаны возможности отладчика Google Chrome, относящиеся именно к работе с кодом. Пока что это всё, что нам надо, но, конечно, инструменты разработчика умеют много чего ещё. В частности, вкладка Elements — позволяет работать со страницей (понадобится позже), Timeline — смотреть, что именно делает браузер и сколько это у него занимает и т.п. Осваивать можно двумя путями:

1. Официальная документация (на англ.) 2. Кликать правой кнопкой и двойным кликом в разных местах и смотреть, что получается. Строки

1. Создание строк 1. Специальные символы 2. Экранирование специальных символов 2. Методы и свойства 1. Длина length 2. Доступ к символам 3. Изменения строк 4. Смена регистра 5. Поиск подстроки 6. Поиск всех вхождений 7. Взятие подстроки: substr, substring, slice. 8. Отрицательные аргументы 3. Кодировка Юникод 4. Сравнение строк 5. Итого В JavaScript любые текстовые данные являются строками. Не существует отдельного типа «символ», который есть в ряде других языков. Внутренним форматом строк, вне зависимости от кодировки страницы, является Юникод (Unicode). Создание строк Строки создаются при помощи двойных или одинарных кавычек:

1 var text = "моя строка"; 2


3 var anotherText = 'еще строка'; 4 5 var str = "012345"; В JavaScript нет разницы между двойными и одинарными кавычками. Специальные символы Строки могут содержать специальные символы. Самый часто используемый из таких символов — этоперевод строки. Он обозначается как \n, например:

1 alert('Привет\nМир'); // выведет "Мир" на новой строке Есть и более редкие символы, вот их список:

Специальные символы Символ \b \f \n \r \t

Описание Backspace Form feed New line Carriage return Tab Символ в кодировке Юникод с шестнадцатиричным кодом NNNN. \uNNNN Например, \u00A9 — юникодное представление символа копирайт © Экранирование специальных символов Если строка в одинарных кавычках, то внутренние одинарные кавычки внутри должны бытьэкранированы, то есть снабжены обратным слешем \', вот так:

var str = 'I\'m a JavaScript programmer'; В двойных кавычках — экранируются внутренние двойные:

1 var str = "I'm a JavaScript \"programmer\" "; 2 alert(str); Экранирование служит исключительно для правильного восприятия строки JavaScript. В памяти строка будет содержать сам символ без '\'. Вы можете увидеть это, запустив пример выше. Сам символ обратного слэша '\' является служебным, поэтому всегда экранируется, т.е пишется как\\:

1 var str = ' символ \\ '; 2 3 alert(str); // символ \ Заэкранировать можно любой символ. Если он не специальный, то ничего не произойдёт:


1 alert( "\a" ); // a 2 // идентично alert( "a" ); Методы и свойства Здесь мы рассмотрим методы и свойства строк, с некоторыми из которых мы знакомились ранее, в главе Методы и свойства. Длина length Одно из самых частых действий со строкой — это получение ее длины:

1 var str = "My\n"; // 3 символа. Третий - перевод строки 2 3 alert(str.length); // 3 Доступ к символам Чтобы получить символ, используйте вызов charAt(позиция). Первый символ имеет позицию 0:

1 var str = "jQuery"; 2 alert( str.charAt(0) );

// "j"

В JavaScript нет отдельного типа «символ», так что charAt возвращает строку, состоящую из выбранного символа.

В современных браузерах (не IE7-) для доступа к символу можно также использовать квадратные скобки:

1 var str = "Я - современный браузер!"; 2 alert(str[0]); // "Я", IE8+ Разница между этим способом и charAt заключается в том, что если символа нет —charAt выдает пустую строку, а скобки — undefined:

1 alert( "".charAt(0) ); // пустая строка

2

alert( ""[0] ); // undefined, IE8+

Вызов метода — всегда со скобками


Обратите внимание, str.length — это свойство строки, а str.charAt(pos) — метод, т.е. функция. Обращение к методу всегда идет со скобками, а к свойству — без скобок. Изменения строк Строки в JavaScript нельзя изменять. Можно прочитать символ, но нельзя заменить его. Как только строка создана — она такая навсегда. Чтобы это обойти, создаётся новая строка и присваивается в переменную вместо старой:

1 var str = "строка"; 2 3 str = str.charAt(3) + str.charAt(4) + str.charAt(5); 4 5 alert(str); // ока Смена регистра Методы toLowerCase() и toUpperCase() меняют регистр строки на нижний/верхний:

1 alert( "Интерфейс".toUpperCase() ); // ИНТЕРФЕЙС Пример ниже получает первый символ и приводит его к нижнему регистру:

alert( "Интерфейс".charAt(0).toLowerCase() ); // 'и' 5 Напишите функцию ucFirst(str), которая возвращает строку str с заглавным первым символом, например:

ucFirst("вася") == "Вася"; ucFirst("") == ""; // нет ошибок при пустой строке P.S. В JavaScript нет встроенного метода для этого. Создайте функцию, используяtoUpperCase() и charAt(). Решение Мы не можем просто заменить первый символ, т.к. строки в JavaScript неизменяемы. Единственный способ - пересоздать строку на основе существующей, но с заглавным первым символом:


01 function ucFirst(str) { 02 var newStr = str.charAt(0).toUpperCase(); 03 04 for(var i=1; i<str.length; i++) { 05 newStr += str.charAt(i); 06 } 07 08 return newStr; 09 } 10 11 alert( ucFirst("вася") ); P.S. Возможны и другие решения, использующие метод str.slice и str.replace. [Открыть задачу в новом окне] Поиск подстроки Для поиска подстроки есть метод indexOf(подстрока[, начальная_позиция]). Он возвращает позицию, на которой находится подстрока или -1, если ничего не найдено. Например:

1 var str = "Widget with id"; 2 alert( str.indexOf("Widget") ); // 0, т.к. "Widget" найден прямо в 3 начале str alert( str.indexOf("id") ); // 1, т.к. "id" найден, начиная с позиции 4 1 5 alert( str.indexOf("Lalala") ); // -1, подстрока не найдена Необязательный второй аргумент позволяет искать, начиная с указанной позиции. Например, первый раз "id" появляется на позиции 1. Чтобы найти его следующее появление — запустим поиск с позиции 2:

1 var str = "Widget with id"; 2 3 alert( str.indexOf("id", 2) ) // 12, поиск начат с позиции 2 Также существует аналогичный метод lastIndexOf, который ищет не с начала, а с конца строки. Для красивого вызова indexOf применяется побитовый оператор НЕ '~'. Дело в том, что вызов ~n эквивалентен выражению -(n+1), например:


1 alert( 2 alert( 3 alert( 4 alert(

~2 ); // -(2+1) = ~1 ); // -(1+1) = ~0 ); // -(0+1) = ~-1 ); // -(-1+1)

-3 -2 -1 = 0

Как видно, ~n — ноль только в случае, когда n == -1. То есть, проверка if ( ~str.indexOf(...) ) означает, что результат indexOfотличен от `-1, т.е. совпадение есть. Вот так:

1 var str = "Widget"; 2 3 if( ~str.indexOf("get") ) { 4 alert('совпадение есть!'); 5} Вообще, использовать возможности языка неочевидным образом не рекомендуется, поскольку ухудшает читаемость кода. Однако, в данном случае, все в порядке. Просто запомните: '~' читается как «не минус один», а "if ~str.indexOf" читается как "если найдено". 5

Напишите функцию checkSpam(str), которая возвращает true, если строка str содержит ‘viagra’ or ‘XXX’. Функция должна быть нечувствительна к регистру:

checkSpam('buy ViAgRA now') == true checkSpam('free xxxxx') == true checkSpam("innocent rabbit") == false Решение Метод indexOf ищет совпадение с учетом регистра. То есть, в строке 'xXx' он не найдет 'XXX'. Для проверки приведем к нижнему регистру и строку str и то, что будем искать:

1 function checkSpam(str) {


2 str = str.toLowerCase(); 3 4 return str.indexOf('viagra') >= 0 || str.indexOf('xxx') >= 0; 5} Полное решение: tutorial/intro/checkSpam.html. [Открыть задачу в новом окне] Поиск всех вхождений Чтобы найти все вхождения подстроки, нужно запустить indexOf в цикле. Как только получаем очередную позицию — начинаем следующий поиск со следующей. Пример такого цикла:

01 var str = "Ослик Иа-Иа посмотрел на виадук"; // ищем в этой строке 02 var target = "Иа"; // цель поиска 03 04 var pos = 0; 05 while(true) { 06 var foundPos = str.indexOf(target, pos); 07 if (foundPos == -1) break; 08 09 alert(foundPos); // нашли на этой позиции 10 pos = foundPos + 1; // продолжить поиск со следующей 11 } Такой цикл начинает поиск с позиции 0, затем найдя подстроку на позиции foundPos, следующий поиск продолжит с позиции pos = foundPos+1, и так далее, пока что-то находит. Впрочем, тот же алгоритм можно записать и короче:

1 var str = "Ослик Иа-Иа посмотрел на виадук"; // ищем в этой строке 2 var target = "Иа"; // цель поиска 3 4 var pos = -1; 5 while ( (pos = str.indexOf(target, pos+1)) != -1) { 6 alert(pos); 7} Взятие подстроки: substr, substring, slice. В JavaScript существуют целых 3 (!) метода для взятия подстроки, с небольшими отличиями между ними. substring(start [, end])


Метод substring(start, end) возвращает подстроку с позиции start до, но не включая end.

1 var str = "stringify"; alert(str.substring(0,1)); // "s", символы с позиции 0 по 1 не 2 включая 1. Если аргумент end отсутствует, то идет до конца строки:

1 var str = "stringify"; 2 alert(str.substring(2)); // ringify, символы с позиции 2 до конца substr(start [, length]) Первый аргумент имеет такой же смысл, как и в substring, а второй содержит не конечную позицию, а количество символов.

1 var str = "stringify"; 2 str = str.substr(2,4); // ring, со 2й позиции 4 символа 3 alert(str) Если второго аргумента нет — подразумевается «до конца строки». slice(start [, end]) Возвращает часть строки от позиции start до, но не включая, позиции end. Смысл параметров — такой же как в substring. Отрицательные аргументы Различие между substring и slice — в том, как они работают с отрицательными и выходящими за границу строки аргументами: substring(start, end) Отрицательные аргументы интерпретируются как равные нулю. Слишком большие значения усекаются до длины строки:

1 alert( "testme".substring(-2) );

// "testme", -2 становится 0

Кроме того, если start > end, то аргументы меняются местами, т.е. возвращается участок строки между start и end:

1 alert( "testme".substring(4, -1) ); // "test" 2 // -1 становится 0 -> получили substring(4, 0) // 4 > 0, так что аргументы меняются местами -> substring(0, 4) = 3 "test" slice Отрицательные значения отсчитываются от конца строки:


1 alert( "testme".slice(-2) ); 1

// "me", от 2 позиции с конца

alert( "testme".slice(1, -1) ); // "estm", от 1 позиции до первой с конца.

Это гораздо более удобно, чем странная логика substring. Отрицательное значение первого параметра поддерживается в substr во всех браузерах, кроме IE8-. Выводы. Самый удобный метод — это slice(start, end). В качестве альтернативы можно использовать substr(start, length), помня о том, что IE8- не поддерживает отрицательный start. 5

Создайте функцию truncate(str, maxlength), которая проверяет длину строки str, и если она превосходит maxlength — заменяет конецstr на '…', так чтобы ее длина стала равна maxlength. Результатом функции должна быть (при необходимости) усечённая строка. Например:

truncate("Вот, что мне хотелось бы сказать на эту тему:", 20) = "Вот, что мне хотело…" truncate("Всем привет!", 20) = "Всем привет!" Эта функция имеет применение в жизни. Она используется, чтобы усекать слишком длинные темы сообщений. Решение Так как окончательная длина строки должна быть maxlength, то нужно её обрезать немного короче, чтобы дать место для троеточия.

01 function truncate(str, maxlength) { 02 if (str.length > maxlength) { 03 return str.slice(0, maxlength - 3) + '...'; 04 // итоговая длина равна maxlength 05 }


06 07 return str; 08 } 09 alert(truncate("Вот, что мне хотелось бы сказать на эту тему:", 10 20)); 11 alert(truncate("Всем привет!", 20)); Ещё лучшим вариантом будет использование вместо трёх точек специального символа «троеточие»: … (…), тогда можно отрезать один символ.

01 function truncate(str, maxlength) { 02 if (str.length > maxlength) { 03 return str.slice(0, maxlength - 1) + '…'; 04 } 05 06 return str; 07 } 08 alert(truncate("Вот, что мне хотелось бы сказать на эту тему:", 09 20)); 10 alert(truncate("Всем привет!", 20)); Можно было бы написать этот код ещё короче:

1 function truncate(str, maxlength) { 2 return (str.length > maxlength) ? 3 str.slice(0, maxlength - 1) + '…' : str; 4} 5 alert(truncate("Вот, что мне хотелось бы сказать на эту тему:", 6 20)); 7 alert(truncate("Всем привет!", 20)); [Открыть задачу в новом окне] Кодировка Юникод Если вы знакомы со сравнением строк в других языках, то позвольте предложить одну маленькую загадку. Даже не одну, а целых две. Как мы знаем, символы сравниваются в алфавитном порядке 'А' < 'Б' < 'В' < ... < 'Я'.


Но есть несколько странностей..

1. Почему буква 'а' маленькая больше буквы 'Я' большой? 1 alert( 'а' > 'Я' ); // true 2. Буква 'ё' находится в алфавите между е и ж: абвгдеёжз... Но почему тогда 'ё' больше 'я'?

1 alert( 'ё' > 'я' ); // true Чтобы разобраться с этим, обратимся к внутреннему представлению строк в JavaScript. Все строки имеют внутреннюю кодировку Юникод. Неважно, на каком языке написана страница, находится ли она в windows-1251 или utf-8. Внутри JavaScript-интерпретатора все строки приводятся к единому «юникодному» виду. Каждому символу соответствует свой код. Есть метод для получения символа по его коду: String.fromCharCode(code) Возвращает символ по коду code:

1 alert( String.fromCharCode(1072) ); // 'а' … И метод для получения цифрового кода из символа: str.charCodeAt(pos) Возвращает код символа на позиции pos. Отсчет позиции начинается с нуля.

1 alert( "абрикос".charCodeAt(0) ); // 1072, код 'а' Теперь вернемся к примерам выше. Почему сравнения 'ё' > 'я' и 'а' > 'Я' дают такой странный результат? Дело в том, что символы сравниваются не по алфавиту, а по коду. У кого код больше — тот и больше. В юникоде есть много разных символов. Кириллическим буквам соответствует только небольшая часть из них, подробнее — Кириллица в Юникоде. Выведем отрезок символов юникода с кодами от 1034 до 1113:

1 var str = ''; 2 for (var i=1034; i<=1113; i++) { 3 str += String.fromCharCode(i); 4} 5 alert(str); Результат:


ЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяѐёђѓєѕіїјљ Мы можем увидеть из этого отрезка две важных вещи:

1. Строчные буквы идут после заглавных, поэтому они всегда больше. В частности, 'а'(код 1072) > 'Я'(код 1071). То же самое происходит и в английском алфавите, там 'a' > 'Z'.

2. Ряд букв, например ё, находятся вне основного алфавита. В частности, маленькая буква ё имеет код, больший чем я, поэтому'ё'(код 1105) > 'я'(код 1103). Кстати, большая буква Ё располагается в Unicode до А, поэтому 'Ё'(код 1025) < 'А'(код 1040). Удивительно: есть буква меньше чем А

Юникод в HTML Кстати, если мы знаем код символа в кодировке юникод, то можем добавить его в HTML, используя «числовую ссылку» (numeric character reference). Для этого нужно написать сначала &#, затем код, и завершить точкой с запятой ';'. Например, символ 'а' в виде числовой ссылки: а. Если код хотят дать в 16-ричной системе счисления, то начинают с &#x. В юникоде есть много забавных и полезных символов, например, символ ножниц: ✂ (✂), дроби: ½ (½) ¾ (¾) и другие. Их можно удобно использовать вместо картинок в дизайне. Сравнение строк Строки сравниваются лексикографически, в порядке «телефонного справочника». Сравнение строк s1 и s2 обрабатывается по следующему алгоритму:

1. Сравниваются первые символы: a = s1.charAt(0) и b = s2.charAt(0). Если они одинаковы, то следующий шаг, иначе, в зависимости от результата их сравнения, возвратить true илиfalse 2. Сравниваются вторые символы, затем третьи и так далее… Если в одной строке закончились символы, то она меньше. Если в обеих закончились — они равны. Спецификация языка определяет этот алгоритм более детально, но смысл в точности соответствует порядку, по которому имена заносятся в телефонный справочник.

"Z" > "A" // true "Вася" > "Ваня" // true, т.к. с > н "aa" > "a" // true, т.к. начало совпадает, но в 1й строке больше символов


Числа в виде строк сравниваются как строки Бывает, что числа приходят в скрипт в виде строк, например как результат prompt. В этом случае результат их сравнения будет неверным:

1

alert("2" > "14"); // true, так как это строки, и для первых символов верно "2" > "1" Чтобы получать верный результат, хотя бы один из аргументов должен не быть строкой. Тогда и другой будет преобразован к числу:

alert(2 > "14"); // false

1

Итого

• Строки в JavaScript имеют внутреннюю кодировку Юникод. При написании строки можно использовать специальные символы, например \n и вставлять юникодные символы по коду.

• Мы познакомились со свойством length и методами charAt, toLowerCase/toUpperCase,substring/substr/slice (предпочтителен slice)

• Строки сравниваются побуквенно. Поэтому если число получено в виде строки, то такие числа могут сравниваться некорректно, нужно преобразовать его к типу number.

• При сравнении строк следует иметь в виду, что буквы сравниваются по их кодам. Поэтому большая буква меньше маленькой, а буква ё вообще вне основного алфавита. Больше информации о методах для строк можно получить в справочнике: http://javascript.ru/String. Числа

1. 2. 3. 4. 5.

Способы записи

Деление на ноль, Infinity NaN isFinite(n) Преобразование к числу 1. isNaN — проверка на число для строк 6. Мягкое преобразование: parseInt и parseFloat 7. Проверка на число для всех типов 8. toString(система счисления)


9. Округление 1. Округление до заданной точности 2. num.toFixed(precision) 10.Неточные вычисления 11.Другие математические методы 1. Тригонометрия 2. Функции общего назначения 12.Итого Все числа в JavaScript, как целые так и дробные, имеют тип Number и хранятся в 64-битном форматеIEEE-754, также известном как «double precision». Здесь мы рассмотрим различные тонкости, связанные с работой с числами в JavaScript. Способ ы записи В JavaScript можно записывать числа не только в десятичной, но и в шестнадцатеричной (начинается с0x), а также восьмеричной (начинается с 0) системах счисления:

1 alert( 0xFF ); // 255 в шестнадцатиричной системе 2 alert( 010 ); // 8 в восьмеричной системе Также доступна запись в «научном формате» (ещё говорят «запись с плавающей точкой»), который выглядит как <число>e<кол-во нулей>. Например, 1e3 — это 1 с 3 нулями, то есть 1000.

1 // еще пример научной формы: 3 с 5 нулями 2 alert( 3e5 ); // 300000 Если количество нулей отрицательно, то число сдвигается вправо за десятичную точку, так что получается десятичная дробь:

1 // здесь 3 сдвинуто 5 раз вправо, за десятичную точку. 2 alert( 3e-5 ); // 0.00003 <-- 5 нулей, включая начальный ноль Деление на ноль, Infinity Представьте, что вы собираетесь создать новый язык… Люди будут называть его «JavaScript» (или LiveScript… неважно). Что должно происходить при попытке деления на ноль? Как правило, ошибка в программе… Во всяком случае, в большинстве языков программирования это именно так. Но создатель JavaScript решил пойти математически правильным путем. Ведь чем меньше делитель, тем больше результат. При делении на очень-очень маленькое число должно получиться очень большое. В математическом анализе это описывается через пределы, и если


подразумевать предел, то в качестве результата деления на 0 мы получаем «бесконечность», которая обозначается символом ∞ или, в JavaScript: "Infinity".

1 alert(1/0); // Infinity 2 alert(12345/0); // Infinity Infinity — особенное численное значение, которое ведет себя в точности как математическая бесконечность ∞.

• Infinity больше любого числа. • Добавление к бесконечности не меняет её.

1 alert(Infinity > 1234567890); // true 2 alert(Infinity + 5 == Infinity); // true Бесконечность можно присвоить и в явном виде: var x = Infinity. Бывает и минус бесконечность -Infinity:

1 alert( -1 / 0 ); // -Infinity Бесконечность можно получить также, если сделать ну очень большое число, для которого количество разрядов в двоичном представлении не помещается в соответствующую часть стандартного 64-битного формата, например:

1 alert( 1e500 ); // Infinity NaN Если математическая операция не может быть совершена, то возвращается специальное значение NaN (Not-A-Number). Например, деление 0/0 в математическом смысле неопределено, поэтому возвращает NaN:

1 alert( 0 / 0 );

// NaN

Значение NaN используется для обозначения математической ошибки и обладает следующими свойствами:

• Значение NaN — единственное, в своем роде, которое не равно ничему, включая себя. Следующий код ничего не выведет:

1 if (NaN == NaN) alert("=="); // Ни один вызов 2 if (NaN === NaN) alert("==="); // не сработает • Значение NaN можно проверить специальной функцией isNaN(n), которая возвращает trueесли аргумент — NaN и false для любого другого значения.


1 var n = 0/0; 2 3 alert( isNaN(n) ); // true Ещё один забавный способ проверки значения на NaN — это проверить его на равенство самому себе, вот так:

1 var n = 0/0; 2 3 if (n !== n) alert('n = NaN!');

Это работает, но для наглядности лучше использовать isNaN.

• Значение NaN «прилипчиво». Любая операция с NaN возвращает NaN. 1 alert( NaN + 1 ); // NaN Если аргумент isNaN — не число, то он автоматически преобразуется к числу. Никакие математические операции в JavaScript не могут привести к ошибке или «обрушить» программу. В худшем случае, результат будет NaN. isFinite(n) Итак, в JavaScript есть обычные числа и три специальных числовых значения: NaN, Infinity иInfinity. Функция isFinite(n) возвращает true только тогда, когда n — обычное число, а не одно из этих значений:

1 alert( isFinite(1) ); // true 2 alert( isFinite(Infinity) ); // false 3 alert( isFinite(NaN) ); // false Если аргумент isFinite — не число, то он автоматически преобразуется к числу. Преобразование к числу Строгое преобразование можно осуществить унарным плюсом '+'


1 var s = "12.34"; 2 alert( +s ); // 12.34 Строгое — означает, что если строка не является в точности числом, то результат будет NaN:

1 alert( +"12test" );

// NaN

Единственное исключение — пробельные символы в начале и в конце строки, которые игнорируются:

1 alert( +" -12"); // -12 alert( +" \n34 \n"); // 34, перевод строки \n является пробельным 2 символом 3 alert( +"" ); // 0, пустая строка становится нулем 4 alert( +"1 2" ); // NaN, пробел посередине числа - ошибка Аналогичным образом происходит преобразование и в других математических операторах и функциях:

1 alert( '12.34' / "-2" );

// -6.17

5

Создайте страницу, которая предлагает ввести два числа и выводит их сумму. Работать должно так: tutorial/intro/sum.html. P.S. Есть «подводный камень» при работе с типами. Решение tutorial/intro/sum.html [Открыть задачу в новом окне] isNaN — проверка на число для строк Функция isNaN является математической, она преобразует аргумент в число, а затем проверяет, NaNэто или нет. Поэтому можно использовать ее для проверки:

1 var x = "-11.5"; 2 if (isNaN(x)) { 3 alert("Строка преобразовалась в NaN. Не число"); 4 } else { 5 alert("Число");


6} Единственный тонкий момент — в том, что пустая строка и строка из пробельных символов преобразуются к 0:

1

alert(isNaN(" \n\n преобразуется к 0

")) // false, т.к. строка из пробелов

И, конечно же, проверка isNaN посчитает числами значения false, true, null, т.к. они хотя и не числа, но преобразуются к ним:

1 +false = 0 2 +true = 1 3 +null = 0 4 +undefined = NaN; Мягкое преобразование: parseInt и parseFloat В мире HTML/CSS многие значения не являются в точности числами. Например, метрики CSS: 10ptили -12px. Оператор '+' для таких значений возвратит NaN:

1 alert( +"12px" ) // NaN Для удобного чтения таких значений существует функция parseInt:

1 alert( parseInt('12px') ); // 12 parseInt и ее аналог parseFloat преобразуют строку символ за символом, пока это возможно. При возникновении ошибки возвращается число, которое получилось. parseInt читает из строки целое число, а parseFloat — дробное.

1 alert( parseInt('12px') ) // 12, ошибка на символе 'p' 2 alert( parseFloat('12.3.4') ) // 12.3, ошибка на второй точке Конечно, существуют ситуации, когда parseInt/parseFloat возвращают NaN. Это происходит при ошибке на первом же символе:

1 alert( parseInt('a123') ); // NaN

Ошибка parseInt('0..') parseInt (но не parseFloat) понимает 16-ричную систему счисления:


1 alert( parseInt('0xFF') ) // 255 В старом стандарте JavaScript он умел понимать и восьмеричную:

1 alert( parseInt('010') )

// в некоторых браузерах 8

Если вы хотите быть уверенным, что число, начинающееся с нуля, будет интерпретировано верно — используйте второй необязательный аргумент parseInt— основание системы счисления:

1

alert( parseInt('010', 10) ); // во всех браузерах 10

Проверка на число для всех типов

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

function isNumeric(n) { return !isNaN(parseFloat(n)) && isFinite(n); } Разберёмся, как она работает. Начнём справа.

• Функция isFinite(n) преобразует аргумент к числу и возвращает true, если это неInfinity/-Infinity/NaN. Таким образом, правая часть отсеет заведомо не-числа, но оставит такие значения какtrue/false/null и пустую строку '', т.к. они корректно преобразуются в числа.

• Для их проверки нужна левая часть. Вызов parseFloat(true/false/null/'') вернёт NaN для этих значений. Так устроена функция parseFloat: она преобразует аргумент к строке, т.е. true/false/nullстановятся "true"/"false"/"null", а затем считывает из неё число, при этом пустая строка даёт NaN. В результате отсеивается всё, кроме строк-чисел и обычных чисел. toString(система счисления) Как показано выше, числа можно записывать не только в 10-чной, но и в 16-ричной системе. Но


бывает и противоположная задача: получить 16-ричное представление числа. Для этого используется метод toString(основание системы), например:

1 var n = 255; 2 3 alert( n.toString(16) ); // ff Основание может быть любым от 2 до 36.

• Основание 2 бывает полезно для отладки битовых операций, которые мы пройдём чуть позже:

1 var n = 4; 2 alert( n.toString(2) ); // 100 • Основание 36 (по количеству букв в английском алфавите — 26, вместе с цифрами, которых 10) используется для того, чтобы «кодировать» число в виде буквенно-цифровой строки. В этой системе счисления сначала используются цифры, а затем буквы от a до z:

1 var n = 1234567890; 2 alert( n.toString(36) ); // kf12oi При помощи такого кодирования можно сделать длинный цифровой идентификатор короче, чтобы затем использовать его в URL. Округление Одна из самых частых операций с числом — округление. В JavaScript существуют целых 3 функции для этого. Math.floor Округляет вниз Math.ceil Округляет вверх Math.round Округляет до ближайшего целого

1 alert( Math.floor(3.1) ); 2 alert( Math.ceil(3.1) ); 3 alert( Math.round(3.1) );

// 3 // 4 // 3

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


В результате побитовая операция, которая не изменяет число, например, двойное битовое НЕ — округляет его:

1 alert( ~~12.3 );

// 12

Любая побитовая операция такого рода подойдет, например XOR (исключающее ИЛИ, "^") с нулем:

1 alert( 12.3 ^ 0 ); // 12 2 alert( 1.2 + 1.3 ^ 0); // 2, приоритет ^ меньше, чем + Это удобно в первую очередь тем, что легко читается и не заставляет ставить дополнительные скобки как Math.floor(...):

var x = a * b / c ^ 0; // читается так: "a*b/c и округлить" Округление до заданной точности Обычный трюк — это умножить и поделить на 10 с нужным количеством нулей. Например, округлим3.456 до 2го знака после запятой:

1 var n = 3.456; alert( Math.round( n * 100 ) / 100 ); 2 3.46

// 3.456 -> 345.6 -> 346 ->

Таким образом можно округлять число и вверх и вниз. num.toFixed(precision) Существует специальный метод num.toFixed(precision), который округляет число num до точностиprecision и возвращает результат в виде строки:

1 var n = 12.34; 2 alert( n.toFixed(1) ); // "12.3" Округление идёт до ближайшего значения, аналогично Math.round:

1 var n = 12.36; 2 alert( n.toFixed(1) ); // "12.4"


Итоговая строка, при необходимости, дополняется нулями до нужной точности:

1 var n = 12.34; alert( n.toFixed(5) ); // "12.34000", добавлены нули до 5 знаков после 2 запятой Если нам нужно именно число, то мы можем получить его, применив '+' к результату n.toFixed(..):

1 var n = 12.34; 2 alert( +n.toFixed(5) ); // 12.34

Метод toFixed не эквивалентен Math.round! Например, произведём округление до одного знака после запятой с использованием двух способов:

1 var price = 6.35; 2 3 alert( price.toFixed(1) ); // 6.3 4 alert( Math.round(price*10)/10 ); // 6.4

Как видно, результат разный! Вариант округления через Math.round получился более корректным, так как по общепринятым правилам 5 округляется вверх. А toFixedможет округлить его как вверх, так и вниз. Почему? Скоро узнаем! Неточн ые в ычисления Запустите этот пример:

1 alert(0.1 + 0.2 == 0.3); Запустили? Если нет — все же сделайте это. Ок, вы запустили его. Результат несколько странный, не так ли? Возможно, ошибка в браузере? Поменяйте браузер, запустите еще раз. Хорошо, теперь мы можем быть уверены: 0.1 + 0.2 это не 0.3. Но тогда что же это?

1 alert(0.1 + 0.2); // 0.30000000000000004


Как видите, произошла небольшая вычислительная ошибка. Дело в том, что в стандарте IEEE 754 на число выделяется ровно 8 байт(=64 бита), не больше и не меньше. Число 0.1 (=1/10) короткое в десятичном формате, а в двоичной системе счисления это бесконечная дробь (перевод десятичной дроби в двоичную систему). Также бесконечной дробью является 0.2 (=2/10). Двоичное значение бесконечных дробей хранится только до определенного знака, поэтому возникает неточность. Это даже можно увидеть:

1 alert( 0.1.toFixed(20) );

// 0.10000000000000000555

Когда мы складываем 0.1 и 0.2, то две неточности складываются, получаем третью. Конечно, это не означает, что точные вычисления для таких чисел невозможны. Они возможны. И даже необходимы. Например, есть два способа сложить 0.1 и 0.2: 1. Сделать их целыми, сложить, а потом поделить:

1 alert( (0.1*10 + 0.2*10) / 10 ); // 0.3 Это работает, т.к. числа 0.1*10 = 1 и 0.2*10 = 2 могут быть точно представлены в двоичной системе. 2. Сложить, а затем округлить до разумного знака после запятой. Округления до 10-го знака обычно бывает достаточно, чтобы отсечь ошибку вычислений:

1 var result = 0.1 + 0.2; 2 alert( +result.toFixed(10) ); // 0.3 4 В математике принято, что 5 округляется вверх, например:

1 alert( 1.5.toFixed(0) ); // 2 2 alert( 1.35.toFixed(1) ); // 1.4 Но почему в примере ниже 6.35 округляется до 6.3?

1 alert( 6.35.toFixed(1) ); // 6.3 Решение


Во внутреннем двоичном представлении 6.35 является бесконечной двоичной дробью. Хранится она с потерей точности.. А впрочем, посмотрим сами:

1 alert( 6.35.toFixed(20) ); // 6.34999999999999964473 Интерпретатор видит число как 6.34..., поэтому и округляет вниз. [Открыть задачу в новом окне] 5

Представьте себе электронный магазин. Цены даны с точностью до копейки(цента, евроцента и т.п.). Вы пишете интерфейс для него. Основная работа происходит на сервере, но и на клиенте все должно быть хорошо. Сложение цен на купленные товары и умножение их на количество является обычной операцией. Получится глупо, если при заказе двух товаров с ценами 0.10$ и 0.20$ человек получит общую стоимость 0.30000000000000004$:

1 alert( 0.1 + 0.2 + '$' ); Что можно сделать, чтобы избежать проблем с ошибками округления? Решение Есть два основных подхода. 1. Можно хранить сами цены в «копейках» (центах и т.п.). Тогда они всегда будут целые и проблема исчезнет. Но при показе и при обмене данными нужно будет это учитывать и не забывать делить на 100. 2. При операциях, когда необходимо получить окончательный результат — округлять до 2го знака после запятой. Все, что дальше — ошибка округления:

1 var price1 = 0.1, price2 = 0.2; 2 alert( +(price1 + price2).toFixed(2) ); [Открыть задачу в новом окне]


Забавный пример Привет! Я — число, растущее само по себе!

1 alert(9999999999999999); Причина та же — потеря точности. Из 64 бит, отведённых на число, сами цифры числа занимают до 52 бит, остальные11 бит хранят позицию десятичной точки и один бит — знак. Так что если 52 бит не хватает на цифры, то при записи пропадут младшие разряды. Интерпретатор не выдаст ошибку, но в результате получится «не совсем то число», что мы и видим в примере выше. Как говорится: «как смог, так записал».

Ради справедливости заметим, что в точности то же самое происходит в любом другом языке, где используется формат IEEE 754, включая Java, C, PHP, Ruby, Perl. Другие математические метод ы JavaScript предоставляет базовые тригонометрические и некоторые другие функции для работы с числами. Тригонометрия Встроенные функции для тригонометрических вычислений: Math.acos(x) Возвращает арккосинус x (в радианах) Math.asin(x) Возвращает арксинус x (в радианах) Math.atan Возвращает арктангенс x (в радианах) Math.atan2(y, x) Возвращает угол до точки (y, x). Описание функции: Atan2. Math.sin(x) Вычисляет синус x (в радианах) Math.cos(x) Вычисляет косинус x (в радианах) Math.tan(x) Возвращает тангенс x (в радианах)


Функции общего назначения Разные полезные функции: Math.sqrt(x) Возвращает квадратный корень из x. Math.log(x) Возвращает натуральный (по основанию e) логарифм x. Math.pow(x, exp) Возводит число в степень, возвращает xexp, например Math.pow(2,3) = 8. Работает в том числе с дробными и отрицательными степенями, например: Math.pow(4, -1/2) = 0.5. Math.abs(x) Возвращает абсолютное значение числа Math.exp(x) Возвращает ex, где e — основание натуральных логарифмов. Math.max(a, b, c...) Возвращает наибольший из списка аргументов Math.min(a, b, c...) Возвращает наименьший из списка аргументов Math.random() Возвращает псевдо-случайное число в интервале [0,1) - то есть между 0(включительно) и 1(не включая). Генератор случайных чисел инициализуется текущим временем. Итого • Числа могут быть записаны в шестнадцатиричной, восьмеричной системе, а также «научным» способом.

• В JavaScript существует числовое значение бесконечность Infinity. • Ошибка вычислений дает NaN. • Арифметические и математические функции преобразуют строку в точности в число, игнорируя начальные и конечные пробелы.

• Функции parseInt/parseFloat делают числа из строк, которые начинаются с числа. • Есть четыре способа округления: Math.floor, Math.round, Math.ceil и битовый оператор. Для округления до нужного знака используйте +n.toFixed(p) или трюк с умножением и делением на 10p. • Дробные числа дают ошибку вычислений. При необходимости ее можно отсечь округлением до нужного знака.

• Случайные числа от 0 до 1 генерируются с помощью Math.random(), остальные — преобразованием из них. Существуют и другие математические функции. Вы можете ознакомиться с ними в справочнике в разделах Number и Math. 4


Этот цикл - бесконечный. Почему?

1 var i = 0; 2 while(i != 10) { 3 i += 0.2; 4} Решение Потому что i никогда не станет равным 10. Запустите, чтобы увидеть реальные значения i:

1 var i = 0; 2 while(i < 11) { 3 i += 0.2; 4 if (i>9.8 && i<10.2) alert(i); 5} Ни одно из них в точности не равно 10. [Открыть задачу в новом окне] 3

Напишите функцию getDecimal(num), которая возвращает десятичную часть положительного числа:

alert( getDecimal(12.5) ); // 0.5 alert( getDecimal(6.25) ); // 0.25 Пожалуйста, не преобразуйте число к строке в процессе. Ответьте на два вопроса: • Точно ли работает ваша функция для большинства чисел? • Если нет, но при этом есть информация, что дробная часть может быть не больше 6 знаков после запятой — можно ли поправить функцию, чтобы работала корректно? Решение Функция Функция может быть такой:


1 function getDecimal(num) { 2 return num - Math.floor(num); 3} 4 5 alert( getDecimal(12.5) ); // 0.5 6 alert( getDecimal(6.25) ); // 0.25 …Или, гораздо проще, такой:

function getDecimal(num) { return num % 1; } Числа Обычно функция работает неправильно из-за неточных алгоритмов работы с дробями. Например:

1 alert( 1.2 % 1 ); // 0.19999999999999996 2 alert( 1.3 % 1 ); // 0.30000000000000004 3 alert( 1.4 % 1 ); // 0.3999999999999999 Исправление Можно добавить округление toFixed, которое отбросит лишние знаки:

1 function getDecimal(num) { 2 return +(num % 1).toFixed(6); 3} 4 5 alert( getDecimal(1.2) ); // 0.2 6 alert( getDecimal(1.3) ); // 0.3 7 alert( getDecimal(1.4) ); // 0.4 [Открыть задачу в новом окне] 4 Последовательность чисел Фибоначчи имеет формулуFn = Fn-1 + Fn-2. То есть, следующее число получается как сумма двух предыдущих.


Первые два числа равны 1, затем 2(1+1), затем 3(1+2), 5(2+3) и так далее:1, 1, 2, 3, 5, 8, 13, 21.... Код для их вычисления (из задачи Числа Фибоначчи):

1 function fib(n){ 2 var a=1, b=0, x; 3 for(i=0; i<n; i++) { 4 x = a+b; 5 a = b 6 b = x; 7 } 8 return b; 9} Существует формула Бине, согласно которой Fn равно ближайшему целому дляϕn/√5, где ϕ=(1+√5)/2 — золотое сечение. Напишите функцию fib(n), которая будет вычислять Fn, используя эту формулу. Проверьте её для значенияF77 (должно получиться fib(77) = 5527939700884757). Правилен ли полученный результат? Если нет, то почему? Решение Вычисление по следствию из формулы Бине:

1 function fib(n) { 2 var phi = (1 + Math.sqrt(5)) / 2; 3 return Math.round( Math.pow(phi, n) / Math.sqrt(5) ); 4} 5 6 alert( fib(2) ); // 1, верно 7 alert( fib(8) ); // 21, верно 8 alert( fib(77)); // 5527939700884755 != 5527939700884757, неверно! Обратите внимание — при вычислении используется округление Math.round, т.к. нужно именно ближайшее целое. Результат вычисления F77 неправильный!


Он отличается от вычисленного другим способом. Причина — в ошибках округления, ведь √5 — бесконечная дробь. Ошибки округления при вычислениях множатся и, в итоге, дают расхождение. [Открыть задачу в новом окне] 2

Напишите код для генерации случайного значения в диапазоне от 0 доmax, не включая max. Решение Сгенерируем значение в диапазоне 0..1 и умножим на max:

1 var max = 10; 2 3 alert( Math.random()*max ); [Открыть задачу в новом окне] 2

Напишите код для генерации случайного числа от min до max, не включая max. Решение Сгенерируем значение из интервала 0..max-min, а затем сдвинем на min:

1 var min=5, max = 10; 2 3 alert( min + Math.random()*(max-min) ); [Открыть задачу в новом окне] 2

Напишите код для генерации случайного целого числа между min иmax, включая min,max как возможные значения. Любое число из интервала min..max должно иметь одинаковую вероятность. Решение


Очевидное неверное решение (round) Самый простой, но неверный способ - это сгенерировать значение в интервалеmin..max и округлить его Math.round, вот так:

var rand = min + Math.random()*(max-min) rand = Math.round(rand); Оно работает. Но при этом вероятность получить крайние значения min и max будет в два раза меньше, чем любые другие. Например, давайте найдем значения между 1 и 3 этим способом:

// случайное число от 1 до 3, не включая 3 var rand = 1 + Math.random()*(3-1) Вызов Math.round() округлит значения следующим образом:

значения из диапазона 1 ... 1.499+ значения из диапазона 1.5 ... 2.499+ значения из диапазона 2.5 ... 2.999+

станут 1 станут 2 станут 3

Отсюда уже видно, что в 1 (как и 3) попадает диапазон в два раза меньший, чем в 2. Так что 1 будет выдаваться в два раза реже, чем 2. Верное решение с round Правильный способ: Math.round(случайное от min-0.5 до max+0.5)

1 var min = 1, max = 3; 2 3 var rand = min - 0.5 + Math.random()*(max-min+1) 4 rand = Math.round(rand); 5 6 alert(rand); В этом случае диапазон будет тот же (max-min+1), но учтена механика округленияround. Решение с floor


Альтернативный путь - применить округление Math.floor() к случайному числу отmin до max+1. Например, для генерации целого числа от 1 до 3, создадим вспомогательное случайное значение от 1 до 4 (не включая 4). Тогда Math.floor() округлит их так:

1 ... 1.999+ станет 1 2 ... 2.999+ станет 2 3 ... 3.999+ станет 3 Все диапазоны одинаковы. Итак, код:

1 var min=5, max=10; 2 var rand = min + Math.random()*(max+1-min); 3 rand = rand^0; // округление битовым оператором 4 alert(rand); [Открыть задачу в новом окне] Объекты как ассоциативные массивы

1. Ассоциативные массивы 2. Создание объектов 3. Операции с объектом 1. Доступ через квадратные скобки 2. Объявление со свойствами 1. Вложенные объекты в объявлении 4. Перебор свойств и значений 1. Количество свойств в объекте 2. Порядок перебора свойств 5. Передача по ссылке 6. Компактное представление объектов 7. Итого Объекты в JavaScript являются «двуличными». Они сочетают в себе два важных функционала. Первый — это ассоциативный массив: структура, пригодная для хранения любых данных. В этой главе мы рассмотрим использование объектов именно как массивов. Второй — языковые возможности для объектно-ориентированного программирования. Эти возможности мы изучим в последующих разделах учебника. Ассоциативн ые массив ы Ассоциативный массив — структура данных, в которой можно хранить любые данные в формате


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

1. o = new Object(); 2. o = {}; // пустые фигурные скобки Обычно все пользуются синтаксисом (2), т.к. он короче. Операции с объектом Объект может содержать в себе любые значения, которые называются свойствами объекта. Доступ к свойствам осуществляется по имени свойства (иногда говорят «по ключу»). Например, создадим объект person для хранения информации о человеке:

var person = {}; // пока пустой Основные операции с объектами — это:

1. Присвоение свойства по ключу. 2. Чтение свойства по ключу. 3. Удаление свойства по ключу. Для обращения к свойствам используется запись «через точку», вида объект.свойство:

01 var person = {}; 02 03 // 1. присвоение 04 // при присвоении свойства в объекте автоматически создаётся "ящик" 05 // с именем "name" и в него записывается содержимое 'Вася' 06 person.name = 'Вася'; 07 person.age = 25; // запишем ещё одно свойство: с именем 'age' и 08 значением 25 09 10 // 2. чтение 11 alert(person.name + ': ' + person.age); // вывести значения 12


13 // 3. удаление delete person.name; // удалить "ящик" с именем "name" вместе со 14 значением в нём Следующая операция:

4. Проверка существования свойства с определенным ключом. Например, есть объект person, и нужно проверить, существует ли в нем свойство age. Для проверки существования есть оператор in. Его синтаксис: "prop" in obj, причем имя свойства — в виде строки, например:

1 var person = { }; 2 3 if ("age" in person) { 4 alert("Свойство age существует!"); 5} Впрочем, чаще используется другой способ — сравнение значения с undefined. Дело в том, что в JavaScript можно обратиться к любому свойству объекта, даже если его нет. Ошибки не будет. Но если свойство не существует, то вернется специальное значение undefined:

1 var person = {}; 2 3 alert(person.lalala); // undefined, нет свойства с ключом lalala Таким образом мы можем легко проверить существование свойства — получив его и сравнив с undefined:

1 var person = { name: "Василий" }; 2 3 alert(person.lalala === undefined); // true, свойства нет 4 alert(person.name === undefined); // false, свойство есть.

Разница между проверками in и === undefined Сравнение с undefined не работает, если значение свойства равно undefined:

1 var obj = {}; 2 obj.test = undefined; // добавили свойство в объект 3


4 // проверим наличие свойств test и заведомо отсутствующего blabla 5 alert(obj.test === undefined); // true 6 alert(obj.blabla === undefined); // true .. А оператор in гарантирует точный результат:

1 var obj = {}; 2 obj.test = undefined; 3 4 alert( "test" in obj ); // true 5 alert( "blabla" in obj ); // false Чтобы у вас работали обе проверки — не присваивайте undefined. В качестве значения, обозначающего неизвестность и неопределенность, используйте null. Доступ через квадратные скобки Существует альтернативный синтаксис работы со свойствами, использующий квадратные скобкиобъект['свойство']:

1 var person = {}; 2 3 person['name'] = 'Вася'; // то же что и person.name 4 5 alert(person['name']); 6 7 delete person['name']; В квадратные скобки можно передать переменную: obj[key] использует значение переменной key в качестве имени свойства:

1 var person = { age: 25 }; 2 var key = 'age'; 3 4 alert(person[key]); // выведет person['age'] Записи person['age'] и person.age идентичны. С другой стороны, если имя свойства хранится в переменной (var key = "age"), то единственный способ к нему обратиться — квадратные скобкиperson[key]. Обращение через точку используется, если мы на этапе написания программы уже знаем название свойства. А если оно будет определено по ходу выполнения, например, введено посетителем и записано в переменную, то единственный выбор — квадратные скобки.


Ограничения на имя свойства

• На имя свойства при доступе «через точку» наложены синтаксические ограничения — примерно те же, что и на имя переменной. Оно не может начинаться с числа, содержать пробелы и т.п. • При обращении через квадратные скобки можно использовать любую строку.

Например:

person['день рождения!'] = '18.04.1982'; // запись через точку "person.день рождения! = ..." невозможна В обоих случаях, имя свойства обязано быть строкой. Если использовано значение другого типа — JavaScript приведет его к строке автоматически. Объявление со свойствами Объект можно заполнить значениями при создании, указав их в фигурных скобках:{ ключ1: значение1, ключ2: значение2, ... }. Такой синтаксис называется литеральным (оригинал - literal), например:

Следующие два фрагмента кода создают одинаковый объект:

01 var menuSetup = { 02 width: 300, 03 height: 200, 04 title: "Menu" 05 }; 06 07 // то же самое, что: 08 09 var menuSetup = {}; 10 menuSetup.width = 300; 11 menuSetup.height = 200;


12 menuSetup.title = 'Menu'; Названия свойств можно перечислять в кавычках или без, если они удовлетворяют ограничениям для имён переменных. Например:

1 var 2 3 4 5 };

menuSetup = { width: 300, 'height': 200, "мама мыла раму": true

3

Мини-задача на синтаксис объектов. Напишите код, по строке на каждое действие.

1. 2. 3. 4. 5.

Создайте пустой объект user. Добавьте свойство name со значением Вася. Добавьте свойство surname со значением Петров. Поменяйте значение name на Сергей. Удалите свойство name из объекта.

Решение

1 var user = {}; 2 user.name = "Вася"; 3 user.surname = "Петров"; 4 user.name = "Сергей"; 5 delete user.name; [Открыть задачу в новом окне] Вложенные объекты в объявлении Значением свойства может быть объект:

01 var user = { 02 name: "Таня", 03 age: 25, 04 size: { 05 top: 90, 06 middle: 60, 07 bottom: 90 08 } 09 } 10


11 alert( user.name ) // "Таня" 12 13 alert( user.size.top ) // 90 Здесь значением свойства size является объект {top: 90, middle: 60, bottom: 90 }.

Вывод объекта, Firefox

Для целей отладки иногда хочется вывести объект целиком. В Firefox для этого существует нестандартный метод toSource.

1 var user = { 2 name: "Таня", 3 size: { 4 top: 90, 5 middle: 60 6 } 7} 8 9 alert( user.toSource() ); // работает только в Firefox В других браузерах его нет, но объект можно посмотреть через инструменты разработчика: отладчик или console.log. Перебор свойств и значений Для перебора всех свойств из объекта используется цикл по свойствам for..in. Это специальная синтаксическая конструкция, которая работает не так, как обычный цикл for (i=0;i<n;i++). Синтаксис:

for (key in obj) { /* ... делать что-то с obj[key] ... */ } При этом в key будут последовательно записаны имена свойств.

Объявление переменной в цикле for (var key in obj) Переменную можно объявить прямо в цикле:


for (var key in menu) { // ... }

Так иногда пишут для краткости кода.

Например:

01 var 02 03 04 05 }; 06 07 for 08 09 10 11 12 }

menu = { width: 300, height: 200, title: "Menu"

(var key in menu) { // этот код будет вызван для каждого свойства объекта // ..и выведет имя свойства и его значение alert("Ключ: " + key + " значение:" + menu[key]);

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

1 function getKeysCount(obj) { 2 var counter = 0; 3 for (var key in obj) { 4 counter++; 5 } 6 return counter; 7} А вот так выглядит функция проверки на «пустоту»:

1 function isEmpty(obj) { 2 for (var key in obj) { return false; // если цикл хоть раз сработал, то объект не пустой 3 => false 4 } // дошли до этой строки - значит цикл не нашёл ни одного свойства => 5 true


6 return true; 7} Порядок перебора свойств Упорядочены ли свойства в объектах? В теории, т.е. по спецификации — нет. На практике, всё сложнее. Браузеры придерживаются важного соглашения о порядке перебора свойств. Оно, хоть и не прописано в стандарте, но достаточно надежно, т.к. много существующего кода зависит от него. • Браузеры IE<9, Firefox, Safari перебирают ключи в том же порядке, в котором свойства присваивались.

• Opera, современный IE, Chrome гарантируют сохранение порядка только длястроковых ключей. Численные ключи сортируются и идут до строковых. То есть, если свойства в объекте строковые, то такой объект упорядочен. Свойства будут перебираться в порядке их присвоения:

1 var user = { 2 name: "Вася", 3 surname: "Петров" 4 }; 5 user.age = 25; 6 7 for (var prop in user) { 8 alert(prop); // name, surname, age 9} Рассмотрим теперь случай, когда ключи не строковые. Например, следующий объект задает список опций для выбора страны:

1 var codes = { 2 // телефонные коды в формате "код страны": "название" 3 "7": "Россия", 4 "38": "Украина", 5 // .., 6 "1": "США" 7 }; Мы хотели бы предложить их посетителю в том же порядке, то есть «Россия» в начале, а «США» — в конце. Однако, при переборе их через for..in некоторые браузеры (IE9+, Chrome, Opera) выведут ключи не в том порядке, в котором они заданы, а в отсортированном порядке:

1 var codes = {


2 "7": "Россия", 3 "38": "Украина", 4 "1": "США" 5 }; 6 7 for (var key in codes) { 8 alert(key + ": "+codes[key]); // 1, 7, 38 в IE9+, Chrome, Opera 9} Нарушение порядка возникло, потому что ключи численные. Чтобы его обойти, можно применить небольшой хак, который заключается в том, что все ключи искусственно делаются строчными. Например, добавим им дополнительный первый символ '+'. В этом случае браузер сочтет ключи строковыми и сохранит порядок перебора:

01 var codes = { "+7": "Россия", // передаём 7 в виде строки, чтобы браузер сохранил 02 порядок 03 "+38": "Украина", 04 "+1": "США" 05 }; 06 07 for (var key in codes ) { 08 var value = codes[key]; var id = +key; // ..если нам нужно именно число, преобразуем: "+7" 09 -> 7 10 11 alert( id + ": " + value ); // 7, 38, 1 во всех браузерах 12 } Передача по ссылке Обычные значения: строки, числа, булевы значения, null/undefined копируются «по значению». Это означает, что если в переменной message хранится значение "Привет", и её скопировалиphrase = message, то значение копируются, и у нас будут две переменные, каждая из которых хранит значение: "Привет". Объекты присваиваются и передаются «по ссылке». В переменной хранится не сам объект, а ссылка на его место в памяти. Чтобы лучше понять это — сравним с обычными переменными. Например:

var message = "Привет"; // значение в переменной


А вот как выглядит переменная user = { name: "Вася" }. Внимание: объект — вне переменной. В переменной — лишь ссылка («адрес места в памяти, где лежит объект»).

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

var user = { name: "Вася" }; // в переменной - ссылка var admin = user; // скопировали ссылку


Так как объект всего один, то в какой бы переменной его не меняли — это отражается на других:

1 var user = { name: 'Вася' }; 2 3 var admin = user; 4 5 admin.name = 'Петя'; // поменяли данные через admin 6 7 alert(user.name); // 'Петя', изменения видны в user

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

«Настоящее» копирование объекта Итак, при передаче объекта куда-либо, копируется лишь ссылка на него. Чтобы скопировать сами данные, нужно достать их из объекта и скопировать на уровне примитивов. Примерно так:


01 function clone(obj) { 02 var obj2 = {}; 03 // если свойства могут быть объектами, то нужно перебирать и 04 их 05 for(var key in obj) obj2[key] = obj[key]; 06 07 return obj2; 08 } 09 10 var user = { name: 'Вася' }; // user - ссылка на объект 11 12 var admin = clone(user); / 'Вася' 13 14 admin.name = 'Петя'; // поменяли данные в admin

5 Создайте функцию multiplyNumeric, которая получает объект и умножает все численные свойства на 2. Например:

01 // до вызова 02 var menu = { 03 width: 200, 04 height: 300, 05 title: "My menu" 06 }; 07 08 multiplyNumeric(menu); 09 10 // после вызова 11 menu = { 12 width: 400, 13 height: 600, 14 title: "My menu" 15 }; P.S. Для проверки на число используйте функцию:


function isNumeric(n) { return !isNaN(parseFloat(n)) && isFinite(n) } Решение

01 var menu = { 02 width: 200, 03 height: 300, 04 title: "My menu" 05 }; 06 07 function isNumeric(n) { 08 return !isNaN(parseFloat(n)) && isFinite(n); 09 } 10 11 function multiplyNumeric(obj) { 12 for(var key in obj) { 13 if (isNumeric( obj[key] )) { 14 obj[key] *= 2; 15 } 16 } 17 } 18 19 multiplyNumeric(menu); 20 alert("menu width="+menu.width+" height="+menu.height+" 21 title="+menu.title); [Открыть задачу в новом окне] Компактное представление объектов

Hardcore coders only Эта секция относится ко внутреннему устройству структуры данных и требует специальных знаний. Она не обязательна к прочтению.

Объект, в таком виде как он описывается — занимает много места в памяти, например:

1 var user = { 2 name: "Vasya", 3 age: 25 4 };


Здесь содержится информация о свойстве name и его строковом значении, а также о свойстве age и его численном значении. Представим, что таких объектов много. Получится, что информация об именах свойств name и age дублируется в каждом объекте. Чтобы избежать этого, браузеры используют специальное «компактное представление объектов». При создании множества объектов одного и того же вида (с одинаковыми полями) интерпретатор запоминает, сколько у него полей и какие они, и хранит эти данные в отдельной структуре. А сам объект — в виде непрерывного массива данных. Например, для объектов вида {name: "Вася", age: 25} будет создана структура, которая описывает данный вид объектов: «строка name, затем целое age», а сами объекты будут представлены в памяти данными: Вася25. Причём вместо строки обычно будет указатель на неё. Браузер для доступа к свойству узнает из структуры, где оно лежит и перейдёт на нужную позицию в данных. Это очень быстро, и экономит память, когда на одно описание структуры приходится много однотипных объектов. Что же происходит, если к объекту добавляется новое поле? Например:

user.admin = true; В этом случае браузер смотрит, есть ли уже структура, под которую подходит такой объект («строкаname, целое age, логическое admin»). Если нет — она создаётся. Изменившиеся данные объекта привязываются к ней. Детали применения и реализации этого способа хранения варьируются от браузера к браузеру. Применяются дополнительные оптимизации. Более подробно внутреннем устройстве типов вы можете узнать, например, из презентации Know Your Engines (Velocity 2011). Итого Объекты — это ассоциативные массивы с дополнительными возможностями: • Доступ к элементам осуществляется:

• Напрямую по ключу obj.prop = 5 • Через переменную, в которой хранится ключ:

var key = "prop"; obj[key] = 5 • Удаление ключей: delete obj.name. • Цикл по ключам: for (key in obj), порядок перебора соответствует порядку объявления для строковых ключей, а для числовых - зависит от браузера.

• Существование свойства может проверять оператор in: if ("prop" in obj), как правило, работает и просто сравнение if (obj.prop !== undefined).


• Переменная хранит не сам объект, а ссылку на него. Копирование такой переменной и передача её в функцию дублируют ссылку, но объект остается один. Массивы c числовыми индексами

1. Объявление 2. Методы pop/push, shift/unshift 1. Конец массива 2. Начало массива 3. Внутреннее устройство массива 1. Влияние на быстродействие 4. Перебор элементов 5. Особенности работы length 1. Используем length для укорачивания массива 6. Создание вызовом new Array 1. new Array() 2. Многомерные массивы 7. Внутреннее представление массивов 8. Итого Массив с числовыми индексами - это коллекция данных, которая хранит сколько угодно значений, причем у каждого значения - свой уникальный номер. Если переменная — это коробка для данных, то массив — это шкаф с нумерованными ячейками, в каждой из которых могут быть свои данные. Например, при создании электронного магазина нужно хранить список товаров — для таких задач и придуман массив. Объявление Синтаксис для создания нового массива — квадратные скобки со списком элементов внутри. Пустой массив:

var arr = []; Массив fruits с тремя элементами:

var fruits = ["Яблоко", "Апельсин", "Слива"]; Элементы нумеруются, начиная с нуля. Чтобы получить нужный элемент из массива — указывается его номер в квадратных скобках:

1 var fruits = ["Яблоко", "Апельсин", "Слива"]; 2 3 alert(fruits[0]); // Яблоко 4 alert(fruits[1]); // Апельсин 5 alert(fruits[2]); // Слива Элемент можно всегда заменить:

fruits[2] = 'Груша'; // теперь ["Яблоко", "Апельсин", "Груша"]


… Или добавить:

fruits[3] = 'Лимон'; // теперь ["Яблоко", "Апельсин", "Груша", "Лимон"] Общее число элементов, хранимых в массиве, содержится в его свойстве length:

1 var fruits = ["Яблоко", "Апельсин", "Груша"]; 2 3 alert(fruits.length); // 3 Через alert можно вывести и массив целиком. При этом его элементы будут перечислены через запятую:

1 var fruits = ["Яблоко", "Апельсин", "Груша"]; 2 3 alert(fruits); // Яблоко,Апельсин,Груша В массиве может храниться любое число элементов любого типа. В том числе, строки, числа, объекты и т.п.:

1 // микс значений 2 var arr = [ 1, 'Имя', { name: 'Петя' }, true ]; 3 4 // получить объект из массива и тут же -- его свойство 5 alert( arr[2].name ); // Петя 5

Как получить последний элемент из произвольного массива? У нас есть массив goods. Сколько в нем элементов — не знаем, но можем прочитать из goods.length. Напишите код для получения последнего элемента goods. Решение Последний элемент имеет индекс на 1 меньший, чем длина массива. Например:

var fruits = ["Яблоко", "Груша", "Слива"]; Длина массива этого массива fruits.length равна 3. Здесь «Яблоко» имеет индекс0, «Груша» — индекс 1, «Слива» — индекс 2. То есть, для массива длины goods:


var lastItem = goods[goods.length-1]; // получить последний элемент [Открыть задачу в новом окне] 5

Как добавить элемент в конец произвольного массива? У нас есть массив goods. Напишите код для добавления в его конец значения «Компьютер». Решение Текущий последний элемент имеет индекс goods.length-1. Значит, индексом нового элемента будет goods.length:

goods[goods.length] = 'Компьютер' [Открыть задачу в новом окне] Метод ы pop/push, shift/unshift Одно из применений массива — это очередь. В классическом программировании так называют упорядоченную коллекцию элементов, такую что элементы добавляются в конец, а обрабатываются — с начала. В реальной жизни эта структура данных встречается очень часто. Например, очередь сообщений, которые надо отослать. Очень близка к очереди еще одна структура данных: стек. Это такая коллекция элементов, в которой новые элементы добавляются в конец и берутся с конца. Например, стеком является колода карт, в которую новые карты кладутся сверху, и берутся — тоже сверху. Для того, чтобы реализовывать эти структуры данных, и просто для более удобной работы с началом и концом массива существуют специальные методы. Конец массива pop Удаляет последний элемент из массива и возвращает его:

1 var fruits = ["Яблоко", "Апельсин", "Груша"]; 2 3 alert( fruits.pop() ); // удалили "Груша" 4


5 alert(fruits); // Яблоко, Апельсин push Добавляет элемент в конец массива:

1 var fruits = ["Яблоко", "Апельсин"]; 2 3 fruits.push("Груша"); 4 5 alert(fruits); // Яблоко, Апельсин, Груша Является полным аналогом fruits[fruits.length] = .... Начало массива shift Удаляет из массива первый элемент и возвращает его:

1 var fruits = ["Яблоко", "Апельсин", "Груша"]; 2 3 alert( fruits.shift() ); // удалили Яблоко 4 5 alert(fruits); // Апельсин, Груша unshift Добавляет элемент в начало массива:

1 var fruits = ["Апельсин", "Груша"]; 2 3 fruits.unshift('Яблоко'); 4 5 alert(fruits); // Яблоко, Апельсин, Груша

Методы push и unshift могут добавлять сразу по несколько элементов:

1 var fruits = ["Яблоко"]; 2 3 fruits.push("Апельсин", "Персик"); 4 fruits.unshift("Ананас", "Лимон"); 5 6 // результат: ["Ананас", "Лимон", "Яблоко", "Апельсин", "Персик"] 7 alert(fruits); 5


Задача из 5 шагов-строк:

1. Создайте массив styles с элементами «Джаз», «Блюз». 2. Добавьте в конец значение «Рок-н-Ролл» 3. Замените предпоследнее значение с конца на «Классика». Код замены предпоследнего значения должен работать для массивов любой длины.

4. Удалите первое значение массива и выведите его alert. 5. Добавьте в начало значения «Рэп» и «Регги». Массив в результате каждого шага:

1 Джаз, Блюз 2 Джаз, Блюз, Рок-н-Ролл 3 Джаз, Классика, Рок-н-Ролл 4 Классика, Рок-н-Ролл 5 Рэп, Регги, Классика, Рок-н-Ролл Решение

1 var styles = ["Джаз", "Блюз"]; 2 styles.push("Рок-н-Ролл"); 3 styles[styles.length-2] = "Классика"; 4 alert( styles.shift() ); 5 styles.unshift( "Рэп", "Регги "); [Открыть задачу в новом окне] 3

Напишите код для вывода alert случайного значения из массива:

var arr = ["Яблоко", "Апельсин", "Груша", "Лимон"]; P.S. Код для генерации случайного целого от min to max включительно:

var rand = min + Math.floor( Math.random() * (max+1-min) ); Решение Для вывода нужен случайный номер от 0 до arr.length-1 включительно.

1 var arr = ["Яблоко", "Апельсин", "Груша", "Лимон"]; 2 3 var rand = Math.floor( Math.random() * arr.length );


4 5 alert(arr[rand]); [Открыть задачу в новом окне] 4

Напишите код, который:

• Запрашивает по очереди значения при помощи prompt и сохраняет их в массиве. • Заканчивает ввод, как только посетитель введёт пустую строку, не число или нажмёт «Отмена». • Выводит сумму всех значений массива На демо-страничке tutorial/intro/array/calculator.html показан результат. Решение Решение: tutorial/intro/array/calculator.html Перед prompt стоит + для преобразования к числу, иначе калькулятор будет складывать строки. [Открыть задачу в новом окне] Внутреннее устройство массива Массив — это объект, где в качестве ключей выбраны цифры, с дополнительными методами и свойством length. Так как это объект, то в функцию он передаётся по ссылке:

01 function eat(arr) { 02 arr.pop(); 03 } 04 05 var arr = ["нам", "не", "страшен", "серый", "волк"] 06 07 alert(arr.length); // 5 08 eat(arr); 09 eat(arr); alert(arr.length); // 3, в функцию массив не скопирован, а передана 10 ссылка Ещё одно следствие — можно присваивать в массив любые свойства.


Например:

1 var fruits = []; // создать массив 2 3 fruits[99999] = 5; // присвоить свойство с любым номером 4 5 fruits.age = 25; // назначить свойство со строковым именем .. Но массивы для того и придуманы в JavaScript, чтобы удобно работать именно с упорядоченными, нумерованными данными. Для этого в них существуют специальные методы и свойство length. Как правило, нет причин использовать массив как обычный объект, хотя технически это и возможно.

Вывод массива с «дырами» Если в массиве есть пропущенные индексы, то при выводе в большинстве браузеров появляются «лишние» запятые, например:

1 var a = []; 2 a[0] = 0; 3 a[5] = 5; 4 5 alert(a); // 0,,,,,5 Эти запятые появляются потому, что алгоритм вывода массива идёт от 0 доarr.length и выводит всё через запятую. Отсутствие значений даёт несколько запятых подряд. Влияние на быстродействие Методы push/pop выполняются быстро, а shift/unshift — медленно. Чтобы понять, почему работать с концом массива -0 быстрее, чем с его началом, разберём происходящее подробнее. Операция shift выполняет два действия: 1. Удалить элемент в начале.

2. Обновить внутреннее свойство length. При этом, так как все элементы находятся в своих ячейках, просто очистить ячейку с номером 0недостаточно. Нужно еще и переместить все ячейки на 1 вниз (красным на рисунке подсвечены изменения):


fruits.shift(); // убрать 1 элемент с начала

Чем больше элементов в массиве, тем дольше их перемещать. Аналогично работает unshift: чтобы добавить элемент в начало массива, нужно сначала перенести все существующие. У методов push/pop таких проблем нет. Для того, чтобы удалить элемент, метод pop очищает ячейку и укорачивает length.

fruits.pop();

// убрать 1 элемент с конца

Аналогично работает push. Перебор элементов Для перебора элементов обычно используется цикл:

1 var arr = ["Яблоко", "Апельсин", "Груша"]; 2 3 for (var i=0; i<arr.length; i++) { 4 alert( arr[i] ); 5}


Не используйте for..in для массивов Так как массив является объектом, то возможен и вариант for..in:

1 var arr = ["Яблоко", "Апельсин", "Груша"]; 2 3 for (var key in arr) { 4 alert( arr[key] ); // Яблоко, Апельсин, Груша 5} Недостатки этого способа:

• Цикл for..in выведет все свойства объекта, а не только цифровые. В браузере, при работе с объектами страницы, встречаются коллекции элементов, которые по виду как массивы, но имеют дополнительные нецифровые свойства, которые будут видны в цикле for..in. Бывают и библиотеки, которые предоставляют такие коллекции. Например jQuery. С виду — массив, но есть дополнительные свойства. Для перебора только цифровых свойств нужен цикл for(var i=0; i<arr.length...)

• Цикл for (var i=0; i<arr.length; i++) в современных браузерах выполняется в 10-100 раз быстрее. Казалось бы, по виду он сложнее, но браузер особым образом оптимизирует такие циклы.

Если кратко: цикл for(var i=0; i<arr.length...) надёжнее и быстрее. Особенности работ ы length Встроенные методы для работы с массивом автоматически обновляют его длину length. Длина length — не количество элементов массива, а последний индекс + 1. Так уж оно устроено. Это легко увидеть на следующем примере:

1 var arr = []; 2 arr[1000] = true; 3 4 alert(arr.length); // 1001


Вообще, если у вас элементы массива нумеруются случайно или с большими пропусками, то стоит подумать о том, чтобы использовать обычный объект. Массивы предназначены именно для работы с непрерывной упорядоченной коллекцией элементов. Используем length для укорачивания массива Обычно нам не нужно самостоятельно менять length… Но есть один фокус, который можно провернуть. При уменьшении length массив укорачивается. Причем этот процесс необратимый, т.е. даже если потом вернуть length обратно — значения не восстановятся:

1 var arr = [1, 2, 3, 4, 5]; 2 3 arr.length = 2; // укоротить до 2 элементов 4 alert(arr); // [1, 2] 5 6 arr.length = 5; // вернуть length обратно, как было 7 alert(arr[3]); // undefined: значения не вернулись Самый простой способ очистить массив — это arr.length=0. Создание в ызовом new Array new Array() Существует еще один синтаксис для создания массива:

var arr = new Array("Яблоко", "Груша", "и т.п."); Он редко используется, т.к. квадратные скобки [] короче. Кроме того, у него есть одна особенность. Обычно new Array(элементы, ...) создаёт массив из данных элементов, но если у него один аргумент, и это число —то он создает массив без элементов, но с заданной длиной.

1 var arr = new Array(2,3); // создает массив [2, 3] 2 3 arr = new Array(2); // создаст массив [2] ? 4 5 alert(arr[0]); // нет! у нас массив без элементов, длины 2 Что же такое этот «массив без элементов, но с длиной»? Как такое возможно? Оказывается, очень даже возможно и соответствует объекту {length: 2}. Получившийся массив ведёт себя так, как будто его элементы равны undefined.


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

1 var matrix = [ 2 [1, 2, 3], 3 [4, 5, 6], 4 [7, 8, 9] 5 ]; 6 7 alert(matrix[1][1]); // центральный элемент Внутреннее представление массивов

Hardcore coders only Эта секция относится ко внутреннему устройству структуры данных и требует специальных знаний. Она не обязательна к прочтению.

Числовые массивы, согласно спецификации, являются объектами, в которые добавили ряд свойств, методов и автоматическую длину length. Но внутри они, как правило, устроены по-другому. Современные интерпретаторы стараются оптимизировать их и хранить в памяти не в виде хэштаблицы, а в виде непрерывной области памяти, так же как в языке C. Операции с массивами также оптимизируются, особенно если массив хранит только один тип данных, например только числа. Порождаемый набор инструкций для процессора получается очень эффективным. Как правило, у интерпретатора это получается, но при этом программист не должен мешать. В частности:

• Не ставить массиву произвольные свойства, такие как arr.test = 5. То есть, работать именно как с массивом, а не как с объектом.

• Заполнять массив непрерывно. Как только браузер встречает необычное поведение массива, например устанавливается значение arr[0], а потом сразу arr[1000], то он начинает работать с ним, как с обычным объектом. Как правило, это влечёт преобразование его в хэш-таблицу. Если следовать этим принципам, то массивы будут занимать меньше памяти и быстрее работать.


Итого Массивы существуют для работы с упорядоченным набором элементов. Объявление:

1 // предпочтительное 2 var arr = [ элемент1, элемент2... ]; 3 4 // new Array 5 var arr = new Array( элемент1, элемент2...); При этом new Array(число) создаёт массив заданной длины, без элементов. Чтобы избежать ошибок, предпочтителен первый синтаксис. Свойство length — длина массива. Если точнее, то последний индекс массива плюс 1. Если её уменьшить вручную, то массив укоротится. Если length больше реального количества элементов, то отсутствующие элементы равны undefined. Массив можно использовать как очередь или стек. Операции с концом массива:

• arr.push(элемент1, элемент2...) добавляет элементы в конец. • var elem = arr.pop() удаляет и возвращает последний элемент. Операции с началом массива:

• arr.unshift(элемент1, элемент2...) добавляет элементы в начало. • var elem = arr.shift() удаляет и возвращает первый элемент. Эти операции перенумеровывают все элементы, поэтому работают медленно. В следующей главе мы рассмотрим другие методы для работы с массивами. 3

Что выведет этот код?

1 var arr = [1,2,3]; 2 3 var a = arr; 4 a[0] = 5; 5 6 alert(arr[0]); 7 alert(a[0]); Решение


1 var arr = [1,2,3]; 2 3 var a = arr; // (*) 4 a[0] = 5; 5 6 alert(arr[0]); 7 alert(a[0]); Код выведет 5 в обоих случаях, так как массив является объектом. В строке (*) в переменную a копируется ссылка на него, а сам объект в памяти по-прежнему один, в нём отражаются изменения, внесенные через a или arr. Если нужно именно скопировать массив, то это можно сделать, например, так:

var a = []; for(var i=0; i<arr.length; i++) a[i] = arr[i]; [Открыть задачу в новом окне] 3

Создайте функцию find(arr, value), которая ищет в массиве arrзначение value и возвращает его номер, если найдено, или -1, если не найдено. Например:

1 arr = [ "test", 2, 1.5, false ]; 2 3 find(arr, "test"); // 0 4 find(arr, 2); // 1 5 find(arr, 1.5); // 2 6 7 find(arr, 0); // -1 Решение Возможное решение:

1 function find(array, value) { 2 3 for(var i=0; i<array.length; i++) { 4 if (array[i] == value) return i; 5 } 6


7 return -1; 8} Однако, в нем ошибка, т.к. сравнение == не различает 0 и false. Поэтому лучше использовать ===. Кроме того, в современном стандарте JavaScript существует встроенная фунция Array#indexOf, которая работает именно таким образом. Имеет смысл ей воспользоваться, если браузер ее поддерживает.

01 function find(array, value) { 02 if (array.indexOf) { // если метод существует 03 return array.indexOf(value); 04 } 05 06 for(var i=0; i<array.length; i++) { 07 if (array[i] === value) return i; 08 } 09 10 return -1; 11 } 12 13 var arr = ["a", -1, 2, "b"]; 14 15 var index = find(arr, 2); 16 17 alert(index); … Но еще лучшим вариантом было бы определить find по-разному в зависимости от поддержки браузером метода indexOf:

01 // создаем пустой массив и проверяем поддерживается ли indexOf 02 if ( [].indexOf ) { 03 04 var find = function(array, value) { 05 return array.indexOf(value); 06 } 07 08 } else { 09 var find = function(array, value) { 10 for(var i=0; i<array.length; i++) { 11 if (array[i] === value) return i; 12 }


13 14 return -1; 15 } 16 17 } Этот способ - лучше всего, т.к. не требует при каждом запуске find проверять поддержку indexOf. [Открыть задачу в новом окне] 3

Создайте фунцию filterRange(arr, a, b), которая принимает массив чисел arr и возвращает новый массив, который содержит только числа из arr из диапазона от a до b. То есть, проверка имеет вид a ≤ arr[i] ≤ b. Функция не должна менять arr. Пример работы:

1 var arr = [5, 4, 3, 8, 0]; 2 3 var filtered = filterRange(arr, 3, 5); 4 // теперь filtered = [5, 4, 3] 5 // arr не изменился Решение Решение, шаг 1 Алгоритм решения:

1. Создайте временный пустой массив var results = []. 2. Пройдите по элементам arr в цикле и заполните его. 3. Возвратите results. Решение, шаг 2 Код: tutorial/intro/array/filterRange.html. [Открыть задачу в новом окне] 3

Целое число, большее 1, называется простым, если оно не делится нацело ни на какое другое, кроме себя и 1.


Древний алгоритм «Решето Эратосфена» для поиска всех простых чисел до nвыглядит так:

1. Создать список последовательных чисел от 2 до n: 2, 3, 4, ..., n. 2. Пусть p=2, это первое простое число. 3. Зачеркнуть все последующие числа в списке с разницей в p, т.е. 2p, 3p, 4p и т.д. В случае p=2 это будут 4,6,8....

4. Поменять значение p на первое незачеркнутое число после p. 5. Повторить шаги 3-4 пока p2 < n. 6. Все оставшиеся незачеркнутыми числа - простые. Посмотрите также анимацию алгоритма. Реализуйте «Решето Эратосфена» в JavaScript. Найдите все простые числа до 100 и выведите их сумму. Решение Их сумма равна 1060. Решение: tutorial/intro/array/sieve.html. [Открыть задачу в новом окне] 2

На входе массив чисел, например: arr = [1, -2, 3, 4, -9, 6]. Задача — найти непрерывный подмассив arr, сумма элементов которого максимальна. Ваша функция должна возвращать только эту сумму. Например:

1 getMaxSubSum([-1, 2, 3, -9]) = 5 (сумма выделенных) 2 getMaxSubSum([2, -1, 2, 3, -9]) = 6 3 getMaxSubSum([-1, 2, 3, -9, 11]) = 11 4 getMaxSubSum([-2, -1, 1, 2]) = 3 5 getMaxSubSum([100, -9, 2, -3, 5]) = 100 6 getMaxSubSum([1, 2, 3]) = 6 (неотрицательные - берем всех)


Если все элементы отрицательные, то не берём ни одного элемента и считаем сумму равной нулю:

getMaxSubSum([-1, -2, -3]) = 0 Постарайтесь придумать решение, которое работает за O(n2), а лучше за O(n) операций. Решение Подсказка для O(n2) Можно просто посчитать для каждого элемента массива все суммы, которые с него начинаются. Например, для [-1, 2, 3, -9, 11]:

01 // Начиная с -1: 02 -1 03 -1 + 2 04 -1 + 2 + 3 05 -1 + 2 + 3 + (-9) 06 -1 + 2 + 3 + (-9) + 11 07 08 // Начиная с 2: 09 2 10 2 + 3 11 2 + 3 + (-9) 12 2 + 3 + (-9) + 11 13 14 // Начиная с 3: 15 3 16 3 + (-9) 17 3 + (-9) + 11 18 19 // Начиная с -9 20 -9 21 -9 + 11 22 23 // Начиная с -11 24 -11


Сделайте вложенный цикл, который на внешнем уровне бегает по элементам массива, а на внутреннем — формирует все суммы элементов, которые начинаются с текущей позиции. Решение для O(n2) Решение через вложенный цикл:

01 function getMaxSubSum(arr) { 02 var maxSum = 0; // если совсем не брать элементов, то сумма 0 03 04 for(var i=0; i<arr.length; i++) { 05 var sumFixedStart = 0; 06 for(var j=i; j<arr.length; j++) { 07 sumFixedStart += arr[j]; 08 maxSum = Math.max(maxSum, sumFixedStart); 09 } 10 } 11 12 return maxSum; 13 } 14 15 alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5 16 alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11 17 alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3 18 alert( getMaxSubSum([1, 2, 3]) ); // 6 19 alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100 Алгоритм для O(n) Будем идти по массиву и накапливать в некоторой переменной s текущую частичную сумму. Если в какой-то момент s окажется отрицательной, то мы просто присвоимs=0. Утверждается, что максимум из всех значений переменной s, случившихся за время работы, и будет ответом на задачу. Докажем этот алгоритм. В самом деле, рассмотрим первый момент времени, когда сумма s стала отрицательной. Это означает, что, стартовав с нулевой частичной суммы, мы в итоге пришли к отрицательной частичной сумме — значит, и весь этот префикс массива, равно как и любой его суффикс имеют отрицательную сумму.


Следовательно, от всего этого префикса массива в дальнейшем не может быть никакой пользы: он может дать только отрицательную прибавку к ответу. Решение за O(n)

01 function getMaxSubSum(arr) { 02 var maxSum = 0, partialSum = 0; 03 for (var i=0; i<arr.length; i++) { 04 partialSum += arr[i]; 05 maxSum = Math.max(maxSum, partialSum); 06 if (partialSum < 0) partialSum = 0; 07 } 08 return maxSum; 09 } 10 11 12 alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5 13 alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11 14 alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3 15 alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100 16 alert( getMaxSubSum([1, 2, 3]) ); // 6 17 alert( getMaxSubSum([-1, -2, -3]) ); // 0 Информацию об алгоритме вы также можете прочитать здесь: http://emaxx.ru/algo/maximum_average_segment и здесь: Maximum subarray problem. [Открыть задачу в новом окне] Массивы: методы

1. 2. 3. 4. 5. 6. 7.

Object.keys(obj)

Метод split Метод join Удаление из массива Метод splice Метод slice Сортировка, метод sort(fn) 1. Свой порядок сортировки 8. reverse 9. concat 10.indexOf/lastIndexOf 11.Итого В этой главе мы рассмотрим встроенные методы массивов JavaScript.


Object.keys(obj) В JavaScript есть встроенный метод Object.keys(obj), который возвращает ключи объекта в виде массива. Он поддерживается везде, кроме IE<9:

1 var user = { 2 name: "Петя", 3 age: 30 4} 5 6 var keys = Object.keys(user); 7 8 alert(keys); // name, age В более старых браузерах аналогом будет цикл:

var keys = []; for(var key in user) keys.push(key); 3 Пусть strings — массив строк. Напишите функцию unique(strings), которая возвращает массив, содержащий только уникальные элементы arr. Например:

1 function unique(arr) { 2 /* ваш код */ 3} 4 5 var strings = ["кришна", "кришна", "харе", "харе", 6 "харе", "харе", "кришна", "кришна", "8-()"]; 7 8 alert( unique(strings) ); // кришна, харе, 8-() Решение Решение перебором (медленное) Пройдём по массиву вложенным циклом. Для каждого элемента мы будем искать, был ли такой уже. Если был — игнорировать:

01 function unique(arr) {


02 03 04 05 06

var obj = {}; var result = [];

nextInput: for(var i=0; i<arr.length; i++) { var str = arr[i]; // для каждого 07 элемента for(var j=0; j<result.length; j++) { // ищем, был ли он 08 уже? if (result[j] == str) continue nextInput; // если да, то 09 следующий 10 } 11 result.push(str); 12 } 13 14 return result; 15 } 16 17 var strings = ["кришна", "кришна", "харе", "харе", 18 "харе", "харе", "кришна", "кришна", "8-()"]; 19 20 alert( unique(strings) ); // кришна, харе, 8-() Давайте посмотрим, насколько быстро он будет работать. Предположим, в массиве 100 элементов. Если все они одинаковые, то result будет состоять из одного элемента и вложенный цикл будет выполняться сразу. В этом случае всё хорошо. А если все, или почти все элементы разные? В этом случае для каждого элемента понадобится обойти весь текущий массив результатов, после чего — добавить в этот массив.

1. Для первого элемента — это обойдётся в 0 операций доступа к элементамresult (он пока пустой).

2. Для второго элемента — это обойдётся в 1 операцию доступа к элементамresult.

3. Для третьего элемента — это обойдётся в 2 операции доступа к элементамresult.

4. …Для n-го элемента — это обойдётся в n-1 операций доступа к элементамresult.


Всего 0 + 1 + 2 + ... + n-1 = (n-1)*n/2 = n 2/2 - n/2 (как сумма арифметической прогрессии), то есть количество операций растёт примерно как квадрат от n. Это очень быстрый рост. Для 100 элементов — 4950 операций, для 1000 —499500 (по формуле выше). Поэтому такое решение подойдёт только для небольших массивов. Вместо вложенного for можно использовать и arr.indexOf, ситуация от этого не поменяется, так как indexOf тоже ищет перебором. Решение с объектом (быстрое) Наилучшая техника для выбора уникальных строк — использование вспомогательного объекта. Ведь название свойства в объекте, с одной стороны — строка, а с другой — всегда уникально. Повторная запись в свойство с тем же именем перезапишет его. Например, если "харе" попало в объект один раз (obj["харе"] = true), то второе такое же присваивание ничего не изменит. Решение ниже создаёт объект obj = {} и записывает в него все строки как имена свойств. А затем собирает свойства из объекта в массив через for..in. Дубликатов уже не будет.

01 function unique(arr) { 02 var obj = {}; 03 04 for(var i=0; i<arr.length; i++) { 05 var str = arr[i]; 06 obj[str] = true; // запомнить строку в виде свойства объекта 07 } 08 return Object.keys(obj); // или собрать ключи перебором для 09 IE<9 10 } 11 12 var strings = ["кришна", "кришна", "харе", "харе", 13 "харе", "харе", "кришна", "кришна", "8-()"]; 14 15 alert( unique(strings) ); // кришна, харе, 8-()


Так что можно положить все значения как ключи в объект, а потом достать. [Открыть задачу в новом окне] Метод split Ситуация из реальной жизни. Мы пишем сервис отсылки сообщений и посетитель вводит имена тех, кому его отправить: Маша, Петя, Марина, Василий.... Но нам-то гораздо удобнее работать с массивом имен, чем с одной строкой. К счастью, есть метод split(s), который позволяет превратить строку в массив, разбив ее по разделителю s. В примере ниже таким разделителем является строка из запятой и пробела.

1 var names = 'Маша, Петя, Марина, Василий'; 2 3 var arr = names.split(', '); 4 5 for (var i=0; i<arr.length; i++) { 6 alert('Вам сообщение ' + arr[i]); 7}

Второй аргумент split У метода split есть необязательный второй аргумент — ограничение на количество элементов в массиве. Если их больше, чем указано — остаток массива будет отброшен:

1 alert( "a,b,c,d".split(',', 2) ); // a,b

На практике он используется редко.

Разбивка по буквам Вызов str.split('') разобьёт строку на буквы:

1 var str = "тест"; 2


3

alert( str.split('') ); // т,е,с,т

Метод join

Вызов arr.join(str) делает в точности противоположное split. Он берет массив и склеивает его в строку, используя str как разделитель. Например:

1 var arr = ['Маша', 'Петя', 'Марина', 'Василий']; 2 3 var str = arr.join(';'); 4 5 alert(str); // Маша;Петя;Марина;Василий

new Array + join = Повторение строки Код для повторения строки 3 раза:

1 alert( new Array(4).join("ля") ); // ляляля Как видно, new Array(4) делает массив без элементов длины 4, который joinобъединяет в строку, вставляя между его элементами строку "ля". В результате, так как элементы пусты, получается повторение строки. Такой вот небольшой трюк. 5

В объекте есть свойство className, которое содержит список «классов» - слов, разделенных пробелом:

var obj = { className: 'open menu' }


Создайте функцию addClass(obj, cls), которая добавляет в список класс cls, но только если его там еще нет:

1 addClass(obj, 'new'); // obj.className='open menu new' 2 addClass(obj, 'open'); // без изменений (класс уже существует) 3 addClass(obj, 'me'); // obj.className='open menu new me' 4 5 alert(obj.className); // "open menu new me" P.S. Ваша функция не должна добавлять лишних пробелов. Решение Решение заключается в превращении obj.className в массив при помощи split. После этого в нем можно проверить наличие класса, и если нет - добавить.

01 function addClass(obj, cls) { 02 var classes = obj.className ? obj.className.split(' ') : []; 03 04 for(var i=0; i<classes.length; i++) { 05 if (classes[i] == cls) return; // класс уже есть 06 } 07 08 classes.push(cls); // добавить 09 10 obj.className = classes.join(' '); // и обновить свойство 11 } 12 13 var obj = { className: 'open menu' }; 14 15 addClass(obj, 'new'); 16 addClass(obj, 'open'); 17 addClass(obj, 'me'); 18 alert(obj.className) // open menu new me P.S. «Альтернативный» подход к проверке наличия класса вызовомobj.className.indexOf(cls) был бы неверным. В частности, он найдётcls = "menu" в строке классов obj.className = "open mymenu".


P.P.S. Проверьте, нет ли в вашем решении присвоенияobj.className += " " + cls. Не добавляет ли оно лишний пробел в случае, если изначально obj.className = ""? [Открыть задачу в новом окне] 3

Напишите функцию camelize(str), которая преобразует строки вида «myshort-string» в «myShortString». То есть, дефисы удаляются, а все слова после них получают заглавную букву. Например:

camelize("background-color") == 'backgroundColor'; camelize("list-style-image") == 'listStyleImage'; Такая функция полезна при работе с CSS. P.S. Вам пригодятся методы строк charAt, split и toUpperCase. Решение Идея Задача может быть решена несколькими способами. Один из них — разбить строку по дефису str.split('-'), затем последовательно сконструировать новую. Решение Разобьем строку в массив, а затем преобразуем его элементы и сольём обратно:

01 function camelize(str) { 02 var arr = str.split('-'); 03 04 for(var i=1; i<arr.length; i++) { 05 // преобразовать: первый символ с большой буквы 06 arr[i] = arr[i].charAt(0).toUpperCase() + arr[i].slice(1); 07 } 08 09 return arr.join(''); 10 }


Демо: tutorial/intro/array/camelize.html. [Открыть задачу в новом окне] Удаление из массива Так как массивы являются объектами, то для удаления ключа можно воспользоваться обычнымdelete:

1 var arr = ["Я", "иду", "домой"]; 2 3 delete arr[1]; // значение с индексом 1 удалено 4 5 // теперь arr = ["Я", undefined, "домой"]; 6 alert(arr[1]); // undefined Да, элемент удален из массива, но не так, как нам этого хочется. Образовалась «дырка». Это потому, что оператор delete удаляет пару «ключ-значение». Это - все, что он делает. Обычно же при удалении из массива мы хотим, чтобы оставшиеся элементы сдвинулись и заполнили образовавшийся промежуток. Поэтому для удаления используются специальные методы - из начала shift, из конца - pop, а из середины - splice, с которым мы сейчас познакомимся. Метод splice Метод splice - это универсальный раскладной нож для работы с массивами. Умеет все: удалять элементы, вставлять элементы, заменять элементы - по очереди и одновременно. Его синтаксис: arr.splice(index[, deleteCount, elem1, ..., elemN]) Удалить deleteCount элементов, начиная с номера index, а затем вставитьelem1, ..., elemN на их место. Посмотрим примеры.

1 var arr = ["Я", "изучаю", "JavaScript"]; 2 3 arr.splice(1, 1); // начиная с позиции 1, удалить 1 элемент 4 5 alert(arr); // осталось ["Я", "JavaScript"] Здесь продемонстрировано, как использовать splice для удаления одного элемента**. Следующие за удаленным элементы сдвигаются, чтобы заполнить его место.


1 var arr = ["Я", "изучаю", "JavaScript"]; 2 3 arr.splice(0, 1); // удалить 1 элемент, начиная с позиции 0 4 5 alert( arr[0] ); // "изучаю" стал первым элементом Следующий пример показывает, как заменять элементы:

1 var arr = ["Я", "сейчас", "изучаю", "JavaScript"]; 2 3 // удалить 3 первых элемента и добавить другие вместо них 4 arr.splice(0, 3, "Мы", "изучаем") 5 6 alert( arr ) // теперь ["Мы", "изучаем", "JavaScript"] Метод splice возвращает массив из удаленных элементов:

1 var arr = ["Я", "сейчас", "изучаю", "JavaScript"]; 2 3 // удалить 2 первых элемента 4 var removed = arr.splice(0, 2); 5 6 alert( removed ); // "Я", "сейчас" <-- array of removed elements Метод splice также может вставлять элементы без удаления, для этого достаточно установитьdeleteCount в 0:

1 var arr = ["Я", "изучаю", "JavaScript"]; 2 3 // с позиции 2 4 // удалить 0 5 // вставить "сложный", "язык" 6 arr.splice(2, 0, "сложный", "язык"); 7 8 alert(arr); // "Я", "изучаю", "сложный", "язык", "JavaScript" Допускается использование отрицательного номера позиции, которая в этом случае отсчитывается с конца:

1 var arr = [1, 2, 5] 2 3 // начиная с позиции индексом -1 (предпоследний элемент) 4 // удалить 0 элементов, 5 // затем вставить числа 3 и 4


6 arr.splice(-1, 0, 3, 4); 7 8 alert(arr); // результат: 1,2,3,4,5 5 У объекта есть свойство className, которое хранит список «классов» - слов, разделенных пробелами:

var obj = { className: 'open menu' } Напишите функцию removeClass(obj, cls), которая удаляет класс cls, если он есть:

removeClass(obj, 'open'); // obj.className='menu' removeClass(obj, 'blabla'); // без изменений (нет такого класса) P.S. Дополнительное усложнение. Функция должна корректно обрабатывать дублирование класса в строке:

obj = { className: 'my menu menu' }; removeClass(obj, 'menu'); alert(obj.className); // 'my' Лишних пробелов после функции образовываться не должно. Решение Решение заключается в том, чтобы разбить className в массив классов, а затем пройтись по нему циклом. Если класс есть - удаляем его splice, заново объединяем массив в строку и присваиваем объекту.

01 function removeClass(obj, cls) { 02 var classes = obj.className.split(' '); 03 04 for(i=0; i<classes.length; i++) { 05 if (classes[i] == cls) { 06 classes.splice(i, 1); // удалить класс 07 i--; // (*) 08 } 09 }


10 obj.className = classes.join(' '); 11 12 } 13 14 var obj = { className: 'open menu menu' } 15 16 removeClass(obj, 'blabla'); 17 removeClass(obj, 'menu') 18 alert(obj.className) // open В примере выше есть тонкий момент. Элементы массива проверяются один за другим. При вызове splice удаляется текущий, i-й элемент, и те элементы, которые идут дальше, сдвигаются на его место. Таким образом, на месте i оказывается новый, непроверенный элемент. Чтобы это учесть, строчка (*) уменьшает i, чтобы следующая итерация цикла заново проверила элемент с номером i. Без нее функция будет работать с ошибками. [Открыть задачу в новом окне] 4

Создайте функцию filterRangeInPlace(arr, a, b), которая получает массив с числами arr и удаляет из него все числа вне диапазона a..b. То есть, проверка имеет вид a ≤ arr[i] ≤ b. Функция должна менять сам массив и ничего не возвращать. Например:

1 arr = [5, 3, 8, 1]; 2 3 filterRangeInPlace(arr, 1, 4); // удалены числа вне диапазона 1..4 4 5 alert(arr); // массив изменился: остались [3, 1] Решение tutorial/intro/array/filterRangeInPlace.html. [Открыть задачу в новом окне] Метод slice Метод slice(begin, end) копирует участок массива от begin до end, не включая end. Исходный массив при этом не меняется.


Например:

1 var arr = ["Почему", "надо", "учить", "JavaScript"]; 2 3 var arr2 = arr.slice(1,3); // элементы 1, 2 (не включая 3) 4 5 alert(arr2); // надо, учить Аргументы ведут себя так же, как и в строковом slice:

• Если не указать end — копирование будет до конца массива: 1 var arr = ["Почему", "надо", "учить", "JavaScript"]; 2 3 alert( arr.slice(1) ); // взять все элементы, начиная с номера 1 • Можно использовать отрицательные индексы, они отсчитываются с конца:

var arr2 = arr.slice(-2); // копировать от 2го элемента с конца и дальше • Если вообще не указать аргументов — скопируется весь массив:

var fullCopy = arr.slice(); Сортировка, метод sort(fn) Метод sort() сортирует массив на месте. Например:

1 var arr = [ 1, 2, 15 ]; 2 3 arr.sort(); 4 5 alert( arr ); // 1, 15, 2 Не заметили ничего странного в этом примере? Порядок стал 1, 15, 2. Это произошло потому, что sort сортирует, преобразуя элементы к строке. Поэтому и порядок у них строковый, ведь "2" > "15". Свой порядок сортировки Внутренняя реализация метода arr.sort(fn) умеет сортировать любые массивы, если указать функцию fn от двух элементов, которая умеет сравнивать их. Если эту функцию не указать, то элементы сортируются как строки. Например, укажем эту функцию явно, отсортируем элементы массива как числа:

01 function compareNumeric(a, b) {


02 if (a > b) return 1; 03 if (a < b) return -1; 04 } 05 06 var arr = [ 1, 2, 15 ]; 07 08 arr.sort(compareNumeric); 09 10 alert(arr); // 1, 2, 15 Обратите внимание, мы передаём в sort() именно саму функцию compareNumeric, без вызова через скобки. Был бы ошибкой следующий код:

arr.sort( compareNumeric() );

// не сработает

К функции, передаваемой sort, есть всего одно требование. Алгоритм сортировки, встроенный в JavaScript, будет передавать ей для сравнения элементы массива. Она должна возвращать:

• Положительное значение, если a > b, • Отрицательное значение, если a < b, • Если равны — не важно, что возвращать, их взаимный порядок не имеет значения.

Алгоритм сортировки В методе sort, внутри самого интерпретатора JavaScript, реализован универсальный алгоритм сортировки. Как правило, это "быстрая сортировка", дополнительно оптимизированная для небольших массивов. Ему совершенно неважно, что сортировать — строки, числа, яблоки с апельсинами или посетителей. Это и не должно быть важно, алгоритм сортировки просто сортирует абстрактные «элементы массива». Всё, что ему нужно о них знать — как эти элементы сравнивать между собой. Для этого мы передаём в sort функцию сравнения. А там уже алгоритм решает, что с чем сравнивать, чтобы отсортировать побыстрее. Кстати, те значения, с которыми sort вызывает функцию сравнения, можно увидеть, если вставить в неё alert:

1 [1, -2, 15, 2, 0, 8].sort(function(a, b) { 2 alert(a + " <> " + b);


3

});

Функцию compareNumeric для сравнения элементов-чисел можно упростить до одной строчки. Как? Показать простой вариант `compareNumeric` 5

Как отсортировать массив чисел в обратном порядке?

1 var arr = [ 5, 2, 1, -10, 8]; 2 3 // отсортируйте? 4 5 alert(arr); // 8, 5, 2, 1, -10 Решение

1 var arr = [ 5, 2, 1, -10, 8]; 2 3 function compareReversed(a, b) { 4 return b - a; 5} 6 7 arr.sort(compareReversed); 8 9 alert(arr); [Открыть задачу в новом окне] 5

Есть массив строк arr. Создайте массив arrSorted — из тех же элементов, но отсортированный. Исходный массив не должен меняться.

1 var arr = [ "HTML", "JavaScript", "CSS" ]; 2 3 // ... ваш код ... 4


5 alert(arrSorted); // CSS, HTML, JavaScript 6 alert(arr); // HTML, JavaScript, CSS (без изменений)

Постарайтесь сделать код как можно короче.

Решение Для копирования массива используем slice(), и тут же — сортировку:

1 var arr = [ "HTML", "JavaScript", "CSS" ]; 2 3 var arrSorted = arr.slice().sort(); 4 5 alert(arrSorted); 6 alert(arr); [Открыть задачу в новом окне] 3

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

1 var arr = [1, 2, 3, 4, 5]; 2 3 arr.sort(ваша функция); 4 5 alert(arr); // элементы в случайном порядке, например [3,5,1,2,4] Решение Решение, шаг 1 Функция сортировки должна возвращать случайный результат сравнения. Используйте для этого Math.random. Решение, шаг 2 Решение: Обычно Math.random() возвращает результат от 0 до 1. Вычтем 0.5, чтобы область значений стала [-0.5 ... 0.5).


1 var arr = [1, 2, 3, 4, 5]; 2 3 function compareRandom(a, b) { 4 return Math.random() - 0.5; 5} 6 7 arr.sort(compareRandom); 8 9 alert(arr); // элементы в случайном порядке, например [3,5,1,2,4] [Открыть задачу в новом окне] 5

Напишите код, который отсортирует массив объектов people по полюage. Например:

01 var vasya = { name: "Вася", age: 23 }; 02 var masha = { name: "Маша", age: 18 }; 03 var vovochka = { name: "Вовочка", age: 6 }; 04 05 var people = [ vasya , masha , vovochka ]; 06 07 ... ваш код ... 08 09 // теперь people: [vovochka, masha, vasya] 10 alert(people[0].age) // 6 Выведите список имён в массиве после сортировки. Решение Для сортировки объявим передадим в sort анонимную функцию, которая сравнивает объекты по полю age:

01 // Наша функция сравнения 02 function compareAge(personA, personB) { 03 return personA.age - personB.age; 04 } 05 06 // проверка 07 var vasya = { name: "Вася", age: 23 };


08 var masha = { name: "Маша", age: 18 }; 09 var vovochka = { name: "Вовочка", age: 6 }; 10 11 var people = [ vasya , masha , vovochka ]; 12 13 people.sort(compareAge); 14 15 // вывести 16 for(var i=0; i<people.length; i++) { 17 alert(people[i].name); // Вовочка Маша Вася 18 } [Открыть задачу в новом окне] reverse Метод arr.reverse() меняет порядок элементов в массиве на обратный.

1 var arr = [1,2,3]; 2 arr.reverse(); 3 4 alert(arr); // 3,2,1 5 Односвязный список — это структура данных, которая состоит изэлементов, каждый из которых хранит ссылку на следующий. Последний элемент может не иметь ссылки, либо она равна null. Например, объект ниже задаёт односвязный список, в next хранится ссылка на следующий элемент:

01 var list = { 02 value: 1, 03 next: { 04 value: 2, 05 next: { 06 value: 3, 07 next: { 08 value: 4, 09 next: null 10 } 11 } 12 }


13 }; Графическое представление этого списка: Альтернативный способ создания:

1 var list = { value: 1 }; 2 list.next = { value: 2 }; 3 list.next.next = { value: 3 }; 4 list.next.next.next = { value: 4 }; Такая структура данных интересна тем, что можно очень быстро разбить список на части, объединить списки, удалить или добавить элемент в любое место, включая начало. При использовании массива такие действия требуют обширных перенумерований. Задачи:

1. Напишите функцию printList(list), которая выводит элементы списка по очереди.

2. Напишите функцию printReverseList(list), которая выводит элементы списка в обратном порядке, используя рекурсию. Для списка выше она должна выводить 4,3,2,1.

3. Напишите printReverseList(list) без использования рекурсии. Какой вариант быстрее? Почему? Решение Вывод списка

01 var list = { 02 value: 1, next: { 03 value: 2, next: { 04 value: 3, next: { 05 value: 4, next: null 06 } 07 } 08 } 09 }; 10 11 function printList(list) { 12 var tmp = list; 13


14 while(tmp) { 15 alert( tmp.value ); 16 tmp = tmp.next; 17 } 18 19 } 20 21 printList(list); Обратите внимание, что для прохода по списку используется временная переменная tmp, а не list. Можно было бы и бегать по списку, используя входной параметр функции:

1 function printList(list) { 2 3 while(list) { 4 alert( list.value ); 5 list = list.next; 6 } 7 8} …Но при этом мы в будущем не сможем расширить функцию и сделать со списком что-то ещё, ведь после окончания цикла начало списка уже нигде не хранится. Поэтому и используется временная переменная — чтобы сделать код расширяемым, и, кстати, более понятным, ведь роль tmp — исключительно обход списка, как i в цикле for. Обратный вывод с рекурсией

01 var list = { 02 value: 1, next: { 03 value: 2, next: { 04 value: 3, next: { 05 value: 4, next: null 06 } 07 } 08 } 09 }; 10 11 function printReverseList(list) {


12 13 if (list.next) { 14 printReverseList(list.next); 15 } 16 17 alert(list.value); 18 } 19 20 printReverseList(list); Обратный вывод без рекурсии

01 var list = { 02 value: 1, next: { 03 value: 2, next: { 04 value: 3, next: { 05 value: 4, next: null 06 } 07 } 08 } 09 }; 10 11 12 function printReverseList(list) { 13 var arr = []; 14 var tmp = list; 15 16 while(tmp) { 17 arr.push(tmp.value); 18 tmp = tmp.next; 19 } 20 21 for( var i = arr.length-1; i>=0; i-- ) { 22 alert( arr[i] ); 23 } 24 } 25 26 printReverseList(list); Обратный вывод без рекурсии быстрее.


По сути, рекурсивный вариант и нерекурсивный работают одинаково: они проходят список и запоминают его элементы, а потом выводят в обратном порядке. В случае с массивом это очевидно, а для рекурсии запоминание происходит в стеке (внутренней специальной структуре данных): когда вызывается вложенная функция, то интерпретатор сохраняет в стек текущие параметры. Вложенные вызовы заполняют стек, а потом он выводится в обратном порядке. При этом, при рекурсии в стеке сохраняется не только элемент списка, а другая вспомогательная информация, необходимая для возвращения из вложенного вызова. Поэтому тратится больше памяти. Все эти расходы отсутствуют во варианте без рекурсии, так как в массиве хранится именно то, что нужно. Преимущество рекурсии, с другой стороны — более короткий и, зачастую, более простой код. [Открыть задачу в новом окне] concat Метод arr.concat(value1, value2, … valueN) создаёт новый массив, в который копируются элементы из arr, а также value1, value2, ... valueN. Например:

1 var arr = [1,2]; 2 var newArr = arr.concat(3,4); 3 4 alert(newArr); // 1,2,3,4 Если value — массив, то concat добавляет его элементы. Например:

1 var arr = [1,2]; var newArr = arr.concat( [3,4], 5);// то же самое, что 2 arr.concat(3,4,5) 3 4 alert(newArr); // 1,2,3,4,5 indexOf/lastIndexOf Эти методы не поддерживаются в IE<9. Для их поддержки подключите библиотеку ES5-shim. Метод arr.indexOf(searchElement[, fromIndex]) возвращает номер элемента searchElement в


массиве arr или -1, если его нет. Поиск начинается с номера fromIndex, если он указан. Если нет — с начала массива. Для поиска используется строгое сравнение ===. Например:

1 var arr = [ 1, 0, false ]; 2 3 alert( arr.indexOf(0) ); // 1 4 alert( arr.indexOf(false) ); // 2 5 alert( arr.indexOf(null) ); // -1 Как вы могли заметить, по синтаксису он полностью аналогичен методу indexOf для строк. Метод arr.lastIndexOf(searchElement[, fromIndex]) ищет справа-налево: с конца массива или с номера fromIndex, если он указан.

Методы indexOf/lastIndexOf осуществляют поиск перебором Если нужно проверить, существует ли значение в массиве — его нужно перебрать. Только так. Внутренняя реализация indexOf/lastIndexOf осуществляет полный перебор, аналогичный циклу for по массиву. Чем длиннее массив, тем дольше он будет работать.

Коллекция уникальных элементов Рассмотрим задачу — есть коллекция строк, и нужно быстро проверять: есть ли в ней какой-то элемент. Массив для этого не подходит из-за медленного indexOf. Но подходит объект! Доступ к свойству объекта осуществляется очень быстро, так что можно сделать все элементы ключами объекта и проверять, есть ли уже такой ключ. Например, организуем такую проверку для коллекции строк "div", "a" и "form":

1 var store = { }; // объект для коллекции 2 3 var items = ["div", "a", "form"]; 4 5 for(var i=0; i<items.length; i++) { 6 var key = items[i]; // для каждого элемента создаём свойство 7 store[ key ] = true; // значение здесь не важно


8}

Теперь для проверки, есть ли ключ key, достаточно выполнить if (store[key]). Если есть — можно использовать значение, если нет — добавить.

Такое решение работает только со строками, но применимо к любым элементам, для которых можно вычислить строковый «уникальный ключ».

4

Анаграммы — слова, состоящие из одинакового количества одинаковых букв, но в разном порядке. Например:

воз - зов киборг - гробик корсет - костер - сектор Напишите функцию aclean(arr), которая возвращает массив слов, очищенный от анаграмм. Например:

var arr = ["воз", "киборг", "корсет", "ЗОВ", "гробик", "костер","сектор"]; alert( aclean(arr) ); // "воз,киборг,корсет" или "ЗОВ,гробик,сектор" Из каждой группы анаграмм должно остаться только одно слово, не важно какое. Решение Чтобы обнаружить анаграммы, разобьём каждое слово на буквы и отсортируем их. В отсортированном по буквам виде все анаграммы одинаковы. Например:

воз, зов -> взо киборг, гробик -> бгикор ...


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

01 function aclean(arr) { 02 // этот объект будем использовать для уникальности 03 var obj = {}; 04 05 for(var i=0; i<arr.length; i++) { 06 // разбить строку на буквы, отсортировать и слить обратно var sorted = 07 arr[i].toLowerCase().split('').sort().join('');// (*) 08 obj[sorted] = arr[i]; // сохраняет только одно значение с 09 таким ключом 10 } 11 12 var result = []; 13 // теперь в obj находится для каждого ключа ровно одно 14 значение 15 for(var key in obj) result.push(obj[key]); 16 17 return result; 18 } 19 var arr = 20 ["воз", "киборг", "корсет", "ЗОВ", "гробик", "костер","сектор"]; 21 22 alert( aclean(arr) ); Приведение слова к сортированному по буквам виду осуществляется цепочкой вызовов в строке (*). Для удобства комментирования разобьём её на несколько строк (JavaScript это позволяет):

1 var sorted = arr[i] 2 .toLowerCase(); 3 .split('') 4 .sort() 5 .join('');

// // // // //

ЗОВ зов ['з','о','в'] ['в','з','о'] взо


Получится, что два разных слова 'ЗОВ' и 'воз' получат одинаковую отсортированную форму 'взо'. Следующая строка:

obj[sorted] = arr[i]; В объект obj будет записано сначала первое из слов obj['взо'] = "воз", а затемobj['взо'] = 'ЗОВ'. Обратите внимание, ключ — отсортирован, а само слово — в исходной форме, чтобы можно было потом получить его из объекта. Вторая запись по тому же ключу перезапишет первую, то есть в объекте останется ровно одно слово с таким набором букв. [Открыть задачу в новом окне] Итого Методы:

• push/pop, shift/unshift, splice — для добавления и удаления элементов. • join/split — для преобразования строки в массив и обратно. • sort — для сортировки массива. Если не передать функцию сравнения — сортирует элементы как строки.

• reverse — меняет порядок элементов на обратный. • concat — объединяет массивы. • indexOf/lastIndexOf — возвращают позицию элемента в массиве (не поддерживается в IE<9). Изученных нами методов достаточно в 95% случаях, но существуют и другие. Для знакомства с ними рекомендуется заглянуть в справочник Array и Array в Mozilla Developer Network. Дата и Время

1. Создание 2. Получение компонент даты 3. Установка компонент даты 1. Автоисправление даты 2. Преобразование к числу, разность дат 3. Бенчмаркинг 4. Форматирование 1. Разбор строки, Date.parse 5. Итого Для работы с датой и временем в JavaScript используются объекты Date.


Создание Для создания нового объекта типа Date используется один из синтаксисов: new Date() Создает объект Date с текущей датой и временем:

1 var now = new Date(); 2 alert(now); new Date(milliseconds) Создает объект Date, значение которого равно количеству миллисекунд (1/1000 секунды), прошедших с 1 января 1970 года GMT+0.

1 // 24 часа после 01.01.1970 GMT+0 2 var Jan02_1970 = new Date(3600*24*1000); 3 alert( Jan02_1970 ); new Date(datestring) Если единственный аргумент - строка, используется вызов Date.parse для ее разбора. new Date(year, month, date, hours, minutes, seconds, ms) Дату можно создать, используя компоненты в местной временной зоне. Для этого формата обязательны только первые два аргумента. Отсутствующие параметры, начиная с hoursсчитаются равными нулю, а date — единице. Заметим, что год year должен быть из 4 цифр, а отсчет месяцев month начинается с нуля 0. Например:

new Date(2011, 0, 1) // 1 января 2011, 00:00:00 в местной временной зоне new Date(2011, 0) // то же самое, date по умолчанию равно 1 new Date(2011, 0, 1, 0, 0, 0, 0); // то же самое Дата задана с точностью до миллисекунд:

1 var d = new Date(2011, 0, 1, 2, 3, 4, 567); 2 alert(d); // 1.01.2011, 02:03:04.567 5 Создайте объект Date для даты: 20 февраля 2012 года, 3 часа 12 минут. Временная зона — местная. Выведите его на экран. Решение Дата в местной временной зоне создается при помощи new Date.


Месяцы начинаются с нуля, так что февраль имеет номер 1. Параметры можно указывать с точностью до минут:

1 var d = new Date(2012, 1, 20, 3, 12); 2 alert(d); [Открыть задачу в новом окне] Получение компонент дат ы Для доступа к компонентам даты-времени объекта Date используются следующие методы: getFullYear() Получить год(из 4 цифр) getMonth() Получить месяц, от 0 до 11. getDate() Получить число месяца, от 1 до 31. getHours(), getMinutes(), getSeconds(), getMilliseconds() Получить соответствующие компоненты.

Устаревший getYear() Некоторые браузеры реализуют нестандартный метод getYear(). Где-то он возвращает только две цифры из года, где-то четыре. Так или иначе, этот метод отсутствует в стандарте JavaScript. Не используйте его. Для получения года естьgetFullYear().

Дополнительно можно получить день недели: getDay() Получить номер дня в неделе. Неделя в JavaScript начинается с воскресенья, так что результат будет числом от 0(воскресенье) до 6(суббота). Все методы, указанные выше, возвращают результат для местной временной зоны. Существуют также UTC-варианты этих методов, возвращающие день, месяц, год и т.п. для зоны GMT+0 (UTC): getUTCFullYear(), getUTCMonth(), getUTCDay(). То есть, сразу после "get"вставляется "UTC". Если ваше локальное время сдвинуто относительно UTC, то следующий код покажет разные часы:


1 var date = new Date(); 2 3 alert( date.getHours() ); // час в вашей зоне для даты date 4 alert( date.getUTCHours() ); // час в зоне GMT+0 для даты date Кроме описанных выше, существуют два специальных метода без UTC-варианта: getTime() Возвращает число миллисекунд, прошедших с 01.01.1970 00:00:00 UTC. Это то же число, которое используется в конструкторе new Date(milliseconds). getTimezoneOffset() Возвращает разницу между местным и UTC-временем, в минутах.

1 alert( new Date().getTimezoneOffset() );

// Для GMT-1 выведет 60

5 Создайте функцию getWeekDay(date), которая выводит текущий день недели в коротком формате ‘пн’, ‘вт’, … ‘вс’. Например:

var date = new Date(2012,0,3); alert( getWeekDay(date) );

// 3 января 2012 // Должно вывести 'вт'

Решение Метод getDay() позволяет получить номер дня недели, начиная с воскресенья. Запишем имена дней недели в массив, чтобы можно было их достать по номеру:

1 function getWeekDay(date) { 2 var days = ['вс','пн','вт','ср','чт','пт','сб'] ; 3 4 return days[ date.getDay() ]; 5} 6 7 var date = new Date(2012,0,3); // 3 января 2012 8 alert( getWeekDay(date) ); // 'вт' [Открыть задачу в новом окне] 5


Напишите функцию, getLocalDay(date) которая возвращает день недели для даты date. День нужно возвратить в европейской нумерации, т.е. понедельник имеет номер 1, вторник номер 2, …, воскресенье - номер 7.

var date = new Date(2012, 0, 3); // 3 янв 2012 alert( getLocalDay(date) ); // вторник, выведет 2 Решение Решение - в использовании встроенной функции getDay. Она полностью подходит нашим целям, но для воскресенья возвращает 0 вместо 7:

01 function getLocalDay(date) { 02 03 var day = date.getDay(); 04 05 if ( day == 0 ) { // день 0 становится 7 06 day = 7; 07 } 08 09 return day; 10 } 11 12 alert( getLocalDay(new Date(2012,0,3)) ); // 2 Если удобнее, чтобы день недели начинался с нуля, то можно возвращать в функцииday - 1, тогда дни будут от 0 (пн) до 6(вс). [Открыть задачу в новом окне] Установка компонент дат ы Следующие методы позволяют устанавливать компоненты даты и времени:

• • • • • • • •

setFullYear(year [, month, date]) setMonth(month [, date]) setDate(date) setHours(hour [, min, sec, ms]) setMinutes(min [, sec, ms]) setSeconds(sec [, ms]) setMilliseconds(ms) setTime(milliseconds) (устанавливает всю дату по миллисекундам с 01.01.1970 UTC)


Все они, кроме setTime(), обладают также UTC-вариантом, например: setUTCHours(). Как видно, некоторые методы могут устанавливать несколько компонент даты одновременно, в частности, setHours. При этом если какая-то компонента не указана, она не меняется. Например:

1 var today = new Date; 2 3 today.setHours(0); 4 alert( today ); // сегодня, но час изменён на 0 5 6 today.setHours(0, 0, 0, 0); 7 alert (today ); // сегодня, ровно 00:00:00. Автоисправление даты Автоисправление — очень удобное свойство объектов Date. Оно заключается в том, что можно устанавливать заведомо некорректные компоненты (например 32 января), а объект сам себя поправит.

1 var d = new Date(2013, 0, 32); // 32 января 2013 ?!? 2 alert(d); // ... это 1 февраля 2013! Неправильные компоненты даты автоматически распределяются по остальным. Например, нужно увеличить на 2 дня дату «28 февраля 2011». Может быть так, что это будет 2 марта, а может быть и 1 марта, если год високосный. Но нам обо всем этом думать не нужно. Просто прибавляем два дня. Остальное сделает Date:

1 var d = new Date(2011, 1, 28); 2 d.setDate( d.getDate() + 2 ); 3 4 alert(d); // 2 марта, 2011 Также это используют для получения даты, отдаленной от имеющейся на нужный промежуток времени. Например, получим дату на 70 секунд большую текущей:

1 var d = new Date(); 2 d.setSeconds( d.getSeconds()+70); 3 4 alert(d); // выведет корректную дату Можно установить и нулевые, и даже отрицательные компоненты. Например:

1 var d = new Date;


2 3 d.setDate(1); // поставить первое число месяца 4 alert(d); 5 d.setDate(0); // нулевого числа нет, будет последнее число предыдущего 6 месяца 7 alert(d); 1 var d = new Date; 2 3 d.setDate(-1); // предпоследнее число предыдущего месяца 4 alert(d); 4

Какое число месяца было 100 дней назад? Какой день недели? Используйте JavaScript, чтобы вывести эту информацию. День недели выводите двухбуквенном виде, т.е. одно значение из (пн, вт, ср, …,вс). Решение Создадим текущую дату и вычтем 100 дней:

1 var d = new Date; 2 d.setDate( d.getDate() - 100 ); 3 4 alert( d.getDate() ); 5 6 var dayNames = ['вс','пн','вт','ср','чт','пт','сб']; 7 alert( dayNames[d.getDay()] ); Объект Date авто-исправит себя и выдаст правильный результат. Обратите внимание на массив с именами дней недели. «Нулевой» день — воскресенье. [Открыть задачу в новом окне] 5

Напишите функцию getLastDayInMonth(year, month), которая возвращает последний день месяца. Параметры:


• year — 4-значный год, например 2012. • month — месяц от 0 до 11. Например, getLastDayInMonth(2012, 1) = 29 (високосный год, февраль). Решение Создадим дату из следующего месяца, но день не первый, а «нулевой» (т.е. предыдущий):

1 function getLastDayOfMonth(year, month) { 2 var date = new Date(year, month+1, 0); 3 return date.getDate(); 4} 5 6 alert( getLastDayOfMonth(2012, 1) ); // 29 [Открыть задачу в новом окне] Преобразование к числу, разность дат Когда объект Date используется в числовом контексте, он преобразуется в количество миллисекунд:

1 alert( +new Date ) // +date то же самое, что: +date.valueOf() Важный побочный эффект: даты можно вычитать, результат вычитания объектов Date — их временная разница, в миллисекундах. Это используют для измерения времени:

01 var start = new Date; // засекли время 02 03 // что-то сделать 04 for (var i=0; i<100000; i++) { 05 var doSomething = i*i*i; 06 } 07 08 var end = new Date; // конец измерения 09 10 alert("Цикл занял " + (end-start) + " ms"); 5


Напишите код, который выводит: 1. Сколько секунд прошло с начала сегодняшнего дня. 2. Сколько осталось до конца дня. Скрипт должен работать в любой день, т.е. в нём не должно быть конкретного значения сегодняшней даты. Решение Первая часть. Для вывода достаточно сгенерировать date, соответствующий началу дня, т.е. «сегодня» 00 часов 00 минут 00 секунд. Разница между текущей датой и началом дня — это количество миллисекунд от начала дня. Его можно легко перевести в секунды:

1 var now = new Date(); 2 3 // создать объект из текущей даты, без часов-минут-секунд var today = new Date(now.getFullYear(), now.getMonth(), 4 now.getDate()); 5 6 var diff = now - today; // разница в миллисекундах 7 alert( Math.round(diff / 1000) ); // перевести в секунды Вторая часть Для получения оставшихся до конца дня секунд нужно из «завтра 00ч 00мин 00сек» вычесть текущее время. Чтобы сгенерировать «завтра», нужно увеличить текущий день на 1:

1 var now = new Date(); 2 3 // создать объект из даты, без часов-минут-секунд var tomorrow = new Date(now.getFullYear(), 4 now.getMonth(),now.getDate()+1); 5 6 var diff = tomorrow - now; // разница в миллисекундах 7 alert( Math.round(diff / 1000) ); // перевести в секунды


[Открыть задачу в новом окне] Бенчмаркинг Допустим, у нас есть несколько вариантов решения задачи, каждый описан функцией. Как узнать, какой быстрее? Для примера возьмем две функции, которые округляют число:

1 function 2 return 3} 4 5 function return 6 число 7}

floorMath(x) { Math.floor(x);

floorXor(x) { x^0; // побитовое исключающее ИЛИ (XOR) всегда округляет

Чтобы померять, какая из них быстрее, нельзя запустить один раз floorMath, один раз floorXor и замерить разницу. Одноразовый запуск ненадежен, любая мини-помеха исказит результат. Для правильного бенчмаркинга функция запускается много раз, чтобы сам тест занял существенное время. Это сведет влияние помех к минимуму. Сложную функцию можно запускать 100 раз, простую — 1000 раз… Померяем, какая из функций округления быстрее:

01 function floorMath(x) { return Math.floor(x); } 02 function floorXor(x) { return x^0; } 03 04 function bench(f) { 05 var d = new Date(); 06 for (var i=0.5; i<1000000; i++) f(i); 07 return new Date() - d; 08 } 09 10 alert('Время floorMath: ' + bench(floorMath) + 'мс'); 11 alert('Время floorXor: ' + bench(floorXor) + 'мс'); В зависимости от браузера, может быть быстрее как floorXor так и floorMath. Многократное повторение функции — обязательно. Иначе измерения сильно подвержены помехам. Ещё более точные результаты можно получить,


если сам пакет тестов, т.е два теста в данном случае, тоже прогоняется много раз. Форматирование Встроенные в Date методы форматирования используются редко, и преимущественно, для отладки. toString(), toDateString(), toTimeString() Возвращают стандартное строчное представление, не указанное в стандарте, а зависящее от браузера. Единственное требование - читаемость человеком. Метод toString возвращает дату целиком, toDateString() и toTimeString() - только дату и время соответственно.

1 var d = new Date(); 2 alert( d.toString() ); // вывод, похожий на 'Wed Jan 26 2011 3 16:40:50 GMT+0300' toLocaleString(), toLocaleDateString(), toLocaleTimeString() То же самое, но строка должна быть с учетом локальных настроек и языка посетителя.

1 var d = new Date(); 2 3 alert( d.toLocaleString() ); // дата на языке посетителя toUTCString() То же самое, что toString(), но дата в зоне UTC. toISOString() Возвращает дату в формате ISO Детали формата будут далее. Поддерживается современными браузерами, не поддерживается IE<9.

1 var d = new Date(); 2 alert( d.toISOString() ); // вывод, похожий на '2011-013 26T13:51:50.417Z' Встроенные методы форматирования Date не допускают указание собственного формата. Поэтому, как правило, любой вывод, кроме отладочного, форматируется своей, а не встроенной функцией. 3

Напишите функцию formatDate(date), которая выводит дату date в формате дд.мм.гг:


Например:

var d = new Date(2011, 0, 30); // 30 января 2011 alert( formatDate(d) ); // '30.01.11' P.S. Обратите внимание, ведущие нули должны присутствовать, то есть 1 января 2011 должно быть 01.01.11, а не 1.1.11. Решение Получим компоненты один за другим.

1. День можно получить как date.getDate(). При неободимости добавим ведущий ноль:

var dd = date.getDate(); if (dd<10) dd= '0'+dd; 2. date.getMonth() возвратит месяц, начиная с нуля. Увеличим его на 1: var mm = date.getMonth() + 1; if (mm<10) mm= '0'+mm;

// месяц 1-12

3. date.getFullYear() вернет год в 4-значном формате. Чтобы сделать его двузначным - воспользуемся оператором взятия остатка '%':

var yy = date.getFullYear() % 100; if (yy<10) yy= '0'+yy; Заметим, что год, как и другие компоненты, может понадобиться дополнить нулем слева, причем возможно что yy == 0 (например, 2000 год). При сложении со строкой 0+'0' == '00', так что будет все в порядке. Полный код:

01 function formatDate(date) { 02 03 var dd = date.getDate() 04 if ( dd < 10 ) dd = '0' + dd; 05 06 var mm = date.getMonth()+1 07 if ( mm < 10 ) mm = '0' + mm; 08 09 var yy = date.getFullYear() % 100;


10 if ( yy < 10 ) yy = '0' + yy; 11 12 return dd+'.'+mm+'.'+yy; 13 } 14 15 var d = new Date(2011, 0, 30); // 30 Jan 2011 16 alert( formatDate(d) ); // '30.01.11' [Открыть задачу в новом окне] 4

Напишите функцию formatDate(date), которая форматирует датуdate так:

• Если со времени date прошло менее секунды, то возвращает "только что".

• Иначе если со времени date прошло менее минуты, то "n сек. назад". • Иначе если прошло меньше часа, то "m мин. назад". • Иначе полная дата в формате "дд.мм.гг чч:мм". Например:

1 function formatDate(date) { /* ваш код */ } 2 3 alert( formatDate( new Date(new Date - 1) ) ); // "только что" 4 alert( formatDate( new Date(new Date - 30*1000) ) ); // "30 сек. 5 назад" 6 alert( formatDate( new Date(new Date- 5*60*1000) ) ); // "5 мин. 7 назад" 8 Решение Для того, чтобы узнать время от date до текущего момента - используем вычитание дат.

01 function formatDate(date) { 02 var diff = new Date() - date; // разница в миллисекундах 03 04 if (diff < 1000) { // прошло менее 1 секунды 05 return 'только что';


06 07 08 09 10 11 12 13

} var sec = Math.floor( diff / 1000 ); // округлить diff до секунд if (sec < 60) { return sec + ' сек. назад'; }

var min = Math.floor( diff / 60000 ); // округлить diff до минут 15 if (min < 60) { 16 return min + ' мин. назад'; 17 } 18 // форматировать дату, с учетом того, что месяцы начинаются с 19 0 20 var d = date; d = ['0'+d.getDate(),'0'+(d.getMonth() 21 +1),''+d.getFullYear(),'0'+d.getHours(),'0'+d.getMinutes() ]; 22 for(var i=0; i<d.length; i++) { 23 d[i] = d[i].slice(-2); 24 } 25 26 return d.slice(0,3).join('.')+' '+d.slice(3).join(':'); 27 } 28 29 alert( formatDate( new Date( new Date - 1) ) ); // только что 30 alert( formatDate( new Date( new Date - 30*1000) ) ); // 30 сек. 31 назад 32 alert( formatDate( new Date( new Date- 5*60*1000) ) ); // 5 мин. 33 назад 34 14

[Открыть задачу в новом окне] Разбор строки, Date.parse Все современные браузеры, включая IE9+, понимают даты в упрощённом формате ISO 8601 Extended. Этот формат выглядит так: YYYY-MM-DDTHH:mm:ss.sssZ. Для разделения даты и времени в нем


используется символ 'T'. Часть 'Z' обозначает (необязательную) временную зону — она может отсутствовать, тогда зона UTC, либо может быть символ z — тоже UTC, или зона в формате +hh:mm. Также возможны упрощенные варианты, к примеру:

YYYY YYYY-MM YYYY-MM-DD Метод Date.parse(str) разбирает строку str в таком формате и возвращает соответствующее ей количество миллисекунд. Если это невозможно, Date.parse возвращает NaN. На момент написания некоторые браузеры (Safari) воспринимали формат без 'Z' как дату в локальной таймзоне (по стандарту UTC), поэтому пример ниже в них работает некорректно:

1

var msNoZone = Date.parse('2012-01-26T13:51:50.417'); // без зоны, значит UTC

2 3 alert(msNoZone); // 1327571510417 (число миллисекунд) 4 var msZ = Date.parse('2012-01-26T13:51:50.417z'); // зона z означает 5 UTC 6 alert(msZ == msNoZone); // true, если браузер правильный С таймзоной -07:00 GMT в конце все современные браузеры работают правильно:

1 var ms = Date.parse('2012-01-26T13:51:50.417-07:00'); 2 3 alert(ms); // 1327611110417 (число миллисекунд)

Формат дат для IE8До появления спецификации EcmaScript 5 формат не был стандартизован, и браузеры, включая IE8-, имели свои собственные форматы дат. Частично, эти форматы пересекаются. Например, код ниже работает везде, включая старые IE:

1 var ms = Date.parse("January 26, 2011 13:51:50"); 2 3 alert(ms); Вы также можете почитать о старых форматах IE в документации к методу MSDN Date.parse.


Конечно же, сейчас лучше использовать современный формат, если цель — современные браузеры, а если дополнительно нужны IE8-, то либо передавать даты через миллисекунды, а не строки, либо добавить библиотеку типа es5shim, которая добавит Date.parse в старые IE. Итого

• Дата и время представлены в JavaScript одним объектом: Date. Создать «только время» при этом нельзя, оно должно быть с датой. Список методов Date вы можете найти в справочникеDate или выше.

• Объект Date удобен тем, что автокорректируется. Благодаря этому легко сдвигать даты. • Объекты Date можно вычитать, результатом будет разница в мс. Преобразование типов

1. Строковое преобразование 2. Численное преобразование 1. Специальные значения 3. Логическое преобразование 4. Итого Система преобразования типов в JavaScript очень проста, но отличается от других языков. Поэтому она часто служит «камнем преткновения» для приходящих из других языков программистов. Всего есть три преобразования: 1. Cтроковое преобразование. 2. Числовое преобразование. 3. Преобразование к логическому значению. Строковое преобразование Строковое преобразование происходит, когда требуется представление чего-либо в виде строки. Например, его производит функция alert.

1 var a = true; 2 3 alert(a); // "true" Можно также осуществить преобразование явным вызовом String(val):

1 alert( String(null) === "null" ); // true Также для явного преобразования применяется оператор "+", у которого один из аргументов строка. В этом случае он приводит к строке и другой аргумент, например:

1 alert( true + "test" ); // "truetest" 2 alert( "123" + undefined); // "123undefined"


Численное преобразование Численное преобразование происходит в математических функциях и выражениях, а также при сравнении данных различных типов (кроме сравнений ===, !==). Для преобразования к числу в явном виде можно вызвать Number(val), либо, что короче, поставить перед выражением оператор "+":

var a = +"\n123\n"; // 123 var a = Number("\n123\n"); // 123, тот же эффект Значение Преобразуется в… undefined NaN null

0

true / false

1 / 0

Строка

Пробелы по краям обрезаются. Далее, если остаётся пустая строка, то 0. Из непустой строки «считывается» число, при ошибке результат: NaN.

Примеры: • Логические значения:

1 alert( +true ); // 1 2 alert( +false); // 0 • Сравнение разных типов — значит численное преобразование:

1 alert( "\n0\n" == 0 ); // true При этом строка "\n0\n" преобразуется к числу — начальные и конечные пробелы игнорируются, получается 0. • Ещё пример:

1 alert( "\n" == false ); Здесь сравнение == снова приводит обе части к числу. И слева и справа получается 0. По аналогичной причине верно равенство "1" == true. Специальные значения Посмотрим на поведение специальных значений более внимательно. Интуитивно, значения null/undefined ассоциируются с нулём, но при преобразованиях ведут себя иначе. Специальные значения преобразуются к числу так:


Значение Преобразуется в… undefine NaN d null

0

Это преобразование осуществляется при арифметических операциях и сравнениях > >= < <=, но не при проверке равенства ==. Алгоритм проверки равенства для этих значений в спецификации прописан отдельно (пункт 11.9.3). В нём считается, что null и undefined равны "==" между собой, но эти значения не равны никакому другому значению. Это ведёт к забавным последствиям. Например, null не подчиняется законам математики — он «больше либо равен нулю»: null>=0, но не больше и не равен:

1 alert(null >= 0); // true, т.к. null преобразуется к 0 2 alert(null > 0); // false (не больше), т.к. null преобразуется к 0 alert(null == 0 ); // false (и не равен!), т.к. == рассматривает null 3 особо. Значение undefined вообще вне сравнений:

1 alert(undefined > 0); // false, т.к. undefined -> NaN alert(undefined == 0); // false, т.к. это undefined (без 2 преобразования) 3 alert(undefined < 0); // false, т.к. undefined -> NaN Для более очевидной работы кода и во избежание ошибок лучше не давать специальным значениям участвовать в сравнениях > >= < <=. Используйте в таких случаях переменные-числа или приводите к числу явно. Логическое преобразование Преобразование к true/false происходит в логическом контексте, таком как if(obj), while(obj) и при применении логических операторов. Все значения, которые интуитивно «пусты», становятся false. Их несколько: 0, пустая строка, null,undefined и NaN. Остальное, в том числе и любые объекты — true. Полная таблица преобразований:

Значение

Преобразуется в… undefined, nul false


l Числа

Все true, кроме 0, NaN — false.

Строки

Все true, кроме пустой строки "" — false

Объекты

Всегда true

Для явного преобразования используется двойное логическое отрицание !!value или вызовBoolean(value).

Обратите внимание: строка "0" становится true В отличие от многих языков программирования (например PHP), "0" в JavaScript является true, как и строка из пробелов:

1 alert( !!"0" ); // true alert( !!" " ); // любые непустые строки, даже из пробелов - true!

2

Пустой объект или массив тоже true Также, в отличие от ряда других языков программирования, пустой объект {} или массив [] являются true:

1 if ( {} ) { 2 alert("{} -> true"); 3} 4 5 if ( [] ) { 6 alert("[] -> true"); 7

}

Логическое преобразование интересно тем, как оно сочетается с численным. Два значения могут быть равны, но одно из них в логическом контексте true, другое — false.


Например, равенства в следующем примере верны, так как происходит численное преобразование:

1 alert( 0 == "\n0\n" ); // true 2 alert( false == " " ); // true …А в логическом контексте левая часть даст false, правая — true:

1 if ("\n0\n") { 2 alert("true, совсем не как 0!"); 3} С точки зрения преобразования типов в JavaScript это совершенно нормально. При равенстве — численное преобразование, а в if — логическое, только и всего. Итого В JavaScript есть три преобразования: 1. Строковое: происходит обычно при выводе.

2. Численное: его делают математические операторы и функции, а также сравнения и проверки равенства, кроме строгих === и !==. 3. Логическое: происходит по таблице. Сравнение не осуществляет преобразование типов в следующих случаях: • При сравнении объектов. Две переменные, которые являются объектами равны только, когда ссылаются на один и тот же объект.

• При сравнении двух строк. Там отдельный алгоритм сравнения. А вот если хоть один операнд — не строка, то значения будут приведены: true > "000" станет 1 > 0.

• При проверке равенства с null и undefined. Они равны друг другу, но не равны чему бы то ни было ещё, этот случай прописан особо в спецификации. Код для явного преобразования типов: Преобразование к числу +value или Number(value) Преобразование к строке '' + value или String(value) Преобразование к логическому значению !!value или Boolean(value) 5

Подумайте, какой результат будет у выражений ниже. Тут не только преобразования типов. Когда закончите — сверьтесь с решением.

01 "" + 1 + 0


02 "" - 1 + 0 03 true + false 04 6 / "3" 05 "2" * "3" 06 4 + 5 + "px" 07 "$" + 4 + 5 08 "4" - 2 09 "4px" - 2 10 7 / 0 11 parseInt("09") 12 " -9\n" + 5 13 " -9\n" - 5 14 5 && 2 15 2 && 5 16 5 || 0 17 0 || 5 18 null + 1 19 undefined + 1 Решение

01 "" + 1 + 0 = "10" // (1) 02 "" - 1 + 0 = -1 // (2) 03 true + false = 1 04 6 / "3" = 2 05 "2" * "3" = 6 06 4 + 5 + "px" = "9px" 07 "$" + 4 + 5 = "$45" 08 "4" - 2 = 2 09 "4px" - 2 = NaN 10 7 / 0 = Infinity 11 parseInt("09") = "0" или "9" // (3) 12 " -9\n" + 5 = " -9\n5" 13 " -9\n" - 5 = -14 14 5 && 2 = 2 15 2 && 5 = 5 16 5 || 0 = 5 17 0 || 5 = 5 18 null + 1 = 1 // (4) 19 undefined + 1 = NaN // (5) 1. Оператор "+" в данном случае прибавляет 1 как строку, и затем 0. 2. Оператор "-" работает только с числами, так что он сразу приводит "" к 0.


3. В некоторых браузерах parseInt без второго аргумента интерпретирует 09 как восьмиричное число.

4. null при численном преобразовании становится 0 5. undefined при численном преобразовании становится NaN [Открыть задачу в новом окне] Функция - это значение

1. Функция — это значение 2. Копирование функций В этом разделе мы познакомимся c важными особенностями функций в JavaScript, а также с тремя способами объявить функцию. Функция — это значение В JavaScript функция является значением, таким же как строка или число. Объявление создает функцию и записывает ссылку на неё в переменную. Как и любое значение, функцию можно вывести, вот так:

1 function sayHi() { 2 alert('Привет'); 3} 4 5 alert(sayHi); // выведет код функции Здесь выводится не результат работы функции sayHi() (кстати, чему он равен?.. правильно,undefined, т.к. нет return), а сама функция, т.е. ее код. Функция — не просто значение, это объект. В него можно даже записать свойство:

1 function sayHi() { } 2 3 sayHi.test = 5; 4 5 alert(sayHi.test); // 5 Используется эта возможность достаточно редко. Копирование функций Функцию можно скопировать в другую переменную.


1 function sayHi(person) { 2 alert('Привет, ' + person); 3} 4 5 var func = sayHi; 6 7 alert(func); // выведет код функции При этом, так как функция — это объект, то и копируется она «по ссылке». То есть, сама функция лежит где-то в памяти, а переменная содержит «адрес», где она находится. При присваивании func = sayHi копируется этот адрес, обе переменные начинают указывать на одно и то же «место в памяти», где находится функция. После копирования ее можно вызывать:

1 function sayHi(person) { 2 alert('Привет, ' + person); 3} 4 5 var func = sayHi; 6 7 func('Вася'); // выведет 'Привет, Вася' 8 9 sayHi('Маша'); // и так по-прежнему работает: 'Привет, Маша' Заметим ещё раз, что обращение вида alert(func) отличается от alert( func() ). В первом случае выводится функция (её код), во втором — эта функция вызывается, и выведен будет уже её результат. Function Declaration и Function Expression

1. Объявление Function Declaration 1. Время создания Function Declaration 2. Объявление Function Expression 1. Функция с вызовом «на месте» 3. Итого В этом разделе мы познакомимся c важными особенностями функций в JavaScript, а также с тремя способами объявить функцию. Объявление Function Declaration Объявление функции, о котором мы говорили все время до этого, называется в спецификации языкаFunction Declaration. Устоявшегося русского перевода нет, также такой синтаксис называют «стандартным» или «обычным» объявлением функции. Чуть дальше мы посмотрим другие варианты создания


функций. Позвольте еще раз выделить основной смысл объявления Function Declaration:

При объявлении функции создаётся переменная со значениемфункцией Другими словами, объявление function func(параметры) { код } говорит интерпретатору: «создай переменную func, и положи туда функцию с указанными параметрами и кодом». При этом func — на самом деле не «имя функции». Оно совсем никак не привязано к функции! Это название переменной, в которую будет помещена функция, и из которой она может быть затем удалена, скопирована в другую переменную, и в результате её какfunc вызвать будет нельзя:

1 function func() { alert(1); } 2 3 var g = func; // скопировали 4 5 func = null; // поменяли значение 6 7 g(); // работает, теперь функция в g, а в func ничего нет

8

func(); // вызываем null()? ошибка!

В частности, невозможно иметь функцию и переменную с одинаковым именем:

1 // Нонсенс! 2 function f() { } // объявить переменную f и записать в нее функцию var f = 5; // объявить переменную f (а она уже объявлена) и присвоить 3 5 4 5 alert(f); // результат: 5 В примере выше переменная f в первой строке получает значение — функцию, а во второй — число5. В итоге мы получим переменную со значением f=5. Время создания Function Declaration Функции, объявленные как Function Declaration, создаются интерпретатором до выполнения


кода. Перед тем, как выполнять первую строку, интерпретатор сканирует код, ищет в нёмFunction Declaration и обрабатывает их. Поэтому их можно вызвать до объявления, например:

1 sayHi("Вася"); 2 3 function sayHi(name) { 4 alert("Привет, " + name); 5} Этот код будет работать, т.к. объявление function sayHi обрабатывается и функция создаётся до выполнения первой строчки кода.

Условно объявить функцию через Function Declaration нельзя Попробуем, в зависимости от условия, объявить функцию по-разному:

1 var age = 20; 2 3 if (age >= 18) { 4 function sayHi() { 5 } else { 6 function sayHi() { 7} 8 9 sayHi();

alert('Прошу вас!');

}

alert('До 18 нельзя'); }

Какой вызов сработает? Чтобы это понять — вспомним, как работают функции.

1. Function Declaration обрабатываются до выполнения первой строчки кода. Браузер сканирует код и создает из таких объявлений функции. При этом второе объявление перезапишет первое. 2. Дальше, во время выполнения объявления игнорируются (они уже обработаны), как если бы код был таким:


01 function sayHi() { 02 function sayHi() { 03 04 var age = 20; 05 06 if (age >= 18) { 07 08 } else { 09 10 } 11 12 sayHi();

alert('Прошу вас!'); } alert('До 18 нельзя'); }

Как видно, конструкция if здесь ни на что не влияет. По-разному объявить функцию, в зависимости от условия, не получилось. P.S. Это — правильное с точки зрения спецификации поведение. На момент написания этого раздела ему следуют все браузеры, кроме Firefox. Объявление Function Expression Существует альтернативный синтаксис для создания функций, который решает проблемы с «условным» объявлением. Функцию можно создать и присвоить переменной как самое обычное значение. Такое объявление называется Function Expression и выглядит так:

1 var f = function(параметры) { 2 // тело функции 3 }; Например:

1 var sayHi = function(person) { 2 alert("Привет, " + person); 3 }; 4 5 sayHi('Вася'); Такую функцию можно объявить в любом выражении, там где допустимо обычное значение. Например, вполне допустимо такое объявление массива:


1 var arr = [1, 2, function(a) { alert(a) }, 3, 4]; 2 3 var fun = arr[2]; 4 fun(1); // 1 В отличие от объявлений Function Declaration, которые создаются заранее, до выполнения кода, объявления Function Expression создают функцию, когда до них доходит выполнение. Поэтому и пользоваться ими можно только после объявления.

1 sayHi(); // <-- работать не будет, функции еще нет 2 3 var sayHi = function() { alert(1) }; … А вот так — будет:

1 var sayHi = function() { 2 3 sayHi(); // 1

alert(1)

};

Благодаря этому свойству Function Expression можно (и даже нужно) использовать для условного объявления функции:

01 var age = prompt('Сколько вам лет?'); 02 var sayHi; 03 04 if (age >= 18) { 05 sayHi = function() { alert('Вход разрешен'); } 06 } else { 07 sayHi = function() { alert('Извините, вы слишком молоды'); 08 } 09 10 sayHi(); // запустит ту функцию, которая присвоена в if

}

Функция с вызовом «на месте» Представим на минуту, что мы создали скрипт, который делает нечто восхитительноошеломительное со страницей. Для этого он, конечно же, объявляет свои временные переменные и функции. Наш скрипт настолько замечательный, что мы хотим дать его другим людям. В этом случае мы бы хотели, чтобы любой мог вставить скрипт на страницу, и временные переменные и функции скрипта не конфликтовали с теми, которые на ней используются. Например, наш скрипт задаёт переменные a, b, и если на странице они уже используются — будет конфликт.


Чтобы его не было, нашему скрипту нужна своя отдельная область видимости, в которой он будет работать. Для этого используют функции с вызовом «на месте». Такая функция объявляется — и тут же вызывается, вот так:

1 (function() { 2 3 var a = 1 , b = 2; // переменные для нашего скрипта 4 5 // код скрипта 6 7 })(); Теперь внутренние переменные скрипта стали локальными. Даже если в другом месте страницы объявлена своя переменная a — проблем не будет. Наш скрипт теперь имеет свою, изолированную область видимости. Эта практика широко используется в библиотеках JavaScript, чтобы временные переменные и функции, которые используются при инициализации, не конфликтовали с внешним кодом. У функции, которая объявлена таким образом, и тут же вызвана, нет имени. Она никуда не сохраняется, и поэтому не может быть вызвана второй раз. Но в данном случае это и не нужно, ведь задача такой функции обёртки — создать отдельную область видимости для скрипта, что она и сделала.

Зачем скобки вокруг функции? В примерах выше вокруг function() {...} находятся скобки. Если записать без них — такой код вызовет ошибку:

1 function() { 2 // syntax error 3 }(); Эта ошибка произойдет потому, что браузер, видя ключевое слово function в основном потоке кода, попытается прочитать Function Declaration, а здесь даже имени нет. Впрочем, даже если имя поставить, то работать тоже не будет:

1 function work() { 2 // ...


3 }()

// syntax error Дело в том, что «на месте» разрешено вызывать только Function Expression. Общее правило таково:

• Если браузер видит function в основном потоке кода - он считает, что это Function Declaration.

• Если же function идёт в составе более сложного выражения, то он считает, что это Function Expression.

Для этого и нужны скобки - показать, что у нас Function Expression, который по правилам JavaScript можно вызвать «на месте». Можно показать это другим способом, например так:

+function() { ... }()

Или так:

!function() { ... }() Главное, чтобы интерпретатор понял, что это Function Expression, тогда он позволит вызвать функцию «на месте». Скобки не нужны, если это и так Function Expression, например в таком вызове:

1 var res = function(a,b) { return a+b }(2,2); 2 alert(res); // 4 Функция здесь создаётся как часть выражения присваивания, поэтому являетсяFunction Expression и может быть вызвана «на месте».

При вызове «на месте» лучше ставить скобки и для Expression Технически, если функция и так задана как Function Expression, то она может быть вызвана «на месте» без скобок:

var result = function(a,b) {


return a*b; }(2,3); Но из соображений стиля и читаемости скобки вокруг function рекомендуется ставить:

var result = (function(a,b) { return a*b; })(2,3); Тогда сразу видно, что в result записывается не сама функция (result = function...), а идёт «вызов на месте». При чтении такого кода меньше ошибок.

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

1 var sayHi = new Function('name', ' alert("Привет, "+name) '); 2 3 sayHi("Вася"); Этот способ используется очень редко.

Предпочитайте Function Declaration В коде неопытных разработчиков зачастую можно увидеть много функций, объявленных как Function Expression:

... код ... var f = function() { ... } ... То же самое с Function Declaration будет короче и читабельнее:

... код ... function f() { ... } ...


Дополнительный бонус - такие функции можно вызывать до того, как они объявлены. Используйте Function Expression только там, где это действительно нужно. Например, для объявления функции в зависимости от условий. Итого Функции в JavaScript являются значениями. Их можно присваивать, передавать, создавать в любом месте кода.

• Если функция объявлена в основном потоке кода, то это Function Declaration. • Если функция создана как часть выражения, то это Function Expression. Между этими двумя основными способами создания функций есть следующие различия:

Function Declaration

Function Expression

Время создания

До выполнения первой строчки кода.

Когда управление достигает строки с функцией.

Можно вызвать до объявления

Да (т.к. создается заранее)

Нет

Можно объявить в if

Нет (т.к. создается заранее)

Да

Можно вызывать «на месте»

Нет (ограничение синтаксиса JavaScript)

Да

По возможности, рекомендуется использовать Function Declaration, если нет особой причины дляFunction Expression. Именованные функциональные выражения

Обычно у функций в JavaScript нет имени. Как мы говорили ранее, функция является всего лишь значением, которое присваивается переменной. То, что обычно называют «именем функции» — на самом деле, является именем переменной. К самой функции это «имя» никак не привязано. Если функцию переместить в другую переменную — она сменит «имя»:

1 function f() { alert(1); }; 2 g = f; 3 f = 0; 4 5 g(); // сменили имя f на g! Однако, есть в JavaScript расширение Function Expression, позволяющее указать имя, действительно привязанное к функции. Оно называется Named Function Expression (NFE) или,


по-русски, именованное функциональное выражение. Простейший пример NFE выглядит так:

var f = function sayHi(...) { /* тело функции */ }; Проще говоря, NFE — это Function Expression с дополнительным именем (в примере выше sayHi). Что же это за имя, которое идёт в дополнение к переменной f, и зачем оно? Имя функционального выражения (sayHi) имеет особый смысл. Оно доступно только изнутри самой функции. Это ограничение видимости входит в стандарт JavaScript и поддерживается всеми браузерами, кроме IE<9. Например:

1 var f = function sayHi(name) { 2 alert(sayHi); // изнутри функции - видно (выведет код функции) 3 }; 4 alert(sayHi); // снаружи - не видно (ошибка: undefined variable 5 'sayHi') Как правило, оно используется для единственной цели — позволить функции вызвать саму себя.

Устаревшее специальное значение arguments.callee Если вы работали с JavaScript, то, возможно, знаете, что для этой цели также служило специальное значение arguments.callee. Если это название вам ни о чём не говорит — всё в порядке, читайте дальше, мы обязательно обсудим его в отдельной главе. Если же вы в курсе, то стоит иметь в виду, что оно официально исключено из современного стандарта. А NFE — это наше настоящее.

Такое имя жёстко привязано к функции. Оно работает всегда, даже если переменная, в которой изначально содержалась функция, была перезаписана, а сама функция — перемещена в другое место. Например, объявим функцию-факториал из задачи Вычислить факториал как NFE:


var f = function factorial(n) { return n ? n*factorial(n-1) : 1; }; Почему мы решили не использовать Function Declaration, а присвоить её в переменную f? Здесь — для примера, а вообще, причин бывает множество, скажем, условное создание функции внутри if, при котором Function Declaration не работает. После создания функции, даже если переменная f перезаписана, функция сможет обращаться к самой себе через имя factorial:

1 var f = function factorial(n) { 2 return n ? n*factorial(n-1) : 1; 3 }; 4 5 var g = f; // скопировали ссылку на функцию-факториал в g 6 7 f = function() { return 0; }; // перезаписали f 8 9 alert( g(5) ); // 120, работает! Сравним это с обычным Function Expression:

1 var f = function(n) { 2 return n ? n*f(n-1) : 1; 3 }; 4 5 var g = f; 6 7 f = null; 8 alert( g(5) ); // ошибка, т.к. g попытается вызвать f(n-1), а f 9 перезаписана

В браузере IE8 и ниже создаются две функции Как мы говорили выше, в браузере IE до 9 версии имя NFE видно везде, что является ошибкой с точки зрения стандарта. ..Но на самом деле ситуация еще забавнее. Старый IE создаёт в таких случаях целых две функции: одна записывается в переменную f, а вторая — в переменнуюfactorial.


Например:

1 var f = function factorial(n) { /*...*/ }; 2 3 // в IE8- false 4 // в остальных браузерах ошибка, т.к. имя factorial не видно 5 alert(f === factorial); Все остальные браузеры полностью поддерживают именованные функциональные выражения.

5

Каков будет результат выполнения кода?

function g() { return 1; } alert(g); А такого? Будет ли разница, если да — почему?

(function g() { return 1; }); alert(g); Решение Первый код выведет function ..., второй — ошибку во всех браузерах, кроме IE8-.

1 // обычное объявление функции (Function Declaration) 2 function g() { return 1; }; 3 4 alert(g); // функция Во втором коде скобки есть, значит функция внутри является неFunction Declaration, а частью выражения, то есть Named Function Expression. Его имя видно только внутри, снаружи переменная g не определена.

1 // Named Function Expression! 2 (function g() { return 1; });


3 4 alert(g);

// Ошибка!

Все браузеры, кроме IE8-, поддерживают это ограничение видимости и выведут ошибку, ‘undefined variable’. [Открыть задачу в новом окне] 5

Что будет при повторном вызове функции?

1 var f = function sayHi(n) { 2 alert(n); 3 sayHi = 0; // перезаписываем имя функции 4 }; 5 6 f(1); // что выведет? 7 f(2); // а теперь что выведет? Разумеется, в жизни мы не делаем такого, но разобрать ситуацию важно для общего понимания функций. Решение При работе без use strict ничего особенного не произойдет, функция будет работать как обычно:

1 var f = function sayHi(n) { 2 alert(n); 3 sayHi = 0; 4 }; 5 6 f(1); // 1 7 f(2); // 2 Дело в том, что переменная f хранит ссылку на функцию. Изнутри функции создается переменная sayHi, которая тоже ссылается на функцию. Если мы перезапишем её, ничего страшного не случится. В современном стандарте указано, что такую переменную изменить нельзя.


Поэтому в строгом режиме будет ошибка, если конечно браузер корректно реализует эту часть стандарта:

1 var f = function sayHi() { 2 "use strict"; 3 4 sayHi = 0; // ошибка в современных браузерах alert("При корректной работе интерпретатора мы не увидим этот 5 alert"); 6} 7 8 f(); [Открыть задачу в новом окне] Глобальный объект

1. Глобальный объект 2. Порядок инициализации 3. Итого Механизм работы функций и переменных в JavaScript очень отличается от большинства языков. Чтобы его понять, мы в этой главе рассмотрим переменные и функции в глобальной области. А в следующей — пойдём дальше. Глобальн ый объект Глобальными называют переменные и функции, которые не находятся внутри какой-то функции. То есть, иными словами, если переменная или функция не находятся внутри конструкции function, то они — «глобальные». В JavaScript все глобальные переменные и функции являются свойствами специального объекта, который называется «глобальный объект» (global object). В браузере этот объект явно доступен под именем window. Объект window одновременно является глобальным объектом и содержит ряд свойств и методов для работы с окном браузера, но нас здесь интересует только его роль как глобального объекта. В других окружениях, например Node.JS, глобальный объект может быть недоступен в явном виде, но суть происходящего от этого не изменяется, поэтому далее для обозначения глобального объекта мы будем использовать "window". Присваивая или читая глобальную переменную, мы, фактически, работаем со свойствамиwindow. Например:

1 var a = 5;

// объявление var создаёт свойство window.a


2 alert(window.a); // 5 Создать переменную можно и явным присваиванием в window:

1 window.a = 5; 2 alert(a); // 5 Порядок инициализации Выполнение скрипта происходит в две фазы: 1. На первой фазе происходит инициализация, подготовка к запуску. Во время инициализации скрипт сканируется на предмет объявления функций вида Function Declaration, а затем — на предмет объявления переменных var. Каждое такое объявление добавляется в window. Функции, объявленные как Function Declaration, создаются сразу работающими, а переменные — равными undefined. 2. На второй фазе — собственно, выполнение. Присваивание (=) значений переменных происходит на второй фазе, когда поток выполнения доходит до соответствующей строчки кода. В начале кода ниже указано содержание глобального объекта на момент окончания инициализации:

1 // По окончании инициализации, до выполнения кода: 2 // window = { f: function, a: undefined, g: undefined } 3 4 var a = 5; // при инициализации даёт: window.a=undefined 5 function f(arg) { /*...*/ } // при инициализации даёт: window.f = 6 function 7 var g = function(arg) { /*...*/ }; // при инициализации даёт: window.g 8 = undefined Кстати, тот факт, что к началу выполнения кода переменные и функции уже содержатся в window, можно легко проверить:

1 alert("a" in 2 alert(a); // 3 alert(f); // 4 alert(g); // 5 6 var a = 5;

window); // true, т.к. есть свойство window.a равно undefined, присваивание будет выполнено далее function ..., готовая к выполнению функция undefined, т.к. это переменная, а не Function Declaration


7 function f() { /*...*/ } 8 var g = function() { /*...*/ };

Присвоение переменной без объявления В старом стандарте JavaScript переменную можно было создать и без объявленияvar:

1 a = 5; 2 3 alert(a); // 5 Такое присвоение, как и var a = 5, создает свойство window.a = 5. Отличие отvar a = 5 — в том, что переменная будет создана не на этапе входа в область видимости, а в момент присвоения. Сравните два кода ниже. Первый выведет undefined, так как переменная была добавлена в window на фазе инициализации:

1 alert(a); // undefined 2 3 var a = 5; Второй код выведет ошибку, так как переменной ещё не существует:

1 alert(a); // error, a is not defined 2 3 a = 5; Вообще, рекомендуется всегда объявлять переменные через var. В современном стандарте присваивание без var вызовет ошибку:

1 'use strict'; 2 a = 5; // error, a is not


defined

Конструкции for, if... не влияют на видимость переменных Фигурные скобки, которые используются в for, while, if, в отличие от объявлений функции, имеют «декоративный» характер. В JavaScript нет разницы между объявлением вне блока:

1 var i; 2{ 3 i = 5; 4}

…И внутри него:

1 i = 5; 2{ 3 var i; 4} Также нет разницы между объявлением в цикле и вне его:

1 for (var i=0; i<5; i++) { }

Идентичный по функциональности код:

1 var i; 2 for (i=0; i<5; i++) { } В обоих случаях переменная будет создана до выполнения цикла, на стадии инициализации, и ее значение будет сохранено после окончания цикла.

Не важно, где и сколько раз объявлена переменная


Объявлений var может быть сколько угодно:

1 var i = 10; 2 3 for (var i=0; i<20; i++) { 4 ... 5} 6 7 var i = 5; Все var будут обработаны один раз, на фазе инициализации. На фазе исполнения объявления var будут проигнорированы: они уже были обработаны. Зато будут выполнены присваивания.

Ошибки при работе с window в IE8В старых IE есть две забавные ошибки при работе с переменными в window:

1. Переопределение переменной, у которой такое же имя, как и id элемента, приведет к ошибке:

1 <div id="a">...</div> 2 <script> 3 a = 5; // ошибка в IE<9! Правильно будет "var a = 5" 4 alert(a); // никогда не сработает 5 </script> А если сделать через var, то всё будет хорошо. Это была реклама того, что надо везде ставить var.

2. Ошибка при рекурсии через функцию-свойство window. Следующий код «умрет» в IE<9:

1 <script> 2 // рекурсия через функцию, явно записанную в window 3 window.recurse = function(times) { 4 if (times !== 0) recurse(times-1); 5} 6


7 recurse(13); 8 </script> Проблема здесь возникает из-за того, что функция напрямую присвоена вwindow.recurse = .... Ее не будет при обычном объявлении функции. Этот пример выдаст ошибку только в настоящем IE8! Не IE9 в режиме эмуляции. Вообще, режим эмуляции позволяет отлавливать где-то 95% несовместимостей и проблем, а для оставшихся 5% вам нужен будет настоящий IE8 в виртуальной машине. Итого В результате инициализации, к началу выполнения кода:

1. Функции, объявленные как Function Declaration, создаются полностью и готовы к использованию.

2. Переменные объявлены, но равны undefined. Присваивания выполнятся позже, когда выполнение дойдет до них. 5

Каков будет результат кода?

1 if ("a" in window) { 2 var a = 1; 3} 4 alert(a); Решение Ответ: 1.

1 if ("a" in window) { 2 var a = 1; 3} 4 alert(a); Посмотрим, почему. На стадии подготовки к выполнению, из var a создается window.a:

1 // window = {a:undefined} 2 3 if ("a" in window) { // в if видно что window.a уже есть


4 var a = 1; // поэтому эта строка сработает 5} 6 alert(a); В результате a становится 1. [Открыть задачу в новом окне] 5

Каков будет результат (перед a нет var)?

1 if ("a" in window) { 2 a = 1; 3} 4 alert(a); Решение Ответ: ошибка. Переменной a нет, так что условие "a" in window не выполнится. В результате на последней строчке - обращение к неопределенной переменной.

1 if ("a" in window) { 2 a = 1; 3} 4 alert(a); // <-- error! [Открыть задачу в новом окне] 5

Каков будет результат (перед a нет var, а ниже есть)?

1 if ("a" in window) { 2 a = 1; 3} 4 var a; 5 6 alert(a); Решение Ответ: 1.


Переменная a создается до начала выполнения кода, так что условие"a" in window выполнится и сработает a = 1.

1 if ("a" in window) { 2 a = 1; 3} 4 var a; 5 6 alert(a); // 1 [Открыть задачу в новом окне] 3

Каков будет результат кода? Почему?

1 var a = 5; 2 3 function a() { } 4 5 alert(a); P.S. Это задание — учебное, на понимание процесса инициализации и выполнения. В реальной жизни мы, конечно же, не будем называть переменную и функцию одинаково. Решение Ответ: 5.

1 var a = 5; 2 3 function a() { } 4 5 alert(a); Чтобы понять, почему — разберём внимательно как работает этот код.


1. До начала выполнения создаётся переменная a и функция a. Стандарт написан так, что функция создаётся первой и переменная ее не перезаписывает. То есть, функция имеет приоритет. Но в данном случае это совершенно неважно, потому что…

2. …После инициализации, когда код начинает выполняться — срабатывает присваивание a = 5, перезаписывая a, и уже не важно, что там лежало.

3. Объявление Function Declaration на стадии выполнения игнорируется (уже обработано).

4. В результате alert(a) выводит 5. [Открыть задачу в новом окне] Замыкания, функции изнутри

1. Лексическое окружение 1. Пример 2. Доступ ко внешним переменным 3. Вложенные функции 4. Управление памятью 5. [[Scope]] для new Function 6. Итого В этой главе мы продолжим рассматривать, как работают переменные, и, как следствие, познакомимся с замыканиями. От глобального объекта мы переходим к работе внутри функций. Лексическое окружение Все переменные внутри функции — это свойства специального внутреннего объектаLexicalEnvironment. Мы будем называть этот объект «лексическое окружение» или просто «объект переменных». При запуске функция создает объект LexicalEnvironment, записывает туда аргументы, функции и переменные. Процесс инициализации выполняется в том же порядке, что и для глобального объекта, который, вообще говоря, является частным случаем лексического окружения. В отличие от window, объект LexicalEnvironment является внутренним, он скрыт от прямого доступа. Пример Посмотрим пример, чтобы лучше понимать, как это работает:

1 function sayHi(name) { 2 var phrase = "Привет, " + name; 3 alert(phrase); 4} 5 6 sayHi('Вася');


При вызове функции:

1. До выполнения первой строчки её кода, на стадии инициализации, интерпретатор создает пустой объект LexicalEnvironment и заполняет его. В данном случае туда попадает аргумент name и единственная переменная phrase:

1 function sayHi(name) { 2 // LexicalEnvironment = { name: 'Вася', phrase: undefined } 3 var phrase = "Привет, " + name; 4 alert(phrase); 5} 6 7 sayHi('Вася'); 2. Функция выполняется. Во время выполнения происходит присвоение локальной переменной phrase, то есть, другими словами, присвоение свойству LexicalEnvironment.phrase нового значения:

1 function sayHi(name) { 2 // LexicalEnvironment = { name: 'Вася', phrase: undefined } 3 var phrase = "Привет, " + name; 4 5 // LexicalEnvironment = { name: 'Вася', phrase: 'Привет, Вася'} 6 alert(phrase); 7} 8 9 sayHi('Вася'); 3. В конце выполнения функции объект с переменными обычно выбрасывается и память очищается.

Тонкости спецификации Если почитать спецификацию ECMA-262, то мы увидим, что речь идёт о двух объектах: VariableEnvironment и LexicalEnvironment. Но там же замечено, что в реализациях эти два объекта могут быть объединены. Так что мы избегаем лишних деталей и используем везде термин LexicalEnvironment, это достаточно точно позволяет описать происходящее. Более формальное описание находится в спецификации ECMA-262, секции 10.2-10.5 и 13.


Доступ ко внешним переменн ым Из функции мы можем обратиться не только к локальной переменной, но и к внешней:

1 var a = 5; 2 3 function f() { 4 alert(a); // 5 5} Интерпретатор, при доступе к переменной, сначала пытается найти переменную в текущемLexicalEnvironment, а затем, если её нет — ищет во внешнем объекте переменных. В данном случае им является window. Такой порядок поиска возможен благодаря тому, что ссылка на внешний объект переменных хранится в специальном внутреннем свойстве функции, которое называется [[Scope]]. Рассмотрим, как оно создаётся и используется в коде выше: 1. Все начинается с момента создания функции. Функция создается не в вакууме, а в некотором лексическом окружении. В случае выше функция создается в глобальном лексическом окружении window:

Для того, чтобы функция могла в будущем обратиться к внешним переменным, в момент создания она получает скрытое свойство [[Scope]], которое ссылается на лексическое окружение, в котором она была создана:

Эта ссылка появляется одновременно с функцией и умирает вместе с ней. Программист не может как-либо получить или изменить её. 2. Позже, приходит время и функция запускается. Интерпретатор вспоминает, что у неё есть свойство f.[[Scope]]:


…И использует его при создании объекта переменных для функции. Новый объект LexicalEnvironment получает ссылку на «внешнее лексическое окружение» со значением из [[Scope]]. Эта ссылка используется для поиска переменных, которых нет в текущей функции.

Например, alert(a) сначала ищет в текущем объекте переменных: он пустой. А потом, как показано зеленой стрелкой на рисунке ниже — по ссылке, во внешнем окружении.

На уровне кода это выглядит как поиск во внешней области видимости, вне функции:

Если обобщить:

• Каждая функция при создании получает ссылку [[Scope]] на объект с переменными, в контексте которого была создана.

• При запуске функции создается новый объект с переменными. В него копируется ссылка на внешний объект из [[Scope]]. • При поиске переменных он осуществляется сначала в текущем объекте переменных, а потом — по этой ссылке. Благодаря этому в функции доступны внешние переменные. 5

Каков будет результат выполнения этого кода?


01 var value = 0; 02 03 function f() { 04 if (1) { 05 value = true; 06 } else { 07 var value = false; 08 } 09 10 alert(value); 11 } 12 13 f(); Изменится ли внешняя переменная value ? P.S. Какими будут ответы, если из строки var value = false убрать var? Решение Результатом будет true, т.к. var обработается и переменная будет создана до выполнения кода. Соответственно, присвоение value=true сработает на локальной переменной, иalert выведет true. Внешняя переменная не изменится. P.S. Если var нет, то в функции переменная не будет найдена. Интерпретатор обратится за ней в window и изменит её там. Так что без var результат будет также true, но внешняя переменная изменится. [Открыть задачу в новом окне] 5

Каков будет результат выполнения этого кода? Почему?

01 function test() { 02 03 alert(window); 04 05 var window = 5; 06


07 alert(window); 08 } 09 10 test(); Решение Результатом будет undefined, затем 5.

01 function test() { 02 03 alert(window); 04 05 var window = 5; 06 07 alert(window); 08 } 09 10 test(); Директива var обработается до начала выполнения кода функции. Будет создана локальная переменная, т.е. свойство LexicalEnvironment:

LexicalEnvironment = { window: undefined } Когда выполнение кода начнется и сработает alert, он выведет локальную переменную. Затем сработает присваивание, и второй alert выведет уже 5. [Открыть задачу в новом окне] 4

Каков будет результат выполнения кода? Почему?

1 var a = 5 2 3 (function() { 4 alert(a) 5 })()


P.S. Подумайте хорошо! Здесь все ошибаются! P.P.S. Внимание, здесь подводный камень! Ок, вы предупреждены. Решение Результат - ошибка. Попробуйте:

1 var a = 5 2 3 (function() { 4 alert(a) 5 })() Дело в том, что после var a = 5 нет точки с запятой. JavaScript воспринимает этот код как если бы перевода строки не было:

1 var a = 5(function() { 2 alert(a) 3 })() То есть, он пытается вызвать функцию 5, что и приводит к ошибке. Если точку с запятой поставить, все будет хорошо:

1 var a = 5; 2 3 (function() { 4 alert(a) 5 })() Это один из наиболее частых и опасных подводных камней, приводящих к ошибкам тех, кто не ставит точки с запятой. [Открыть задачу в новом окне] Вложенн ые функции Внутри функции можно объявлять не только локальные переменные, но и другие функции. Как правило, это делают для вспомогательных операций, например в коде ниже для генерации сообщения используются makeMessage и getHello:


01 function sayHi(person) { 02 03 var message = makeMessage(person); 04 alert( message ); 05 06 // ----- вспомогательные функции ----07 08 function getHello(age) { 09 return age >= 18 ? 'Здравствуйте' : 'Привет'; 10 } 11 12 function makeMessage(person) { 13 return getHello(person.age) + ', ' + person.name; 14 } 15 } 16 17 sayHi({ 18 name: 'Петька', 19 age: 17 20 }); // привет, Петька Вложенные функции могут быть объявлены и как Function Declaration и какFunction Expression. Вложенные функции обрабатываются в точности так же, как и глобальные. Единственная разница — они создаются в объекте переменных внешней функции, а не в window. То есть, при запуске внешней функции sayHi, в её LexicalEnvironment попадают локальные переменные и вложенные Function Declaration. При этом Function Declaration сразу готовы к выполнению. В примере выше при запуске sayHi(person) будет создан такой LexicalEnvironment:

1 LexicalEnvironment = { 2 person: переданный аргумент, 3 message: undefined, 4 getHello: function..., 5 makeMessage: function... 6} Затем, во время выполнения sayHi, к вложенным функциям можно обращаться, они будут взяты из локального объекта переменных. Вложенная функция имеет доступ к внешним переменным через [[Scope]].


1. При создании любая функция, в том числе и вложенная, получает свойство [[Scope]], указывающее на объект переменных, в котором она была создана. 2. При запуске она будет искать переменные сначала у себя, потом во внешнем объекте переменных, затем в более внешнем, и так далее. Поэтому в примере выше из объявления функции makeMessage(person) можно убрать аргументperson. Было:

1 function sayHi(person) { 2 ... 3 function makeMessage(person) { 4 return getHello(person.age) + ', ' + person.name; 5 } 6} Станет:

1 function sayHi(person) { 2 ... 3 function makeMessage() { // убрали аргумент 4 // переменная person будет взята из внешнего объекта переменных 5 return getHello(person.age) + ', ' + person.name; 6 } 7} Вложенную функцию можно возвратить. Например, пусть sayHi не выдаёт alert тут же, а возвращает функцию, которая это делает:

01 function sayHi(person) { 02 03 return function() { // (*) 04 var message = makeMessage(person); // (**) 05 alert( message ); 06 }; 07 08 // ----- вспомогательные функции ----09 10 function getHello(age) { 11 return age >= 18 ? 'Здравствуйте' : 'Привет'; 12 } 13 14 function makeMessage() { 15 return getHello(person.age) + ', ' + person.name;


16 } 17 } 18 19 var sayHiPete = sayHi({ name: 'Петька', age: 17 }); 20 var sayHiOther = sayHi({ name: 'Василий Иванович', age: 35 }); 21 22 sayHiPete(); // эта функция может быть вызвана позже В реальной жизни это нужно, чтобы вызвать получившуюся функцию позже, когда это будет нужно. Например, при нажатии на кнопку. Возвращаемая функция (*) при запуске будет иметь полный доступ к аргументам внешней функции, а также к другим вложенным функциям makeMessage и getHello, так как при создании она получает ссылку [[Scope]], которая указывает на текущий LexicalEnvironment. Переменные, которых нет в ней, например, person, будут взяты из него. В частности, функция makeMessage при вызове в строке (**) будет взята из внешнего объекта переменных. Внешний LexicalEnvironment, в свою очередь, может ссылаться на ещё более внешнийLexicalEnvironment, и так далее. Замыканием функции называется сама эта функция, плюс вся цепочкаLexicalEnvironment, которая при этом образуется. Иногда говорят «переменная берётся из замыкания». Это означает — из внешнего объекта переменных. Можно сказать и по-другому: «замыкание — это функция и все внешние переменные, которые ей доступны». Управление память ю JavaScript устроен так, что любой объект и, в частности, функция, существует до тех пор, пока на него есть ссылка, пока он как-то доступен для вызова, обращения. Более подробно об управлении памятью — далее, в статье Управление памятью в JS и DOM. Отсюда следует важное следствие при работе с замыканиями. Объект переменных внешней функции существует в памяти до тех пор, пока существует хоть одна внутренняя функция, ссылающаяся на него через свойство [[Scope]]. Посмотрим на примеры. • Обычно объект переменных удаляется по завершении работы функции. Даже если в нём есть объявление внутренней функции:


1 function f() { 2 var value = 123; 3 function g() { } // g видна только изнутри 4} 5 6 f(); В коде выше внутренняя функция объявлена, но она осталась внутри. После окончания работыf() она станет недоступной для вызовов, так что будет убрана из памяти вместе с остальными локальными переменными.

• …А вот в этом случае лексическое окружение, включая переменную a, будет сохранено: 1 function f() { 2 var a = Math.random(); 3 4 function g() { } 5 6 return g; 7} 8 var g = f(); // функция g будет жить и сохранит ссылку на объект 9 переменных • Если f() будет вызываться много раз, а полученные функции будут сохраняться, например, складываться в массив, то будут сохраняться и объекты LexicalEnvironment с соответствующими значениями a:

1 function f() { 2 var a = Math.random(); 3 4 return function() { }; 5} 6 7 // 3 функции, // каждая ссылается на соответствующий объект LexicalEnvironment 8 = {a: ...} 9 var arr = [f(), f(), f()]; Обратим внимание, что переменная a не используется в возвращаемой функции. Это означает, что браузерный оптимизатор может «де-факто» удалять её из памяти. Всё равно ведь никто не заметит.

• В этом коде замыкание сначала сохраняется в памяти, а после удаления ссылки на g умирает:

01 function f() { 02 var a = Math.random(); 03 04 function g() { } 05


06 return g; 07 } 08 09 var g = f(); // функция g жива 10 // а значит в памяти остается соответствующий объект переменных 11 // ..а вот теперь память будет очищена 12 g = null; [[Scope]] для new Function Есть одно исключение из общего правила присвоения [[Scope]]. При создании функции с использованием new Function, её свойство [[Scope]] ссылается не на текущий LexicalEnvironment, а на window. Следующий пример демонстрирует как функция, созданная new Function, игнорирует внешнюю переменную a и выводит глобальную вместо нее. Сначала обычное поведение:

01 var a = 1; 02 function getFunc() { 03 var a = 2; 04 05 var func = function() { alert(a); }; 06 07 return func; 08 } 09 10 getFunc()(); // 2, из LexicalEnvironment функции getFunc А теперь — для функции, созданной через new Function:

01 var a = 1; 02 function getFunc() { 03 var a = 2; 04 05 var func = new Function('', 'alert(a)'); 06 07 return func; 08 } 09 10 getFunc()(); // 1, из window


Итого

1. Все переменные и параметры функций являются свойствами объекта переменныхLexicalEnvironment. Каждый запуск функции создает новый такой объект. На верхнем уровне роль LexicalEnvironment играет «глобальный объект», в браузере этоwindow.

2. При создании функция получает системное свойство [[Scope]], которое ссылается наLexicalEnvironment, в котором она была создана (кроме new Function).

3. При запуске функции её LexicalEnvironment ссылается на внешний, сохраненный в[[Scope]]. Переменные сначала ищутся в своём объекте, потом — в объекте по ссылке и так далее, вплоть до window. Разрабатывать без замыканий в JavaScript почти невозможно. Вы еще не раз встретитесь с ними в следующих главах учебника. Хранение данных в замыкании, модули

1. Данные для счётчика 1. Объект счётчика 2. Объект счётчика + функция 2. Приём проектирования «Модуль» 3. Задачи на понимание замыканий Замыкания можно использовать сотнями способов. Иногда люди сами не замечают, что использовали замыкания — настолько это просто и естественно. В этой главе мы рассмотрим примеры использования замыканий для хранения данных и задачи на эту тему. Данн ые для счётчика В примере ниже makeCounter создает функцию, которая считает свои вызовы:

01 function makeCounter() { 02 var currentCount = 0; 03 04 return function() { 05 currentCount++; 06 return currentCount; 07 }; 08 } 09 10 var counter = makeCounter(); 11 12 // каждый вызов увеличивает счётчик 13 counter(); 14 counter(); 15 alert( counter() ); // 3


Хранение текущего числа вызовов осуществляется в переменной currentCount внешней функции. При этом, так как каждый вызов makeCounter создает новый объект переменных, то все создаваемые функции-счётчики взаимно независимы.

1 var c1 2 3 var c2 4 5 alert( 6 alert(

= makeCounter(); = makeCounter(); c1() ); // 1 c2() ); // 1, счётчики независимы

Добавим счётчику аргумент, который, если передан, устанавливает значение:

01 function makeCounter() { 02 var currentCount = 0; 03 04 return function(newCount) { 05 if (newCount !== undefined) { // есть аргумент? currentCount = +newCount; // сделаем его новым значением 06 счётчика // вернём текущее значение, счётчик всегда возвращает его (это 07 удобно) 08 return currentCount; 09 } 10 11 currentCount++; 12 return currentCount; 13 }; 14 } 15 16 var counter = makeCounter(); 17 18 alert( counter() ); // 1 19 alert( counter(3) ); // 3 20 alert( counter() ); // 4 Здесь для проверки первого аргумента использовано сравнение с undefined, так что вызовыcounter(undefined) и counter() сработают идентично, как будто аргументов нет. Объект счётчика Можно пойти дальше и возвратить полноценный объект с функциями управления счётчиком:

• getNext() — получить следующее значение, то, что раньше делал вызов counter(). • set(value) — поставить значение.


• reset() — обнулить счётчик. 01 function makeCounter() { 02 var currentCount = 0; 03 04 return { 05 getNext: function() { 06 return ++currentCount; 07 }, 08 09 set: function(value) { 10 currentCount = value; 11 }, 12 13 reset: function() { 14 currentCount = 0; 15 } 16 }; 17 } 18 19 var counter = makeCounter(); 20 21 alert( counter.getNext() ); // 1 22 alert( counter.getNext() ); // 2 23 24 counter.reset(); 25 alert( counter.getNext() ); // 1 …Теперь counter — объект с методами, которые при работе используют currentCount. Снаружи никак иначе, кроме как через эти методы, к currentCount получить доступ нельзя, так как это локальная переменная. Объект счётчика + функция К сожалению, пропал короткий красивый вызов counter(), вместо него теперь counter.getNext(). Но он ведь был таким коротким и удобным… Так что давайте вернём его:

01 function makeCounter() { 02 var currentCount = 0; 03 04 // возвращаемся к функции 05 function counter() { 06 return ++currentCount;


07 } 08 09 // ...и добавляем ей методы! 10 counter.set = function(value) { 11 currentCount = value; 12 }; 13 14 counter.reset = function() { 15 currentCount = 0; 16 }; 17 18 return counter; 19 } 20 21 var counter = makeCounter(); 22 23 alert( counter() ); // 1 24 alert( counter() ); // 2 25 26 counter.reset(); 27 alert( counter() ); // 1 Красиво, не правда ли? Тот факт, что объект — функция, вовсе не мешает добавить к нему сколько угодно методов. Приём проектирования «Модуль » Приём программирования «модуль» имеет громадное количество вариаций. Его цель — объявить функции так, чтобы ненужные подробности их реализаций были скрыты. В том числе, временные переменные, константы, вспомогательные мини-функции и т.п. Оформление кода в модуль предусматривает следующие шаги: 1. Создаётся функция-обёртка, которая выполняется «на месте»:

(function() { ... })(); 2. Внутрь этой функции пишутся локальные переменные и функции, которые пользователю модуля не нужны, но нужны самому модулю:

1 (function() { 2 var count = 0; 3 4 function helper() { ... } 5 })();


Они будут доступны только изнутри. 3. Те функции, которые нужны пользователю, «экспортируются» во внешнюю область видимости. Если функция одна — это можно сделать явным возвратом return:

1 var func = (function() { 2 var count = 0; 3 function helper() { ... } 4 5 return function() { ... } 6 })(); …Если функций много — можно присвоить их напрямую в window:

1 (function() { 2 3 function helper() { ... } 4 5 window.createMenu = function() { ... }; 6 window.createDialog = function() { ... }; 7 8 })(); Или, что ещё лучше, вернуть объект с ними:

01 var MyLibrary = (function() { 02 03 function helper() { ... } 04 05 return { 06 createMenu: function() { ... }, 07 createDialog: function() { ... } 08 }; 09 10 })(); 11 12 // использование 13 MyLibrary.createMenu(); Все функции модуля будут через замыкание иметь доступ к другим переменным и внутренним функциям. Но снаружи программист, использующий модуль, может обращаться напрямую только к тем, которые экспортированы. Благодаря этому будут скрыты внутренние аспекты реализации, которые нужны только разработчику модуля. Можно придумать и много других вариаций такого подхода. В конце концов, «модуль» — это


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

Напишите функцию sum, которая работает так: sum(a)(b) = a+b. Да, именно так, через двойные скобки (это не опечатка). Например:

sum(1)(2) = 3 sum(5)(-1) = 4 Решение Чтобы вторые скобки в вызове работали - первые должны возвращать функцию. Эта функция должна знать про a и уметь прибавлять a к b. Вот так:

01 function sum(a) { 02 03 return function(b) { 04 return a + b; // возьмет a из внешнего LexicalEnvironment 05 }; 06 07 } 08 09 alert( sum(1)(2) ); 10 alert( sum(5)(-1) ); [Открыть задачу в новом окне] 5

1. Создайте функцию filter(arr, func), которая получает массивarr и возвращает новый, в который входят только те элементыarr, для которых func возвращает true.

2. Создайте набор «готовых фильтров»: inBetween(a,b) — «между a,b»,inArray([...]) — «в массиве [...]». Использование должно быть таким:

• filter(arr, inBetween(3,6)) — выберет только числа от 3 до 6,

• filter(arr, inArray([1,2,3])) — выберет только элементы, совпадающие с одним из значений массива.


Пример, как это должно работать:

1 /* .. ваш код для filter, inBetween, inArray */ 2 var arr = [1, 2, 3, 4, 5, 6, 7]; 3 4 alert( filter(arr, function(a) { return a % 2 == 0 }) ); // 2,4,6 5 6 alert( filter(arr, inBetween(3,6)) ); // 3,4,5,6 7 8 alert( filter(arr, inArray([1,2,10])) ); // 1,2 Решение Функция фильтрации

01 function filter(arr, func) { 02 var result = []; 03 04 for(var i=0; i<arr.length; i++) { 05 var val = arr[i]; 06 if (func(val)) { 07 result.push(val); 08 } 09 } 10 11 return result; 12 } 13 14 var arr = [1, 2, 3, 4, 5, 6, 7]; 15 alert( filter(arr, function(a) { return a % 2 == 0; }) ); // 2, 16 4, 6 Фильтр inBetween

01 function filter(arr, func) { 02 var result = []; 03 04 for(var i=0; i<arr.length; i++) { 05 var val = arr[i]; 06 if (func(val)) { 07 result.push(val); 08 } 09 }


10 11 return result; 12 } 13 14 function inBetween(a, b) { 15 return function(x) { 16 return x >=a && x <= b; 17 }; 18 } 19 20 var arr = [1, 2, 3, 4, 5, 6, 7]; 21 alert( filter(arr, inBetween(3,6)) ); // 3,4,5,6 Фильтр inArray

01 function filter(arr, func) { 02 var result = []; 03 04 for(var i=0; i<arr.length; i++) { 05 var val = arr[i]; 06 if (func(val)) { 07 result.push(val); 08 } 09 } 10 11 return result; 12 } 13 14 function inArray(arr) { 15 return function(x) { 16 return arr.indexOf(x) != -1; 17 }; 18 } 19 20 var arr = [1, 2, 3, 4, 5, 6, 7]; 21 alert( filter(arr, inArray([1,2,10])) ); // 1,2 [Открыть задачу в новом окне] 5


В некоторых языках программирования существует объект «строковый буфер», который аккумулирует внутри себя значения. Его функционал состоит из двух возможностей: 1. Добавить значение в буфер. 2. Получить текущее содержимое. Задача — реализовать строковый буфер на функциях в JavaScript, со следующим синтаксисом:

• Создание объекта: var buffer = makeBuffer();. • Вызов makeBuffer должен возвращать такую функцию buffer, которая при вызове buffer(value) добавляет значение в некоторое внутреннее хранилище, а при вызове без аргументов buffer() — возвращает его. Вот пример работы:

01 function makeBuffer() { /* ваш код */ } 02 03 var buffer = makeBuffer(); 04 05 // добавить значения к буферу 06 buffer('Замыкания'); 07 buffer(' Использовать'); 08 buffer(' Нужно!'); 09 10 // получить текущее значение 11 alert( buffer() ); // Замыкания Использовать Нужно! Буфер должен преобразовывать все данные к строковому типу:

1 var buffer = makeBuffer(); 2 buffer(0); buffer(1); buffer(0); 3 4 alert( buffer() ); // '010' Решение не должно использовать глобальные переменные. Решение Текущее значение текста удобно хранить в замыкании, в локальной переменнойmakeBuffer:


01 function makeBuffer() { 02 var text = ''; 03 04 return function(piece) { 05 if (piece === undefined) { // вызов без аргументов 06 return text; 07 } 08 text += piece; 09 }; 10 }; 11 12 var buffer = makeBuffer(); 13 14 // добавить значения к буферу 15 buffer('Замыкания'); 16 buffer(' Использовать'); 17 buffer(' Нужно!'); 18 alert( buffer() ); // 'Замыкания Использовать Нужно!' 19 20 var buffer2 = makeBuffer(); 21 buffer2(0); buffer2(1); buffer2(0); 22 23 alert( buffer2() ); // '010' Начальное значение text = '' — пустая строка. Поэтому операция text += pieceприбавляет piece к строке, автоматически преобразуя его к строковому типу, как и требовалось в условии. [Открыть задачу в новом окне] 5

Добавьте буфферу из решения задачи Функция - строковый буферметод buffer.clear(), который будет очищать текущее содержимое буфера:

01 function makeBuffer() { 02 ...ваш код... 03 } 04 05 var buffer = makeBuffer(); 06 07 buffer("Тест");


08 buffer(" тебя не съест "); 09 alert( buffer() ); // Тест тебя не съест 10 11 buffer.clear(); 12 13 alert( buffer() ); // "" Решение

01 function makeBuffer() { 02 var text = ''; 03 04 function buffer(piece) { 05 if (piece === undefined) { // вызов без аргументов 06 return text; 07 } 08 text += piece; 09 }; 10 11 buffer.clear = function() { 12 text = ""; 13 } 14 15 return buffer; 16 }; 17 18 var buffer = makeBuffer(); 19 20 buffer("Тест"); 21 buffer(" тебя не съест "); 22 alert( buffer() ); // Тест тебя не съест 23 24 buffer.clear(); 25 26 alert( buffer() ); // "" [Открыть задачу в новом окне] 5

• Будет ли работать доступ к переменной name через замыкание в примере ниже?


• Удалится ли переменная name из памяти при выполненииdelete donkey.sayHi? Если нет — можно ли к name как-то обратиться после удаления donkey.sayHi?

• А если присвоить donkey.sayHi = donkey.yell = null — останется ли nameв памяти?

01 var makeDonkey = function() { 02 var name = "Ослик Иа"; 03 04 return { 05 sayHi: function() { 06 alert(name); 07 }, 08 yell: function() { 09 alert('И-а, и-а!'); 10 } 11 }; 12 } 13 14 var donkey = makeDonkey(); 15 donkey.sayHi(); Решение

1. Да, будет работать, благодаря ссылке [[Scope]] на внешний объект переменных, которая будет присвоена функциям sayHi и yell при создании объекта.

2. Нет, name не удалится из памяти, поскольку несмотря на то, что sayHi больше нет, есть ещё функция yell, которая также ссылается на внешний объект переменных. Этот объект хранится целиком, вместе со всеми свойствами. При этом, так как функция sayHi удалена из объекта и ссылок на нее нет, то больше к переменной name переменной обращаться некому. Получилось, что она «застряла» в памяти, хотя, по сути, никому не нужна.

3. Если и sayHi и yell удалить, тогда, так как больше внутренних функций не останется, удалится и объект переменных вместе с name. [Открыть задачу в новом окне] 5


Следующий код создает массив функций-стрелков shooters. По замыслу, каждый стрелок должен выводить свой номер:

01 function makeArmy() { 02 03 var shooters = []; 04 05 for(var i=0; i<10; i++) { 06 var shooter = function() { // функция-стрелок 07 alert(i); // выводит свой номер 08 }; 09 shooters.push(shooter); 10 } 11 12 return shooters; 13 } 14 15 var army = makeArmy(); 16 17 army[0](); // стрелок выводит 10, а должен 0 18 army[5](); // стрелок выводит 10... 19 // .. все стрелки выводят 10 вместо 0,1,2...9 Почему все стрелки́ выводят одно и то же? Поправьте код, чтобы стрелки работали как задумано. Предложите несколько вариантов исправления. Решение Что происходит в этом коде Функция makeArmy делает следующее:

1. Создаёт пустой массив shooter: var shooters = []; 2. В цикле заполняет массив элементами через shooter.push. При этом каждый элемент массива — это функция, так что в итоге после цикла массив будет таким:

01 shooters = 02 function 03 function 04 function

[ () { alert(i); }, () { alert(i); }, () { alert(i); },


05 function 06 function 07 function 08 function 09 function 10 function 11 function 12 ];

() () () () () () ()

{ { { { { { {

alert(i); alert(i); alert(i); alert(i); alert(i); alert(i); alert(i);

}, }, }, }, }, }, }

Этот массив возвращается из функции.

3. Вызов army[5]() — это получение элемента массива (им будет функция) на позиции 5, и тут же — её запуск. Почему ошибка Вначале разберемся, почему все стрелки выводят одно и то же значение. В функциях-стрелках shooter отсутствует переменная i. Когда такая функция вызывается, то i она берет из внешнего LexicalEnvironment… Каким будет значение i? К моменту вызова army[0](), функция makeArmy уже закончила работу. Цикл завершился, последнее значение было i=10. В результате все функции shooter получают одно и то же, последнее, значениеi=10. Попробуйте исправить проблему самостоятельно. Исправление (3 варианта) Есть несколько способов исправить ситуацию.

1. Первый способ исправить код - это привязать значение непосредственно к функции-стрелку:

01 function makeArmy() { 02 03 var shooters = []; 04 05 for(var i=0; i<10; i++) { 06


07 var shooter = function me() { 08 alert( me.i ); 09 }; 10 shooter.i = i; 11 12 shooters.push(shooter); 13 } 14 15 return shooters; 16 } 17 18 var army = makeArmy(); 19 20 army[0](); // 0 21 army[1](); // 1 В этом случае каждая функция хранит в себе свой собственный номер. Кстати, обратите внимание на использование Named Function Expression, вот в этом участке:

1 ... 2 var shooter = function me() { 3 alert( me.i ); 4 }; 5 ... Если убрать имя me и оставить обращение через shooter, то работать не будет:

1 for(var i=0; i<10; i++) { 2 var shooter = function() { 3 alert(shooter.i); // вывести свой номер (не работает!) 4 // потому что откуда функция возьмёт переменную shooter? // ..правильно, из внешнего объекта, а там она одна на 5 всех 6 }; 7 shooter.i = i; 8 shooters.push(shooter); 9}


Вызов alert(shooter.i) при вызове будет искать переменную shooter, а эта переменная меняет значение по ходу цикла, и к моменту вызову она равна последней функции, созданной в цикле. Если использовать Named Function Expression, то имя жёстко привязывается к конкретной функции, и поэтому в коде выше me.i возвращает правильный i.

2. Другое, более продвинутое решение —- использовать дополнительную функцию для того, чтобы «поймать» текущее значение i:

01 function makeArmy() { 02 03 var shooters = []; 04 05 for(var i=0; i<10; i++) { 06 07 var shooter = (function(x) { 08 09 return function() { 10 alert( x ); 11 }; 12 13 })(i); 14 15 shooters.push(shooter); 16 } 17 18 return shooters; 19 } 20 21 var army = makeArmy(); 22 23 army[0](); // 0 24 army[1](); // 1 Посмотрим выделенный фрагмент более внимательно, чтобы понять, что происходит:

1 var shooter = (function(x) { 2 return function() { 3 alert( x ); 4 };


5 })(i); Функция shooter создана как результат вызова промежуточного функционального выражения function(x), которое объявляется — и тут же выполняется, получая x = i. Так как function(x) тут же завершается, то значение x больше не меняется. Оно и будет использовано в возвращаемой функции-стрелке. Для красоты можно изменить название переменной x на i, суть происходящего при этом не изменится:

1 var shooter = (function(i) { 2 return function() { 3 alert( i ); 4 }; 5 })(i); Кстати, обратите внимание — скобки вокруг function(i) не нужны, можно и так:

1 var shooter = function(i) { // без скобок вокруг function(i) 2 return function() { 3 alert( i ); 4 }; 5 }(i); Скобки добавлены в код для лучшей читаемости, чтобы человек, который просматривает его, не подумал, что var shooter = function, а понял что это вызов «на месте», и присваивается его результат.

3. Еще один забавный способ - обернуть весь цикл во временную функцию:

01 function makeArmy() { 02 03 var shooters = []; 04 05 for(var i=0; i<10; i++) (function(i) { 06 07 var shooter = function() { 08 alert( i ); 09 }; 10


11 shooters.push(shooter); 12 13 })(i); 14 15 return shooters; 16 } 17 18 var army = makeArmy(); 19 20 army[0](); // 0 21 army[1](); // 1 Вызов (function(i) { ... }) обернут в скобки, чтобы интерпретатор понял, что это Function Expression. Плюс этого способа - в большей читаемости. Фактически, мы не меняем создание shooter, а просто обертываем итерацию в функцию. [Открыть задачу в новом окне] Статические переменные

1. Использование замыкания 2. Запись в функцию Статическая переменная функции — это такая, которая сохраняет значение между вызовами. Такие переменные есть во многих языках. В JavaScript они не реализованы синтаксически, но можно организовать аналог. Использование замыкания В предыдущей главе мы видели, как реализовать статическую переменную, используя замыкание. В примере ниже количество count вызовов функции sayHi сохраняется в обёртке:

01 var sayHi = (function() { 02 03 var count = 0; // статическая переменная 04 05 return function() { 06 count++; 07 08 alert("Привет " + count); 09 };


10 11 })(); 12 13 sayHi(); // Привет 1 14 sayHi(); // Привет 2 Это достаточно хороший способ, но, в качестве альтернативы, рассмотрим ещё один. Запись в функцию Благодаря тому, что функция — это объект, можно добавить статические свойства прямо к ней. Перепишем пример, используя запись в функцию:

01 function sayHi() { 02 sayHi.count++; 03 04 alert("Привет " + sayHi.count); 05 } 06 07 sayHi.count = 0; // начальное значение 08 09 sayHi(); // Привет 1 10 sayHi(); // Привет 2 Как видно, пример работает также, но внутри всё по-другому. Статическая переменная, записанная как свойство функции — общедоступна. К ней имеет доступ любой, у кого есть объект функции. Этим она отличается от привязки через замыкание.

Глобальная переменная? В примере выше можно было бы вынести количество вызовов в глобальную переменную sayHiCount. Но это лишь потому, что пример прост. Глобальные переменные, вообще, следует использовать пореже. Статическую переменную естественно связывать напрямую с функцией, что и решается замыканием, либо записью в свойство. 3


Создайте счётчик, который вместо замыкания использует статическую переменную для хранения currentCount. Код счётчика, работащий через замыкание (для переделки):

1 function makeCounter() { // currentCount можно считать "статической переменной" 2 счётчика 3 var currentCount = 0; 4 5 return function() { 6 currentCount++; 7 return currentCount; 8 }; 9} Важно: счётчики должны быть независимыми:

1 var c1 2 var c2 3 4 alert( 5 alert( 6 alert(

= makeCounter(); = makeCounter(); c1() ); // 1 c2() ); // 1 c1() ); // 2

Решение Переписанный счётчик:

01 function makeCounter() { 02 return function f() { 03 if (!f.currentCount) { 04 f.currentCount = 0; 05 } 06 07 return ++f.currentCount; 08 }; 09 } 10 11 var c1 = makeCounter(); 12 var c2 = makeCounter(); 13 14 alert( c1() ); // 1


15 alert( c2() ); // 1 16 alert( c1() ); // 2 Побочный эффект — текущее значение счётчика теперь доступно снаружи через свойство функции:

1 var counter = makeCounter(); 2 3 counter(); 4 alert( counter.currentCount ); // 1 [Открыть задачу в новом окне] Конструкция "with"

1. Пример 2. Изменения переменной 3. Почему отказались от with? 1. Замена with 4. Итого Конструкция with позволяет использовать для области видимости произвольный объект. В современном JavaScript от этой конструкции отказались, но её еще можно найти в старом коде. Синтаксис:

with(obj) { ... код ... } Любое обращение к переменной внутри with сначала ищет её среди свойств obj, а только потом — вне with. Пример В примере ниже переменная будет взята не из глобальной области, а из obj:

1 var a = 5; 2 3 var obj = { a : 10 }; 4 5 with(obj) { 6 alert(a); // 10, из obj 7} Попробуем получить переменную, которой в obj нет:

1 var b = 1;


2 3 var obj = { a : 10 }; 4 5 with(obj) { 6 alert(b); // 1, из window 7} Здесь интерпретатор сначала проверяет наличие obj.b, не находит и идет вне with. Особенно забавно выглядит применение вложенных with:

01 var obj = { 02 weight: 10, 03 size: { 04 width: 5, 05 height: 7 06 } 07 }; 08 09 with(obj) { 10 with(size) { // size будет взят из obj alert( width*height / weight ); // width,height из size, weight 11 из obj 12 } 13 } Свойства из разных объектов используются как обычные переменные… Магия! Порядок поиска переменных в выделенном коде: size => obj => window

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


Например:

1 var obj = { a : 10 } 2 3 with(obj) { 4 a = 20; 5} 6 alert(obj.a); // 20, переменная была изменена в объекте Почему отказались от with? Есть несколько причин.

1. В современном стандарте JavaScript отказались от with, потому что конструкция withподвержена ошибкам и непрозрачна. Проблемы возникают в том случае, когда в with(obj) присваивается переменная, которая по замыслу должна быть в свойствах obj, но ее там нет. Например:

1 var obj = { weight: 10 }; 2 3 with(obj) { 4 weight = 20; // (1) 5 size = 35; // (2) 6} 7 8 alert(obj.size); 9 alert(window.size); В строке (2) присваивается свойство, отсутствующее в obj. В результате интерпретатор, не найдя его, создает новую глобальную переменную window.size. Такие ошибки редки, но очень сложны в отладке, особенно если size изменилась не в window, а где-нибудь во внешнем LexicalEnvironment.

2. Еще одна причина — алгоритмы сжатия JavaScript не любят with. Перед выкладкой на сервер JavaScript сжимают. Для этого есть много инструментов, например Closure Compiler иUglifyJS. Если вкратце — они либо сжимают код с with с ошибками, либо оставляют его частично несжатым.

3. Ну и, наконец, производительность — усложнение поиска переменной из-за with влечет дополнительные накладные расходы. Современные движки применяют много внутренних оптимизаций, ряд которых не могут быть применены к коду, в котором есть with. Вот, к примеру, запустите этот код в современном браузере. Производительность функции fastсущественно отличается slow с пустым(!) with. И дело тут именно в with,


т.к. наличие этой конструкции препятствует оптимизации.

01 var i = 0; 02 03 function fast() { 04 i++; 05 } 06 07 function slow() { 08 with(i) {} 09 i++; 10 } 11 12 13 var time = new Date(); 14 while(i < 1000000) fast(); 15 alert(new Date - time); 16 17 var time = new Date(); 18 i=0; 19 while(i < 1000000) slow(); 20 alert(new Date - time); Замена with Вместо with рекомендуется использовать временную переменную, например:

01 /* вместо 02 with(elem.style) { 03 top = '10px'; 04 left = '20px'; 05 } 06 */ 07 08 var s = elem.style; 09 10 s.top = '10px'; 11 s.left = '0'; Это не так элегантно, но убирает лишний уровень вложенности и абсолютно точно понятно, что будет происходить и куда присвоятся свойства. Итого

• Конструкция with(obj) { ... } использует obj как дополнительную область видимости. Все переменные, к которым идет обращение внутри блока, сначала ищутся в obj.


• Конструкция with устарела и не рекомендуется по ряду причин. Избегайте её. 5

Какая из функций будет вызвана?

1 function f() { alert(1) } 2 3 var obj = { 4 f: function() { alert(2) } 5 }; 6 7 with(obj) { 8 f(); 9} Решение Вторая (2), т.к. при обращении к любой переменной внутри with — она ищется прежде всего в объекте. Соответственно, будет выведено 2:

1 function f() { alert(1) } 2 3 var obj = { 4 f: function() { alert(2) } 5 }; 6 7 with(obj) { 8 f(); 9} [Открыть задачу в новом окне] 5

Что выведет этот код?

1 var a = 1; 2 3 var obj = { b: 2 }; 4 5 with(obj) { 6 var b;


7 alert( a + b ); 8} Решение Выведет 3. Конструкция with не создаёт области видимости, её создают только функции. Поэтому объявление var b внутри конструкции работает также, как если бы оно было вне её. Код в задаче эквивалентен такому:

1 var a = 1; 2 var b; 3 4 var obj = { b: 2 } 5 6 with(obj) { 7 alert( a + b ); 8} [Открыть задачу в новом окне] Псевдо- массив arguments

1. Доступ к «лишним» аргументам 1. Пример использования: copy(dst, src1,...) 2. arguments.callee и arguments.callee.caller 1. arguments.callee 2. arguments.callee.caller 3. Почему callee и caller устарели? В JavaScript любая функция может быть вызвана с произвольным количеством аргументов. Например:

1 function go(a,b) { 2 alert("a="+a+", b="+b); 3} 4 5 go(1); // a=1, b=undefined 6 go(1,2); // a=1, b=2 7 go(1,2,3); // a=1, b=2, третий аргумент не вызовет ошибку


В JavaScript нет «перегрузки» функций В некоторых языках программист может создать две функции с одинаковым именем, но разным набором аргументов, а при вызове интерпретатор сам выберет нужную:

01 function log(a) { 02 ... 03 } 04 05 function log(a,b,c) { 06 ... 07 } 08 09 log(a); // вызовется первая функция 10 log(a,b,c); // вызовется вторая функция Это называется «полиморфизмом функций» или «перегрузкой функций». В JavaScript ничего подобного нет. Может быть только одна функция с именем log, которая вызывается с любыми аргументами. А уже внутри она может посмотреть, с чем вызвана и по-разному отработать. В примере выше второе объявление log просто переопределит первое. Доступ к « лишним» аргументам Как получить значения аргументов, которых нет в списке параметров? Доступ к ним осуществляется через «псевдо-массив» arguments. Он содержит список аргументов по номерам: arguments[0], arguments[1]…, а также свойствоlength. Например, выведем список всех аргументов:

1 function sayHi() { 2 for (var i=0; i<arguments.length; i++) { 3 alert("Привет, " + arguments[i]); 4 } 5} 6


7 sayHi("Винни", "Пятачок");

// 'Привет, Винни', 'Привет, Пятачок'

Все параметры находятся в arguments, даже если они есть в списке. Код выше сработал бы также, будь функция объявлена sayHi(a,b,c).

Связь между arguments и параметрами В старом стандарте JavaScript псевдо-массив arguments и переменныепараметры ссылаются на одни и те же значения. В результате изменения arguments влияют на параметры и наоборот. Например:

1 function f(x) { 2 arguments[0] = 5; // меняет переменную x 3 alert(x); // 5 4} 5 6 f(1); Наоборот:

1 function f(x) { 2 x = 5; 3 alert(arguments[0]); // 5, обновленный x 4} 5 6 f(1); В современной редакции стандарта это поведение изменено. Аргументы отделены от локальных переменных:

1 function f(x) { 2 "use strict"; // для браузеров с поддержкой строгого режима 3 4 arguments[0] = 5; 5 alert(x); // не 5, а 1! Переменная "отвязана" от arguments 6} 7


8 f(1); Если вы не используете строгий режим, то чтобы переменные не менялись «неожиданно», рекомендуется никогда не изменять arguments. Частая ошибка новичков — попытка применить методы Array к arguments. Это невозможно:

1 function sayHi() { 2 var a = arguments.shift(); // ошибка! нет такого метода! 3} 4 5 sayHi(1); Дело в том, что arguments — это не массив Array. В действительности, это обычный объект, просто ключи числовые и есть length. На этом сходство заканчивается. Никаких особых методов у него нет, и методы массивов он тоже не поддерживает. Впрочем, никто не мешает сделать обычный массив из arguments:

1 var args = []; 2 for(var i=0; i<arguments.length; i++) { 3 args[i] = arguments[i]; 4

}

Пример использования: copy(dst, src1,...) Иногда встаёт задача — скопировать в существующий объект свойства из одного или нескольких других. Напишем для этого функцию copy. Она будет работать с любым числом аргументов, благодаря использованию arguments. Синтаксис: copy(dst, src1, src2…) Копирует свойства из объектов src1, src2,... в объект dst. Возвращает получившийся объект. Использование:


• Для добавления свойств в объект user: 1 var user = { 2 name: "Вася", 3 }; 4 5 // добавить свойства 6 copy(user, { 7 age: 25, 8 surname:"Петров" 9 }); Использование copy позволяет сократить код, который потребовался бы для ручного копирования свойств:

user.age = ... user.surname = ... ... Объект user пишется только один раз, и общий смысл кода более ясен.

• Для создания копии объекта user: // скопирует все свойства в пустой объект var userClone = copy({}, user); Такой «клон» объекта может пригодиться там, где мы хотим изменять его свойства, при этом не трогая исходный объект user. В нашей реализации мы будем копировать только свойства первого уровня, то есть вложенные объекты не обрабатываются. Впрочем, её можно расширить. Теперь перейдём к реализации. Первый аргумент у copy всегда есть, поэтому укажем его в определении. А остальные будем получать из arguments, вот так:

1 function copy(dst) { 2 for (var i=1; i<arguments.length; i++) { 3 var obj = arguments[i]; 4 for (var key in obj) { 5 dst[key] = obj[key]; 6 } 7 } 8 return dst; 9} При желании, такое копирование можно реализовать рекурсивно. arguments.callee и arguments.callee.caller Объект arguments не только хранит список аргументов, но и обеспечивает доступ к ряду интересных свойств. В современном стандарте JavaScript они отсутствуют, но часто встречаются в старом коде.


arguments.callee Свойство arguments.callee содержит ссылку на функцию, которая выполняется в данный момент.

Это свойство устарело. Современная спецификация рекомендует использоватьименованные функциональные выражения (NFE). Браузеры могут более эффективно оптимизировать код, если arguments.callee не используется. Тем не менее, свойство arguments.callee зачастую удобнее, так как функцию с ним можно переименовывать как угодно и не надо менять ничего внутри. Кроме того, NFE некорректно работают в IE<9.

Например:

1 function f() { 2 alert( arguments.callee === f); // true 3} 4 5 f(); Зачем нужно делать какое-то свойство callee, если можно использовать просто f? Чтобы это понять, рассмотрим несколько другой пример. В JavaScript есть встроенная функция setTimeout(func, ms), которая вызывает func через msмиллисекунд, например:

1 // выведет 1 через 1000 ms (1 секунда) 2 setTimeout( function() { alert(1) }, 1000); Функция, которую вызывает setTimeout, объявлена как Function Expression, без имени. А что если хочется из самой функции вызвать себя еще раз? По имени-то обратиться нельзя. Как раз для таких случаев и придуман arguments.callee, который гарантирует обращение изнутри функции к самой себе. То есть, рекурсивный вызов будет выглядеть так:

1 setTimeout( 2 function() { 3 alert(1); 4 arguments.callee(); // вызвать себя


5 }, 6 1000 7) Аргументы можно передавать в arguments.callee() так же, как в обычную функцию. Пример с факториалом:

1 // factorial(n) = n*factorial(n-1) 2 var factorial = function(n) { 3 return n==1 ? 1 : n*arguments.callee(n-1); 4} Функция factorial не использует свое имя внутри , поэтому рекурсивные вызовы будут идти правильно, даже если функция «уехала» в другую переменную.

1 // factorial(n) = n*factorial(n-1) 2 var factorial = function(n) { 3 return n==1 ? 1 : n*arguments.callee(n-1); 4} 5 6 var g = factorial; 7 factorial = 0; // функция переместилась в переменную g 8 9 alert( g(5) ); // 120, работает! Рекомендованной альтернативой arguments.callee являются именованные функциональные выражения. arguments.callee.caller Свойство arguments.callee.caller хранит ссылку на функцию, которая вызвала данную. Это свойство устарело, аналогично arguments.callee. Также существует похожее свойство arguments.caller (без callee). Оно не кросс-браузерное, не используйте его. Свойство arguments.callee.callerподдерживается везде.

Например:

01 f1(); 02 03 function f1() { 04 alert(arguments.callee.caller); // null 05 f2();


06 } 07 08 function f2() { 09 alert(arguments.callee.caller); // f1, функция вызвавшая меня 10 } На практике это свойство используется очень редко, например для получения информации о стеке (текущей цепочке вложенных вызовов) в целях логирования ошибок. Но в современных браузерах существуют и другие способы это сделать. Почему callee и caller устарели? В современном стандарте эти свойства объявлены устаревшими, и использовать их не рекомендуется. Хотя де-факто они используются хотя бы потому, что IE до 9 версии не поддерживает Named Function Expression так, как должен. Причина отказа от этих свойств простая — интерпретатор может оптимизировать JavaScript более эффективно. Тем более, что для arguments.callee есть замена — NFE. 5

Как в функции отличить отсутствующий аргумент от undefined?

1 function f(x) { 2 // ..ваш код.. 3 // выведите 1, если первый аргумент есть, и 0 - если нет 4} 5 6 f(undefined); // 1 7 f(); // 0 Решение Узнать количество реально переданных аргументов можно по значениюarguments.length:

1 function f(x) { 2 alert(arguments.length ? 1 : 0); 3} 4 5 f(undefined); 6 f(); [Открыть задачу в новом окне] 5


Напишите функцию sum(...), которая возвращает сумму всех своих аргументов:

1 sum() = 0 2 sum(1) = 1 3 sum(1, 2) = 3 4 sum(1, 2 ,3) = 6 5 sum(1, 2, 3, 4) = 10 Решение

01 function sum() { 02 var result = 0; 03 04 for(var i=0; i<arguments.length; i++) { 05 result += arguments[i]; 06 } 07 08 return result; 09 } 10 11 alert( sum() ); // 0 12 alert( sum(1) ); // 1 13 alert( sum(1, 2) ); // 3 14 alert( sum(1, 2, 3) ); // 6 15 alert( sum(1, 2, 3, 4) ); // 10 [Открыть задачу в новом окне] Именованные аргументы

Именованные аргументы не имеют отношения к arguments. Это альтернативная техника работы с аргументами, которая позволяет обращаться к ним по имени, а не по номеру. Зачастую это гораздо удобнее. Представьте себе, что у вас есть функция с несколькими аргументами, причем большинство из них имеют значения по умолчанию. Например:

1 function showWarning(width, height, title, contents, showYesNo) { 2 width = width || 200; // почти все значения - по умолчанию 3 height = height || 100; 4 5 title = title || "Предупреждение";


6 7 //... 8} Функция showWarning позволяет указать ширину и высоту width, height, заголовок title, содержание contents и создает дополнительную кнопку, если showYesNo == true. Большинство этих параметров имеют значение по умолчанию. В примере выше значения по умолчанию:

• width = 200, • height = 100, • title = "Предупреждение". Если необязательный параметр находится в середине списка аргументов, то для передачи «значения по умолчанию» обычно используют null:

// width, height, title - по умолчанию showWarning(null, null, null, "Предупреждение", true); Неудобство такого вызова заключается в том, что порядок аргументов легко забыть или перепутать. Кроме того, «дырки» в списке аргументов — это не красиво. Обычно необязательные параметры переносятся в конец списка, но если таких большинство, то это невозможно. Для решения этой проблемы в Python, Ruby и многих языках существуют именованные аргументы(keyword arguments, named arguments). В JavaScript именованные параметры реализуются при помощи объекта. Вместо списка аргумента передается объект с параметрами, вот так:

1 function showWarning(options) { 2 var width = options.width || 200; // по умолчанию 3 var height = options.height || 100; 4 5 var title = options.title || "Предупреждение"; 6 7 // ... 8} Вызвать такую функцию очень легко. Достаточно передать объект аргументов, указав в нем только нужные:

1 showWarning({ 2 contents: "Вы вызвали функцию", 3 showYesNo: true 4 });


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

01 var opts = { 02 width: 400, 03 height: 200, 04 contents: "Текст", 05 showYesNo: true 06 }; 07 08 showWarning(opts); 09 10 opts.contents = "Другой текст"; 11 12 showWarning(opts); // не нужно копировать остальные аргументы в вызов Именованные аргументы применяются в большинстве JavaScript-фреймворков. Свои объекты: конструкторы и методы

1. 2. 3. 4. 5. 6.

Свои методы объектов Доступ к объекту через this Функция-конструктор, «new» Создание методов в конструкторе Приватные свойства Итого

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

01 var user = { 02 name: 'Василий', 03 04 // метод 05 sayHi: function() { 06 alert('Привет!'); 07 } 08 09 }; 10 11 // Вызов метода 12 user.sayHi();


Можно создать метод и позже, явным присвоением:

01 var user = { 02 name: 'Василий' 03 }; 04 05 user.sayHi = function() { 06 alert('Привет!'); 07 }; 08 09 // Вызов метода: 10 user.sayHi(); Доступ к объекту через this Для полноценной работы метод должен иметь доступ к данным объекта. В частности, вызовuser.sayHi() может захотеть вывести имя пользователя. Для доступа к объекту из метода используется ключевое слово this. Значением this является объект, в контексте которого вызван метод, например:

1 var user = { 2 name: 'Василий', 3 4 sayHi: function() { 5 alert( this.name ); 6 } 7 }; 8 9 user.sayHi(); Здесь при выполнении функции user.sayHi() в this будет храниться ссылка на текущий объектuser. В данном случае вместо this можно было бы использовать и переменную: alert( user.name ), но объект user может быть куда-то передан, переменная user перезаписана и т.п. Использование thisгарантирует, что функция работает именно с тем объектом, в контексте которого вызвана. Через this можно обратиться к любому свойству объекта, а при желании и передать его куда-то:

01 var user = { 02 name: 'Василий', 03


04 sayHi: function() { 05 showName(this); // передать текущий объект в showName 06 } 07 }; 08 09 function showName(obj) { 10 alert( obj.name ); 11 } 12 13 user.sayHi(); 5 Создайте объект calculator с тремя методами:

• readValues() запрашивает prompt два значения и сохраняет их как свойства объекта

• sum() возвращает сумму двух значений • mul() возвращает произведение двух значений На демо-страничке tutorial/intro/object/calculator.html показан результат кода:

1 var calculator = { 2 ... ваш код... 3} 4 5 calculator.readValues(); 6 alert( calculator.sum() ); 7 alert( calculator.mul() ); Решение tutorial/intro/object/calculator.html [Открыть задачу в новом окне] 2

Есть объект «лестница» ladder:

01 var ladder = { 02 step: 0, 03 up: function() { 04 this.step++; 05 },

// вверх по лестнице


06 down: function() { // вниз по лестнице 07 this.step--; 08 }, 09 showStep: function() { // вывести текущую ступеньку 10 alert(this.step); 11 } 12 }; Сейчас работать с ним скучно:

1 ladder.up(); 2 ladder.up(); 3 ladder.down(); 4 ladder.showStep(); // 1 Модифицируйте код, чтобы вызовы можно было делать цепочкой, вот так:

ladder.up().up().down().up().down().showStep();

// 1

Такой подход используется, например, во фреймворке jQuery. Решение Решение состоит в том, чтобы каждый раз возвращать текущий объект. Это делается добавлением return this в конце каждого метода:

01 var ladder = { 02 step: 0, 03 up: function() { 04 this.step++; 05 return this; 06 }, 07 down: function() { 08 this.step--; 09 return this; 10 }, 11 showStep: function() { 12 alert(this.step); 13 return this; 14 } 15 } 16


17 ladder.up().up().down().up().down().showStep();

// 1

[Открыть задачу в новом окне] Функция- конструктор, «new» Обычный синтаксис {...} позволяет создать один объект. Но зачастую нужно создать много однотипных объектов. Для этого используют функции, запуская их при помощи специального оператора new. Конструктором становится любая функция, вызванная через new. Например:

1 function Animal(name) { 2 this.name = name; 3 this.canWalk = true; 4} 5 6 var animal = new Animal("ёжик"); Любую функцию можно вызвать при помощи new. При этом она работает несколько иным образом, чем обычно: 1. Автоматически создается новый, пустой объект.

2. Специальное ключевое слово this получает ссылку на этот объект. 3. Функция выполняется. Как правило, она модифицирует this, добавляет методы, свойства.

4. Возвращается this. Так что результат выполнения примера выше — это объект:

1 animal = { 2 name: "ёжик", 3 canWalk: true 4} О создаваемом объекте говорят, что это «объект класса(или типа) Animal». Термин «класс» здесь является профессиональным жаргоном. Во многих других языках программирования есть специальная сущность «класс». В JavaScript её нет, но кое-что похожее организовать можно, поэтому так и называют.

Функция может возвратить другой объект вместо this Если функция явно возвращает объект, то будет возвращён он, а не this.


Например:

1 function BigAnimal() { 2 3 this.name = 'Мышь'; 4 5 return { name: 'Годзилла' }; 6} 7 8 alert( new BigAnimal().name );

// <-- будет возвращено

// Годзилла

Если функция возвращает не объект, к примеру, число, то такой вызов return ни на что не повлияет. Например:

1 function BigAnimal() { 2 3 this.name = 'Мышь'; 4 return 'Годзилла'; // не объект, такой return в режиме new ни 5 на что не влияет 6} 7 8 alert( new BigAnimal().name ); // Мышь Эта особенность работы new прописана в стандарте, знать о ней полезно, но используется она весьма редко.

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

Кстати, при вызове new без аргументов скобки можно не ставить:

var animal = new BigAnimal; // <-- без скобок // то же самое что var animal = new BigAnimal();

2


Возможны ли такие функции A и B в примере ниже, что соответствующие объекты a,b равны (см. код ниже)?

1 function A() { ... } 2 function B() { ... } 3 4 var a = new A; 5 var b = new B; 6 7 alert( a == b ); // true Если да — приведите пример кода с такими функциями. Решение Да, возможны. Они должны возвращать одинаковый объект. При этом если функция возвращает объект, то this не используется. Например, они могут вернуть один и тот же объект obj, определённый снаружи:

1 var obj = {}; 2 3 function A() { return obj; } 4 function B() { return obj; } 5 6 var a = new A; 7 var b = new B; 8 9 alert( a == b ); // true [Открыть задачу в новом окне] Создание методов в конструкторе Использование функций для создания объекта дает большую гибкость. Можно передавать функции свойства создаваемого объекта и параметры, определяющие как его создавать. Например, функция User(name) создает объект с заданным значением свойства name и методомsayHi:

1 function User(name) { 2 this.name = name;


3 4 this.sayHi = function() { 5 alert("Моё имя: " + this.name); 6 }; 7} Пример использования:

01 var ivan = new User("Иван"); 02 03 /* Объект ivan имеет вид: 04 { 05 name: "Иван", 06 sayHi: функция, обращение к имени идёт через this.name 07 } 08 */ 09 10 ivan.sayHi(); // Моё имя: Иван 5 Напишите функцию-конструктор Summator, которая создает объект с двумя методами:

• sum(a,b) возвращает сумму двух значений • run() запрашивает два значения при помощи prompt и выводит их сумму, используя метод sum. В итоге вызов new Summator().run() должен спрашивать два значения и выводить их сумму, вот так: tutorial/intro/object/summator2New.html Следует ли в вызове run сохранять введённые значения как свойства объекта? При этом метод sum() будет без параметров, и обращения в нём будут к this.a,this.b. Решение Код решения tutorial/intro/object/summator2New.html Ответ на вопрос Если a,b будут свойствами — то объект Summator получит «состояние». Их можно будет использовать в других функциях этого же объекта.


В данном случае это, скорее, полезно. С другой стороны, поток управления примитивнее, проще, если a,b — локальные переменные. А проще — это хорошо. Окончательный выбор делается в зависимости от дальнейших планов. Если имеет смысл сохранить эти переменные как состояние и использовать, то пусть будут свойства. [Открыть задачу в новом окне] Свойства объекта могут со временем изменяться. Используйте это в следующей задаче. 5

Напишите функцию-конструктор Adder(startingValue). Объекты, которые она создает, должны хранить текущую сумму и прибавлять к ней то, что вводит посетитель. Более формально, объект должен:

• Хранить текущее значение в своём свойстве value. Начальное значение свойства value ставится конструктором равным startingValue.

• Метод addInput() вызывает prompt, принимает число и прибавляет его к свойству value.

• Метод showValue() выводит текущее значение value. Таким образом, свойство value является текущей суммой всего, что ввел посетитель при вызовах метода addInput(), с учетом начального значения startingValue. По ссылке ниже вы можете посмотреть работу кода:

1 var adder = new Adder(1); // начальное значение 1 2 adder.addInput(); // прибавит ввод prompt к текущему значению 3 adder.addInput(); // прибавит ввод prompt к текущему значению 4 adder.showValue(); // выведет текущее значение Демо результата: tutorial/intro/object/adderNew.html Решение tutorial/intro/object/adderNew.html [Открыть задачу в новом окне]


Приватн ые свойства Локальные переменные функции-конструктора, с одной стороны, доступны вложенным функциям, с другой — недоступны снаружи. В объектно-ориентированном программировании это называется «приватный (private) доступ». Например, в коде ниже к name имеет доступ только метод say. Со стороны объекта, после его создания, больше никто не может получить name.

1 function User(name) { 2 3 this.say = function(phrase) { 4 alert(name + ' сказал: ' + phrase); 5 }; 6 7} 8 9 var user = new User('Вася'); Если бы name было свойством this.name — можно было бы получить его как user.name, а тут — локальная переменная. Приватный доступ.

Замыкания никак не связаны с this Доступ через замыкание осуществляется к локальной переменной, находящейся «выше» по области видимости. А this содержит ссылку на «текущий» объект — контекст вызова, и позволяет обращаться к его свойствам. С локальными переменными это никак не связано. Приватные свойства можно менять, например ниже метод this.upperCaseName() меняет приватное свойство name:

01 function User(name) { 02 03 this.upperCaseName = function() { 04 name = name.toUpperCase(); // <-- изменяет name из User 05 }; 06 07 this.say = function(phrase) { 08 alert(name + ' сказал: ' + phrase); // <-- получает name из User


09 }; 10 11 } 12 13 var user = new User('Вася'); 14 15 user.upperCaseName(); 16 17 user.say("Да здравствует ООП!") // ВАСЯ сказал: Да здравствует ООП! Вы помните, в главе Замыкания, функции изнутри мы говорили о скрытых ссылках [[Scope]] на внешний объект переменных? В этом примере user.upperCaseName.[[Scope]] иuser.say. [[Scope]] как раз ссылаются на один и тот же объект LexicalEnvironment, в контексте которого они были созданы. За счёт этого обе функции имеют доступ к name и другим локальным переменным. Все переменные конструктора User становятся приватными, так как доступны только через замыкание, из внутренних функций. 4

Можно ли из this.sayHi получить доступ к name из User и при этом обойтись без переименований?

1 function User(name) { 2 3 this.sayHi = function(name) { 4 // ваш код... ? 5 } 6} Решение Нет, нельзя. Локальная переменная полностью перекрывает внешнюю, обращение к ней становится невозможным. [Открыть задачу в новом окне] 5

Напишите конструктор Calculator, который создаёт расширяемые объектыкалькуляторы.


1. Первый шаг задачи: вызов calculate(str) принимает строку, например «1 + 2», с жёстко заданным форматом «ЧИСЛО операция ЧИСЛО» (по одному пробелу вокруг операции), и возвращает результат. Понимает плюс + и минус-. Пример использования:

var calc = new Calculator; alert(calc.calculate("3 + 7")); // 10 2. Второй шаг — добавить метод addMethod(name, func), который учит калькулятор новой операции. Он получает имя операции name и функцию от двух аргументов func(a,b), которая должна её реализовывать. Например, добавим операции умножить *, поделить / и возвести в степень **:

1 var powerCalc = new Calculator; 2 powerCalc.addMethod("*", function(a, b) { return a * b; }); 3 powerCalc.addMethod("/", function(a, b) { return a / b; }); powerCalc.addMethod("**", function(a, b) { returnMath.pow(a, 4 b); }); 5 6 var result = powerCalc.calculate("2 ** 3"); • Поддержка скобок и более сложных математических выражений в этой задаче не требуется. • Числа и операции могут состоять из нескольких символов. Между ними ровно один пробел. • Предусмотрите обработку ошибок. Какая она должна быть - решите сами. Решение Решение: tutorial/intro/object/calculator-extendable.html • Обратите внимание на хранение методов. Они просто добавляются к внутреннему объекту.

• Все проверки и преобразование к числу производятся в методе calculate. В дальнейшем он может быть расширен для поддержки более сложных выражений. [Открыть задачу в новом окне]


Итого У объекта могут быть методы:

• Свойство, значение которого - функция, называется методом объекта и может быть вызвано как obj.method(). При этом объект доступен как this. Объекты могут быть созданы при помощи функций-конструкторов:

• Любая функция может быть вызвана с new, при этом она получает новый пустой объект в качестве this, в который она добавляет свойства. Если функция не решит возвратить свой объект, то её результатом будет this.

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

Вызов функции с new Вызов в контексте объекта Вызов в режиме обычной функции Явное указание this: apply и call 1. Метод call 2. Метод apply 3. «Одалживание метода» 4. Делаем из arguments настоящий Array 5. «Переадресация» вызова через apply 5. Итого 1. 2. 3. 4.

Значение this в JavaScript не зависит от объекта, в котором создана функция. Оно определяется во время вызова. Любая функция может иметь в себе this. Совершенно неважно, объявлена она в объекте или вне него. Значение this называется контекстом вызова и будет определено в момент вызова функции. Например: такая функция вполне допустима:

function sayHi() { alert( this.firstName ); } Эта функция ещё не знает, каким будет this. Это выяснится при выполнении программы. Есть несколько правил, по которым JavaScript устанавливает this. В ызов функции с new При вызове функции с new, значением this является новосоздаваемый объект. Мы уже обсуждали это в разделе о создании объектов.


В ызов в контексте объекта Самый распространенный случай — когда функция объявлена в объекте или присваивается ему, как в примере ниже:

01 var user = { 02 firstName: "Вася" 03 }; 04 05 function func() { 06 alert( this.firstName ); 07 } 08 09 user.sayHi = func; 10 11 user.sayHi(); // this = user При вызове функции как метода объекта, через точку или квадратные скобки — функция получает вthis этот объект. В данном случае user.sayHi() присвоит this = user. Если одну и ту же функцию запускать в контексте разных объектов, она будет получать разный this:

01 var user = { firstName: "Вася" }; 02 var admin = { firstName: "Админ" }; 03 04 function func() { 05 alert( this.firstName ); 06 } 07 08 user.a = func; // присвоим одну функцию в свойства 09 admin.b = func; // двух разных объектов user и admin 10 11 user.a(); // Вася admin['b'](); // Админ (не важно, доступ через точку или квадратные 12 скобки) Значение this не зависит от того, как функция была создана, оно определяется исключительно в момент вызова. В ызов в режиме об ычной функции Если функция использует this - это подразумевает работу с объектом. Но и прямой вызов func()технически возможен.


Как правило, такая ситуация возникает при ошибке в разработке. При этом this получает значение window, глобального объекта.

1 function func() { 2 alert(this); // выведет [object Window] или [object global] 3} 4 5 func(); В современном стандарте языка это поведение изменено, вместо глобального объекта this будетundefined.

1 function func() { 2 "use strict"; 3 alert(this); // выведет undefined (кроме IE<10) 4} 5 6 func(); …Но по умолчанию браузеры ведут себя по-старому. Явное указание this: apply и call Функцию можно вызвать, явно указав значение this. Для этого у неё есть два метода: call и apply. Метод call Синтаксис метода call:

func.call(context, arg1, arg2,...) При этом вызывается функция func, первый аргумент call становится её this, а остальные передаются «как есть». Вызов func.call(context, a, b...) — то же, что обычный вызов func(a, b...), но с явно указанным контекстом context. Например, функция showName в примере ниже вызывается через call в контексте объекта user:

01 var user = { 02 firstName: "Василий", 03 lastName: "Петров" 04 }; 05


06 function showName() { 07 alert( this.firstName + ' ' + this.lastName ); 08 } 09 10 showName.call(user) // "Василий Петров" Можно сделать её более универсальной, добавив аргументы:

01 var user = { 02 firstName: "Василий", 03 surname: "Петров" 04 }; 05 06 function getName(a, b) { 07 alert( this[a] + ' ' + this[b] ); 08 } 09 10 getName.call(user, 'firstName', 'surname')

// "Василий Петров"

Здесь функция getName вызвана с контекстом this = user и выводит user['firstName'] иuser['surname']. Метод apply Метод call жёстко фиксирует количество аргументов, через запятую:

f.call(context, 1, 2, 3); ..А что, если мы захотим вызвать функцию с четырьмя аргументами? А что, если количество аргументов заранее неизвестно, и определяется во время выполнения? Для решения этой задачи существует метод apply. Вызов функции при помощи func.apply работает аналогично func.call, но принимает массив аргументов вместо списка:

func.call(context, arg1, arg2...) // то же что и: func.apply(context, [arg1, arg2 ... ]); Эти две строчки cработают одинаково:

getName.call(user, 'firstName', 'surname'); getName.apply(user, ['firstName', 'surname']); Метод apply гораздо мощнее, чем call, так как можно сформировать массив аргументов динамически:

1 var args = [];


2 args.push('firstName'); 3 args.push('surname'); 4 func.apply(user, args); // вызовет func('firstName', 'surname') c 5 this=user

Вызов call/apply с null или undefined При указании первого аргумента null или undefined в call/apply, функция получает this = window:

1 function f() { alert(this) } 2 3 f.call(null); // window Это поведение исправлено в современном стандарте (15.3). Если функция работает в строгом режиме, то this передаётся «как есть»:

1 function f() { 2 "use strict"; 3 4 alert(this); // null, "как есть" 5} 6

7

f.cal l(nul l);

«Одалживание метода» При помощи call/apply можно легко взять метод одного объекта, в том числе встроенного, и вызвать в контексте другого. В JavaScript методы объекта, даже встроенные — это функции. Поэтому можно скопировать функцию, даже встроенную, из одного объекта в другой. Это называется «одалживание метода» (на англ. method borrowing). Используем эту технику для упрощения манипуляций с arguments. Как мы знаем, это не массив, а обычный объект.. Но как бы хотелось вызывать на нём методы массива.


1 function sayHi() { 2 arguments.join = [].join; // одолжили метод (1) 3 4 var argStr = arguments.join(':'); // (2) 5 6 alert(argStr); // сработает и выведет 1:2:3 7} 8 9 sayHi(1, 2, 3); В строке (1) создали массив. У него есть метод [].join(..), но мы не вызываем его, а копируем, как и любое другое свойство в объект arguments. В строке (2) запустили его, как будто он всегда там был.

Почему вызов сработает?

Здесь метод join массива скопирован и вызван в контексте arguments. Не произойдёт ли что-то плохое от того, что arguments — не массив? Почему он, вообще, сработал? Ответ на эти вопросы простой. В соответствии со спецификацией, внутри joinреализован примерно так:

01 function join(separator) { 02 if (!this.length) return ''; 03 04 var str = this[0]; 05 06 for (var i = 1; i<this.length; i++) { 07 str += separator + this[i]; 08 } 09 10 return str; 11 } Как видно, используется this, числовые индексы и свойство length. Если эти свойства есть, то все в порядке. А больше ничего и не нужно. Подходит даже обычный объект:

1 var obj = { 2 0: "А", 3 1: "Б",

// обычный объект с числовыми индексами и length


4 2: "В", 5 length: 3 6 }; 7 8 obj.join = [].join;

9

alert( obj.join(';') ); // "A;Б;В"

…Однако, прямое копирование метода не всегда приемлемо. Представим на минуту, что вместо arguments у нас — произвольный объект, и мы хотим вызвать в его контексте метод [].join. Копировать этот метод, как мы делали выше, опасно: вдруг у объекта есть свой собственный join? Перезапишем, а потом что-то сломается.. Для безопасного вызова используем apply/call:

01 function sayHi() { 02 var join = [].join; // ссылка на функцию теперь в переменной 03 04 // вызовем join с this=arguments, 05 // этот вызов эквивалентен arguments.join(':') из примера выше 06 var argStr = join.call(arguments, ':'); 07 08 alert(argStr); // сработает и выведет 1:2:3 09 } 10 11 sayHi(1, 2, 3); Мы вызвали метод без копирования. Чисто, безопасно. Делаем из arguments настоящий Array В JavaScript есть очень простой способ сделать из arguments настоящий массив. Для этого возьмём метод массива: arr.slice(start, end). По стандарту он копирует часть массива arr от start до end в новый массив. А если start и end не указаны, то копирует весь массив. Вызовем его в контексте arguments:

1 function sayHi() {


2 // вызов arr.slice() скопирует все элементы из this в новый массив 3 var args = [].slice.call(arguments); 4 5 alert( args.join(':') ); // args -- массив аргументов 6} 7 8 sayHi(1,2); Как и в случае с join, такой вызов возможен потому, что slice использует от массива только нумерованные свойства и length. Всё это в arguments есть. «Переадресация» вызова через apply При помощи apply мы можем сделать универсальную «переадресацию» вызова из одной фунции в другую. Например, функция f вызывает g в том же контексте, с теми же аргументами:

function f(a, b) { g.apply(this, arguments); } Плюс этого подхода — в том, что он полностью универсален:

• Его не понадобится менять, если в f добавятся новые аргументы. • Если f является методом объекта, то текущий контекст также будет передан. Если не является — то this здесь вроде как не при чём, но и вреда от него не будет. 5

Напишите функцию f, которая будет обёрткой вокруг другой функции g. Функция f обрабатывает первый аргумент сама, а остальные аргументы передаёт в функцию g, сколько бы их ни было. Например:

1 function f() { ... } 2 3 function g(a, b, c) { 4 alert( (a || 0) + (b || 0) + (c || 0) ); 5} 6 7 f("тест", 1, 2); // f выведет "тест", дальше g посчитает сумму "3" f("тест2", 1, 2, 3); // f выведет "тест2", дальше g посчитает 8 сумму "6" Код функции f не должен зависеть от количества аргументов.


Решение

01 function f(a) { 02 alert(a); 03 var args = [].slice.call(arguments, 1); 04 g.apply(this, args); 05 } 06 07 function g(a, b, c) { 08 alert( (a || 0) + (b || 0) + (c || 0) ); 09 } 10 11 f("тест", 1, 2); 12 f("тест2", 1, 2, 3); [Открыть задачу в новом окне] 3

Вызовы (1) и (2) в примере ниже работают не так, как (3) и (4):

01 "use strict" 02 03 var obj, f; 04 05 obj = { 06 go: function() { alert(this); } 07 }; 08 09 obj.go(); // (1) object 10 11 (obj.go)(); // (2) object 12 13 (f = obj.go)(); // (3) undefined 14 15 (obj.x || obj.go)(); // (4) undefined В чём дело? Объясните логику работы this. Решение 1. Обычный вызов функции в контексте объекта. 2. То же самое, скобки ни на что не влияют.


3. Здесь не просто вызов obj.method(), а более сложный вызов вида(выражение).method(). Такой вызов работает, как если бы он был разбит на две строки:

f = obj.go; // сначала вычислить выражение f(); // потом вызвать то, что получилось При этом f() выполняется как обычная функция, без передачи this. 4. Здесь также слева от точки находится выражение, вызов аналогичен двум строкам. В спецификации это объясняется при помощи специального внутреннего типаReference Type. Если подробнее — то obj.go() состоит из двух операций:

1. Сначала получить свойство obj.go. 2. Потом вызвать его как функцию. Но откуда на шаге 2 получить this? Как раз для этого операция получения свойстваobj.go возвращает значение особого типа Reference Type, который в дополнение к свойству go содержит информацию об obj. Далее, на втором шаге, вызов его при помощи скобок () правильно устанавливает this. Любые другие операции, кроме вызова, превращают Reference Type в обычный тип, в данном случае — функцию go (так уж этот тип устроен). Поэтому получается, что (a = obj.go) присваивает в переменную a функцию go, уже без всякой информации об объекте obj. Аналогичная ситуация и в случае (4): оператор ИЛИ || делает из Reference Typeобычную функцию. [Открыть задачу в новом окне] 2

Каков будет результат этого кода?

1 var obj = { 2 go: function() { alert(this) } 3} 4 5 (obj.go || 0)()


P.S. Есть подвох. Решение Решение, шаг 1 Ошибка! Попробуйте:

1 var obj = { 2 go: function() { alert(this) } 3} 4 5 (obj.go || 0)() // error! Причем сообщение об ошибке - очень странное. В большинстве браузеров этоobj is undefined. Дело, как ни странно, ни в самом объявлении obj, а в том, что после него пропущена точка с запятой. JavaScript игнорирует перевод строки перед скобкой (obj.go || ..) и читает этот код как:

var obj = { go:... }(obj.go || 0)() Интерпретатор попытается вычислить это выражение, которое обозначает вызов объекта { go: ... } как функции с аргументом (obj.go || 0). При этом, естественно, возникнет ошибка. А что будет, если добавить точку с запятой?

1 obj = { 2 go: function() { alert(this); } 3 }; 4 5 (obj.go || 0)(); Все ли будет в порядке? Каков будет результат? Решение, шаг 2


Результат — window, поскольку вызов obj.go || 0 аналогичен коду:

1 obj = 2 go: 3 }; 4 5 var f 6 f();

{ function() { alert(this); }

= obj.go || 0; // эти две строки - аналог (obj.go || 0)(); // window [Открыть задачу в новом окне]

5

Каким будет результат? Почему?

1 arr = ["a", "b"]; 2 3 arr.push( function() { alert(this); } ) 4 5 arr[2](); // ? Решение Вызов arr[2]() — это обращение к методу объекта obj[method](), в роли objвыступает arr, а в роли метода: 2. Поэтому, как это бывает при вызове функции как метода, функция arr[2] получитthis = arr и выведет массив:

1 arr = ["a", "b"]; 2 3 arr.push( function() { alert(this); } ) 4 5 arr[2](); // "a","b",function [Открыть задачу в новом окне] Итого Значение this устанавливается в зависимости от того, как вызвана функция: При вызове функции как метода

obj.func(...) // this = obj obj["func"](...)


При обычном вызове

func(...)

// this = window

new func()

// this = {} (новый объект)

В new Явное указание

func.apply(ctx, args) // this = ctx (новый объект) func.call(ctx, arg1, arg2, ...)

Приём программирования "Декоратор"

1. 2. 3. 4.

Пример декоратора Ещё пример Зачем декораторы? Задачи

Декоратор — приём программирования, который позволяет взять существующую функцию и изменить/расширить ее поведение. Декоратор получает функцию и возвращает обертку, которая модифицирует (декорирует) её поведение, оставляя синтаксис вызова тем же. Пример декоратора Например, у нас есть функция sum(a,b):

function sum(a, b) { return a + b; } Создадим декоратор doublingDecorator, который меняет поведение, увеличивая результат работы функции в два раза:

01 function doublingDecorator(f) { 02 return function() { 03 return 2*f.apply(this, arguments); // (*) 04 }; 05 } 06 07 // Использование: 08 09 function sum(a, b) { 10 return a + b; 11 } 12 13 sum = doublingDecorator(sum); 14


15 alert( sum(1,2) ); // 6 16 alert( sum(2,3) ); // 10 Декоратор doublingDecorator создает анонимную функцию-обертку, которая в строке (*) вызываетf при помощи apply с тем же контекстом this и аргументами arguments, а затем удваивает результат. Этот декоратор можно применить два раза:

1 sum = doublingDecorator(sum); 2 sum = doublingDecorator(sum); 3 4 alert( sum(1,2) ); // 12, т.е. 3 умножается на 4 Контекст this в sum никак не используется, поэтому можно бы было вызватьf.apply(null, arguments). Ещё пример Посмотрим еще пример. Предположим, у нас есть функция isAdmin(), которая возвращает true, если у посетителя есть права администратора. Можно создать универсальный декоратор, который добавляет в функцию проверку прав: Например, создадим декоратор checkPermissionDecorator(f). Он будет возвращать обертку, которая передает вызов f в том случае, если у посетителя достаточно прав:

1 function checkPermissionDecorator(f) { 2 return function() { 3 if ( isAdmin() ) { 4 return f.apply(this, arguments); 5 } 6 alert('Недостаточно прав'); 7 } 8} Использование декоратора:

1 function save() { ... } 2 3 save = checkPermissionDecorator(save); 4 // Теперь вызов функции save() проверяет права Декораторы можно использовать в любых комбинациях:

sum = checkPermissionDecorator(sum); sum = doublingDecorator(sum); // ...


Зачем декораторы? Декораторы меняют поведение функции прозрачным образом.

1. Декораторы можно повторно использовать. Например, doublingDecorator можно применить не только к sum, но и к multiply, divide. Декоратор для проверки прав можно применить к любой функции. 2. Несколько декораторов можно скомбинировать. Это придает дополнительную гибкость коду. Примеры использования есть в задачах. Задачи 5

Создайте декоратор makeLogging(f, log), который берет функцию f и массив log. Он должен возвращать обёртку вокруг f, которая при каждом вызове записывает («логирует») аргументы в log, а затем передает вызов в f. В этой задаче можно считать, что у функции f ровно один аргумент. Работать должно так:

01 function work(a) { 02 /* ... */ // work - произвольная функция, один аргумент 03 } 04 05 function makeLogging(f, log) { /* ваш код */ } 06 07 var log = []; 08 work = makeLogging(work, log); 09 10 work(1); // 1, добавлено в log 11 work(5); // 5, добавлено в log 12 13 for(var i=0; i<log.length; i++) { 14 alert( 'Лог:' + log[i] ); // "Лог:1", затем "Лог:5" 15 } Решение Возвратим декоратор wrapper который будет записывать аргумент в log и передавать вызов в f:


01 function work(a) { 02 /*...*/ // work - произвольная функция, один аргумент 03 } 04 05 function makeLogging(f, log) { 06 07 function wrapper(a) { 08 log.push(a); 09 return f.call(this, a); 10 } 11 12 return wrapper; 13 } 14 15 var log = []; 16 work = makeLogging(work, log); 17 18 work(1); // 1 19 work(5); // 5 20 21 for(var i=0; i<log.length; i++) { 22 alert( 'Лог:' + log[i] ); // "Лог:1", затем "Лог:5" 23 } При вызове f.call на всякий случай передадим и this, ведь функция может быть вызвана и в контексте объекта. [Открыть задачу в новом окне] 3

Создайте декоратор makeLogging(func, log), для функции funcвозвращающий обёртку, которая при каждом вызове добавляет её аргументы в массив log. Условие аналогично задаче Логирующий декоратор (1 аргумент), но допускаетсяfunc с любым набором аргументов. Работать должно так:

01 function work(a, b) { 02 alert(a + b); // work - произвольная функция 03 }


04 05 function makeLogging(f, log) { /* ваш код */ } 06 07 var log = []; 08 work = makeLogging(work, log); 09 10 work(1, 2); // 3 11 work(4, 5); // 9 12 13 for(var i=0; i<log.length; i++) { alert( 'Лог:' + [].join.call(log[i]) ); // "Лог:1,2", 14 "Лог:4,5" 15 } Решение Решение аналогично задаче Логирующий декоратор (1 аргумент), разница в том, что в в лог вместо одного аргумента идет весь объект arguments. Для передачи вызова с произвольным количеством аргументов используемf.apply(this, arguments).

01 function work(a, b) { 02 alert(a + b); // work - произвольная функция 03 } 04 05 function makeLogging(f, log) { 06 07 function wrapper() { 08 log.push(arguments); 09 return f.apply(this, arguments); 10 } 11 12 return wrapper; 13 } 14 15 var log = []; 16 work = makeLogging(work, log); 17 18 work(1, 2); // 3 19 work(4, 5); // 9 20 21 for(var i=0; i<log.length; i++) { alert( 'Лог:' + [].join.call(log[i]) ); // "Лог:1,2", 22 "Лог:4,5"


23 } [Открыть задачу в новом окне] 5

Создайте декоратор makeCaching(f), который берет функцию f и возвращает обертку, которая кеширует её результаты. В этой задаче функция f имеет только один аргумент, и он является числом.

1. При первом вызове обертки с определенным аргументом — она вызывает f и запоминает значение. 2. При втором и последующих вызовах с тем же аргументом возвращается запомненное значение. Должно работать так:

01 function f(arg) { 02 return Math.random()*arg; // может быть любой функцией 03 } 04 05 function makeCaching(f) { /* ваш код */ } 06 07 f = makeCaching(f); 08 09 var a, b; 10 11 a = f(1); 12 b = f(1); 13 alert( a == b ); // true (значение закешировано) 14 15 b = f(2); 16 alert( a == b ); // false, другой аргумент => другое значение Решение Запоминать результаты вызова функции будем в замыкании, в объектеcache: { ключ:значение }.

01 function f(x) { 02 return Math.random()*x; 03 } 04


05 function makeCaching(f) { 06 var cache = {}; 07 08 return function(x) { 09 if (!(x in cache)) { 10 cache[x] = f.call(this, x); 11 } 12 return cache[x]; 13 }; 14 15 } 16 17 f = makeCaching(f); 18 19 var a = f(1); 20 var b = f(1); 21 alert( a == b ); // true (значение закешировано) 22 23 b = f(2); 24 alert( a == b ); // false, другой аргумент => другое значение Обратите внимание: проверка на наличие уже подсчитанного значения выглядит так:if (x in cache). Менее универсально можно проверить так: if (cache[x]), это если мы точно знаем, что cache[x] никогда не будет false, 0 и т.п. [Открыть задачу в новом окне] Преобразование объектов: toString и valueOf

1. 2. 3. 4.

Строковое преобразование Численное преобразование Преобразование в примитив Итого

Ранее, в главе Преобразование типов мы рассматривали преобразование типов, уделяя основное внимание примитивам. Теперь добавим в нашу стройную картину преобразования типов объекты. При этом будут следующие изменения: 1. В объектах численное и строковое преобразования можно переопределить. 2. Если операция требует примитивное значение, то сначала объект преобразуется к примитиву, а затем — всё остальное. При этом для преобразования к примитиву используется численное преобразование. Для того, чтобы лучше интегрировать их в общую картину, рассмотрим примеры.


Строковое преобразование Строковое преобразование проще всего увидеть, если вывести объект при помощи alert:

1 var user = { 2 firstName: 'Василий' 3 }; 4 5 alert(user); // [object Object] Как видно, содержимое объекта не вывелось. Это потому, что стандартным строковым представлением пользовательского объекта является строка "[object Object]". Такой вывод объекта не содержит интересной информации. Поэтому имеет смысл его поменять на что-то более полезное. Если в объекте присутствует метод toString, который возвращает примитив, то он используется для преобразования.

01 var user = { 02 03 firstName: 'Василий', 04 05 toString: function() { 06 return 'Пользователь ' + this.firstName; 07 } 08 }; 09 10 alert( user ); // Пользователь Василий

Результатом toString может быть любой примитив Метод toString не обязан возвращать именно строку. Его результат может быть любого примитивного типа. Например, это может быть число, как в примере ниже:

1 var obj = { 2 toString: function() { return 123; } 3 }; 4 5 alert(obj); // 123


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

alert( [1,2] ); // toString для массивов выводит список элементов "1,2" 2 alert( new Date ); // toString для дат выводит дату в виде строки 1

Численное преобразование Для численного преобразования объекта используется метод valueOf, а если его нет — тоtoString:

01 var room = { 02 number: 777, 03 04 valueOf: function() { return this.number; }, 05 toString: function() { return this.number; } 06 }; 07 08 alert( +room ); // 777, вызвался valueOf 09 10 delete room.valueOf; 11 12 alert( +room ); // 777, вызвался toString Метод valueOf обязан возвращать примитивное значение, иначе его результат будет проигнорирован. При этом — не обязательно числовое. У большинства встроенных объектов такого valueOf нет, поэтому численное и строковое преобразования для них работают одинаково. Исключением является объект Date, который поддерживает оба типа преобразований:

1 alert( new Date() ); // toString: Дата в виде читаемой строки alert( +new Date() ); // valueOf: кол-во миллисекунд, прошедших с 2 01.01.1970

Стандартный valueOf


Если посмотреть в стандарт, то в 15.2.4.4 определён valueOf для любых объектов. Но он ничего не делает, просто возвращает сам объект (не-примитивное значение!), а потому игнорируется. Преобразование в примитив Большинство операций, которые ожидают примитивное значение, при виде объекта сначала преобразуют его к примитиву. Например, арифметическая операция или сравнение > < >= <= сначала преобразует объект в примитив. А затем уже идёт операция с примитивами, при этом возможны последующие преобразования. При приведении объекта к примитиву используется численное преобразование. Например:

1 var obj = { 2 valueOf: function() { return 1; } 3 }; 4 5 // операции сравнения, кроме ===, приводят к примитиву 6 alert(obj == true); // true, объект приведён к примитиву 1 7 8 // бинарный + приводит к примитиву, а затем складывает 9 alert(obj + "test"); // 1test Пример ниже демонстрирует, что несмотря на то, что приведение «численное» — его результатом может быть любое примитивное значение, не обязательно число:

1 var a = { 2 valueOf: function() { return "1"; } 3 }; 4 var b = { 5 valueOf: function() { return true; } 6 }; 7 8 alert(a + b); // "1" + true = "1true" После того, как объект приведён к примитиву, могут быть дополнительные преобразования. Например:

1 var a = { 2 valueOf: function() { return "1"; }


3 }; 4 var b = { 5 valueOf: function() { return "2"; } 6 }; 7 8 alert(a - b); // "1" - "2" = -1

Исключение: Date Существует исключение: объект Date преобразуется в примитив, используя строковое преобразование. С этим можно столкнуться в операторе "+":

1 // бинарный вариант, преобразование к примитиву 2 alert( new Date + "" ); // "строка даты" 3 4 // унарный вариант, наравне с - * / и другими приводит к числу 5 alert( +new Date ); // число миллисекунд

Это исключение явно прописано в стандарте и является единственным в своём роде.

Как испугать Java-разработчика В языке Java логические значения можно создавать, используя синтаксисnew Boolean(true/false). Также можно преобразовывать значения к логическому типу, применяя к ним new Boolean. В JavaScript тоже есть подобная возможность, которая возвращает «объектную обёртку» для логического значения. Эта возможность сохраняется для совместимости и не используется на практике, поскольку приводит к странным результатам. Например:

1 var value = new Boolean(false); 2 if ( value ) { 3 alert(true); // сработает! 4}


Почему запустился alert? Ведь в if находится false… Проверим:

1 var value = new Boolean(false); 2 3 alert(value); // выводит false, все ок.. 4 5 if ( value ) { 6 alert(true); // ..но тогда почему выполняется alert в if ?!? 7} Дело в том, что new Boolean - это объект. В логическом контексте он, безусловно,true. Поэтому работает первый пример. А второй пример вызывает alert, который преобразует объект к строке, и он становится "false". Чтобы преобразовать значение к логическому типу, нужно использовать двойное отрицание: !!val или прямой вызов Boolean(val). Итого

• При строковом преобразовании объекта используется его метод toString. Он должен возвращать примитивное значение, причём не обязательно именно строку. В стандарте прописано, что если toString нет, или он возвращает не примитив, а объект, то вызывается valueOf, но обычно toString есть.

• При численном преобразовании объекта используется метод valueOf, а если его нет, тоtoString. У встроенных объектов valueOf обычно нет.

• При операции над объектом, которая требует примитивное значение, объект первым делом преобразуется в примитив. Для этого используется численное преобразование, исключение — встроенный объект Date. Полный алгоритм преобразований есть в спецификации EcmaScript, смотрите пункты 11.8.5,11.9.3, а также 9.1 и 9.3. 5

Почему результат true ?

1 alert( ['x'] == 'x' ); Решение


Если с одной стороны — объект, а с другой — нет, то сначала приводится объект. В данном случае сравнение означает численное приведение. У массивов нетvalueOf, поэтому вызывается toString, который возвращает список элементов через запятую. В данном случае, элемент только один - он и возвращается. Так что ['x']становится 'x'. Получилось 'x' == 'x', верно. P.S. По той же причине верны равенства:

1 alert( ['x','y'] == 'x,y' ); // true 2 alert( [] == '' ); // true [Открыть задачу в новом окне] 5

Объявлен объект с toString и valueOf. Какими будут результаты alert?

01 var foo = { 02 toString: function () { 03 return 'foo'; 04 }, 05 valueOf: function () { 06 return 2; 07 } 08 }; 09 10 alert(foo); 11 alert(foo + 1); 12 alert(foo + "3"); Подумайте, прежде чем ответить. Решение Ответы, один за другим.


alert(foo)

Возвращает строковое представление объекта, используя toString, т.е."foo".

alert(foo + 1)

Оператор '+' преобразует объект к примитиву, используя valueOf, так что результат: 3.

alert(foo + '3')

То же самое, что и предыдущий случай, объект превращается в примитив 2. Затем происходит сложение 2 + '3'. Оператор '+' при сложении чего-либо со строкой приводит и второй операнд к строке, а затем применяет конкатенацию, так что результат — строка "23".

[Открыть задачу в новом окне] 5

Почему первое равенство — неверно, а второе — верно?

1 alert( [] == [] ); // false 2 alert( [] == ![] ); // true

Какие преобразования происходят при вычислении?


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

1. Первым делом, обе части сравнения вычисляются. Справа находится ! []. Логическое НЕ '!' преобразует аргумент к логическому типу. Массив является объектом, так что это true. Значит, правая часть становится! [] = !true = false. Так что получили:

alert( [] == false ); 2. Проверка равенства между объектом и примитивом вызывает численное преобразование объекта. У массива нет valueOf, сработает toString и преобразует массив в список элементов, то есть - в пустую строку:

alert( '' == false ); 3. Сравнение различных типов вызывает численное преобразование слева и справа:

alert( 0 == 0 ); Теперь результат очевиден. [Открыть задачу в новом окне] 5

Подумайте, какой результат будет у выражений ниже. Когда закончите — сверьтесь с решением.

1 new Date(0) - 0 2 new Array(1)[0] + "" 3 ({})[0] 4 [1] + 1 5 [1,2] + [3,4] 6 [] + null + 1 7 [[0]][0][0] 8 ({} + {})


Решение

1 new Date(0) - 0 = 0 // (1) 2 new Array(1)[0] + "" = "undefined" // (2) 3 ({})[0] = undefined // (3) 4 [1] + 1 = "11" // (4) 5 [1,2] + [3,4] = "1,23,4" // (5) 6 [] + null + 1 = "null1" // (6) 7 [[0]][0][0] = 0 // (7) 8 ({} + {}) = "[object Object][object Object]" // (8) 1. new Date(0) — дата, созданная по миллисекундам и соответствующая 0мс от 1 января 1970 года 00:00:00 UTC. Оператор минус - преобразует дату обратно в число миллисекунд, то есть в 0.

2. new Array(num) при вызове с единственным аргументом-числом создаёт массив данной длины, без элементов. Поэтому его нулевой элемент равенundefined, при сложении со строкой получается строка "undefined".

3. Фигурные скобки — это создание пустого объекта, у него нет свойства '0'. Так что значением будет undefined. Обратите внимание на внешние, круглые скобки. Если их убрать и запустить{}[0] в отладочной консоли браузера — будет 0, т.к. скобки {} будут восприняты как пустой блок кода, после которого идёт массив.

4. Массив преобразуется в строку "1". Оператор "+" при сложении со строкой приводит второй аргумент к строке — значит будет "1" + "1" = "11". 5. Массивы приводятся к строке и складываются.

6. Массив преобразуется в пустую строку "" + null + 1, оператор "+" видит, что слева строка и преобразует null к строке, получается "null" + 1, и в итоге "null1".

7. [[0]] — это вложенный массив [0] внутри внешнего [ ]. Затем мы берём от него нулевой элемент, и потом еще раз. Если это непонятно, то посмотрите на такой пример:

alert( [1,[0],2][1] ); Квадратные скобки после массива/объекта обозначают не другой массив, а взятие элемента.

8. Каждый объект преобразуется к примитиву. У встроенных объектов Object нет подходящего valueOf, поэтому используется toString, так что складываются в итоге строковые представления объектов.


[Открыть задачу в новом окне] 2

Напишите функцию sum, которая будет работать так:

1 sum(1)(2) == 3; // 1 + 2 2 sum(1)(2)(3) == 6; // 1 + 2 + 3 3 sum(5)(-1)(2) == 6 4 sum(6)(-1)(-2)(-3) == 0 5 sum(0)(1)(2)(3)(4)(5) == 15 Количество скобок может быть любым. Пример такой функции для двух аргументов — есть в решении задачи Сумма через замыкание. Решение Решение, шаг 1 Чтобы sum(1), а также sum(1)(2) можно было вызвать новыми скобками — результатом sum должна быть функция. Но эта функция также должна уметь превращаться в число. Для этого нужно дать ей соответствующий valueOf. А если мы хотим, чтобы и в строковом контексте она вела себя так же — то toString. Решение, шаг 2 Функция, которая возвращается sum, должна накапливать значение при каждом вызове. Удобнее всего хранить его в замыкании, в переменной currentSum. Каждый вызов прибавляет к ней очередное значение:

01 function sum(a) { 02 03 var currentSum = a; 04 05 function f(b) { 06 currentSum += b; 07 return f;


08 } 09 10 f.toString = function() { return currentSum; }; 11 12 return f; 13 } 14 15 alert( sum(1)(2) ); // 3 16 alert( sum(5)(-1)(2) ); // 6 17 alert( sum(6)(-1)(-2)(-3) ); // 0 18 alert( sum(0)(1)(2)(3)(4)(5) ); // 15 При внимательном взгляде на решение легко заметить, что функция sumсрабатывает только один раз. Она возвращает функцию f. Затем, при каждом запуске функция f добавляет параметр к сумме currentSum, хранящейся в замыкании, и возвращает сама себя. В последней строчке f нет рекурсивного вызова. Вот так была бы рекурсия:

1 function f(b) { 2 currentSum += b; 3 return f(); // <-- подвызов 4} А в нашем случае, мы просто возвращаем саму функцию, ничего не вызывая.

1 function f(b) { 2 currentSum += b; return f; // <-- не вызывает сама себя, а возвращает ссылку на 3 себя

Эта f используется при следующем вызове, опять возвратит себя, и так сколько нужно раз. Затем, при использовании в строчном или численном контексте — сработает toString, который вернет текущую сумму currentSum. [Открыть задачу в новом окне] Оператор typeof, [[Class]] и утиная типизация

1. Оператор typeof 2. [[Class]] для встроенных объектов 3. «Утиная» типизация


4. Проверка типа для пользовательских объектов 5. Полиморфизм 6. Итого В этой главе мы рассмотрим, как создавать полиморфные функции, то есть такие, которые поразному обрабатывают аргументы, в зависимости от их типа. Например, функция вывода может по-разному форматировать числа и даты. Для реализации такой возможности нужен способ определить тип переменной. И здесь в JavaScript есть целый «зоопарк» способов, но мы в нём сейчас разберемся.

Как мы знаем, существует несколько примитивных типов: null Специальный тип, содержит только значение null. undefined Специальный тип, содержит только значение undefined. number Числа: 0, 3.14, а также значения NaN и Infinity boolean true, false. string Строки, такие как "Мяу" или пустая строка "". Все остальные значения являются объектами, включая функции и массивы. Оператор typeof Оператор typeof возвращает тип аргумента. У него есть два синтаксиса:

1. Синтаксис оператора: typeof x. 2. Синтаксис функции: typeof(x). Работают они одинаково, но первый синтаксис короче. Результатом typeof является строка, содержащая тип:

01 typeof 02 03 typeof 04 05 typeof 06 07 typeof 08 09 typeof 10 11 typeof 12 typeof

undefined // "undefined" 0

// "number"

true // "boolean" "foo" // "string" {} // "object" null // "object" function(){} // "function"

Последние две строки помечены, потому что typeof ведет себя в них по-особому.


1. Результат typeof null == "object" — это официально признанная ошибка в языке, которая сохраняется для совместимости. На самом деле null — это не объект, а примитив. Это сразу видно, если попытаться присвоить ему свойство:

1 var x = null; 2 x.prop = 1; // ошибка, т.к. нельзя присвоить свойство примитиву 2. Для функции f значением typeof f является "function". Конечно же, функция является объектом. С другой стороны, такое выделение функций на практике скорее плюс, т.к. позволяет легко определить функцию.

Не используйте typeof для проверки переменной В старом коде можно иногда увидеть код вроде такого:

if (typeof jQuery !== 'undefined') { ... }

Его автор, видимо, хочет проверить, существует ли переменная jQuery. Причём, он имеет в виду именно глобальную переменную jQuery, которая создаётся во внешнем скрипте, так как про свои локальные он и так всё знает.

Более короткий код if (jQuery) выдаст ошибку, если переменная не определена, аtypeof jQuery в таких случаях ошибку не выдаёт, а возвращает undefined. Но как раз здесь typeof не нужен! Есть другой способ:

1 if (window.jQuery !== undefined) { ... } 2 3 //а если мы знаем, что это объект, то проверку можно упростить: 4 if (window.jQuery) { ... }

При доступе к глобальной переменной через window не будет ошибки, ведь по синтаксису это — обращение к свойству объекта, а такие обращения при отсутствующем свойстве просто возвращают undefined.

Оператор typeof надежно работает с примитивными типами, кроме null, а также с


функциями. Но обычные объекты, массивы и даты для typeof все на одно лицо, они имеют тип 'object':

1 alert( typeof {} ); // 'object' 2 alert( typeof [] ); // 'object' 3 alert( typeof new Date ); // 'object' Поэтому различить их при помощи typeof нельзя. [[Class]] для встроенн ых объектов Основная проблема typeof — неумение различать объекты, кроме функций. Но есть и другой способ получения типа. У всех встроенных объектов есть скрытое свойство [[Class]]. Оно равно "Array" для массивов, "Date" для дат и т.п. Это свойство нельзя получить напрямую, есть трюк для того, чтобы прочитать его. Дело в том, что toString от стандартного объекта выводит [[Class]] в небольшой обертке. Например:

1 var obj = {}; 2 alert( obj ); // [object Object] Здесь внутри [object ...] указано как раз значение [[Class]], которое для обычного объекта как раз и есть "Object". Для дат оно будет Date, для массивов — Array и т.п. К сожалению, большинство объектов переопределяют toString на свой собственный. Поэтому мы будем использовать технику, которая называется «одалживание метода» («method borrowing»). Мы возьмем функцию toString от стандартного объекта и будем запускать его в контексте тех значений, для которых нужно получить тип:

01 var toClass = {}.toString; // (1) 02 03 var arr = [1,2]; 04 alert( toClass.call(arr) ); // (2) [object Array] 05 06 var date = new Date; 07 alert( toClass.call(date) ); // [object Date] 08 09 var type = toClass.call(date).slice(8, -1); // (3) 10 alert(type); // Date


Разберем происходящее более подробно. 1. Можно переписать эту строку в две:

var obj = {}; var toClass = obj.toString; Иначе говоря, мы создаём пустой объект {} и копируем ссылку на его метод toString в переменную toClass. Мы делаем это, потому что внутренняя реализация toString стандартного объекта Objectвозвращает [[Class]]. У других объектов (Date, Array и т.п.) toString свой и для этой цели не подойдёт.

2. Вызываем скопированный метод в контексте нужного объекта obj. Мы могли бы поступить проще:

1 var arr = [1,2]; 2 arr.toClass = {}.toString; 3 4 alert( arr.toClass() ); // [object Array] …Но зачем копировать лишнее свойство в объект? Синтаксис toClass.call(arr) делает то же самое, поэтому используем его.

3. Всё, класс получен. При желании можно убрать обёртку [object ...], взяв подстроку вызовомslice(8,-1). Метод также работает с примитивами:

1 alert( {}.toString.call(123) ); // [object Number] 2 alert( {}.toString.call("строка") ); // [object String] …Но без use strict вызов call с аргументами null или undefined передает this = window. Таково поведение старого стандарта JavaScript. Этот метод может дать тип только для встроенных объектов. Для пользовательских конструкторов всегда [[Class]] = "Object":

1 function Animal(name) { 2 this.name = name; 3} 4 var animal = new Animal("Винни-пух"); 5 6 var type = {}.toString.call( animal ); 7 8 alert(type); // [object Object]


Вызов {}.toString в консоли может выдать ошибку При тестировании кода в консоли вы можете обнаружить, что если ввести в командную строку {}.toString.call(...) — будет ошибка. С другой стороны, вызов alert( {}.toString... ) — работает. Эта ошибка возникает потому, что фигурные скобки { } в основном потоке кода интерпретируются как блок. Интерпретатор читает {}.toString.call(...) так:

{ } // пустой блок кода .toString.call(...) // а что это за точка в начале? не понимаю, ошибка!

Фигурные скобки считаются объектом, только если они находятся в контексте выражения. В частности, оборачивание в скобки ( {}.toString... ) тоже сработает нормально. «Утиная » типизация Утиная типизация основана на одной известной пословице: «If it looks like a duck, swims like a duck and quacks like a duck, then it probably is a duck (who cares what it really is)». В переводе: «Если это выглядит как утка, плавает как утка и крякает как утка, то, вероятно, это утка (какая разница, что это на самом деле)». Смысл утиной типизации — в проверке методов и свойств, безотносительно типа объекта. Проверить массив мы можем, уточнив наличие метода splice:

1 var x = [1,2,3]; 2 3 if (x.splice) { 4 alert('Массив!'); 5} Обратите внимание — в if(x.splice) мы не вызываем метод x.splice(), а пробуем получить само свойство x.splice. Для массивов оно всегда есть и является функцией, т.е. даст в логическом контексте true. Проверить на дату можно, проверив наличие метода getTime:

1 var x = new Date();


2 3 if (x.getTime) { 4 alert('Дата!'); 5} С виду такая проверка хрупка, ее можно сломать, передав похожий объект с тем же методом. Но на практике утиная типизация хорошо и стабильно работает, особенно когда важен не сам тип, а поддержка методов. Проверка типа для пользовательских объектов Для проверки, кем был создан объект, есть оператор instanceof. Синтаксис: obj instanceof Func. Например:

1 function Animal(name) { 2 this.name = name; 3} 4 var animal = new Animal("Винни-пух"); 5 6 alert( animal instanceof Animal ); // true Оператор instanceof также работает для встроенных объектов:

1 var d = new Date(); 2 alert(d instanceof Date); // true 3 4 function f() { } 5 alert(f instanceof Function); // true Оператор instanceof может лишь осуществить проверку, он не позволяет получить тип в виде строки, но в большинстве случаев этого и не требуется.

Почему [[Class]] надежнее instanceof? Как мы видели, instanceof успешно работает со встроенными объектами:

1 var arr = [1,2,3]; 2 alert(arr instanceof Array); // true


…Однако, есть случай, когда такой способ нас подведет. А именно, если объект создан в другом окне или iframe, а оттуда передан в текущее окно. При этом arr instanceof Array вернет false, т.к. в каждом окне и фрейме — свой собственный набор встроенных объектов. Массив arr является Array в контексте того window. Метод [[Class]] свободен от этого недостатка. Полиморфизм Используем проверку типов для того, чтобы создать полиморфную функцию sayHi. Она будет работать в трёх режимах:

1. Без аргументов: выводит Привет. 2. С аргументом, который не является массивом: выводит «привет» и этот аргумент. 3. С аргументом, который является массивом — говорит всем «привет». Пример такой функции:

01 function sayHi(who) { 02 if (!arguments.length) { 03 alert('Привет'); 04 return; 05 } 06 07 if ( {}.toString.call(who) == '[object Array]' ) { 08 for(var i=0; i<who.length; i++) sayHi(who[i]); 09 return; 10 } 11 12 alert('Привет, ' + who); 13 } 14 15 // Использование: 16 sayHi(); // Привет 17 sayHi("Вася"); // Привет, Вася 18 sayHi( ["Саша", "Петя", ["Маша", "Юля"] ] ); // Привет 19 Саша..Петя..Маша..Юля Обратите внимание, получилась даже поддержка вложенных массивов


Итого Для получения типа есть два способа: typeof Хорош для примитивов и функций, врёт про null. Свойство [[Class]] Можно получить, используя {}.toString.call(obj). Это свойство содержит тип для встроенных объектов и примитивов, кроме null и undefined. Для проверки типов есть еще два способа: Утиная типизация Можно проверить поддержку метода. Оператор instanceof Работает с любыми объектами, встроенными и созданными посетителем при помощи конструкторов: if (obj instanceof User) { ... }. Используется проверка типа, как правило, для создания полиморфных функций, то есть таких, которые по-разному работают в зависимости от типа аргумента. 5

Напишите функцию outputDate(date), которая выводит дату в формате dd.mm.yy. Ее первый аргумент должен содержать дату в одном из видов:

1. 2. 3. 4.

Как объект Date. Как строку в формате yyyy-mm-dd. Как число секунд с 01.01.1970. Как массив [гггг, мм, дд], месяц начинается с нуля

Для этого вам понадобится определить тип данных аргумента и, при необходимости, преобразовать входные данные в нужный формат. Пример работы:

1 function outputDate(date) { /* ваш код */ } 2 3 outputDate( '2011-10-02' ); // 02.10.11 4 outputDate( 1234567890 ); // 14.02.09 5 outputDate( [2000,0,1] ); // 01.01.00 6 outputDate( new Date(2000,0,1) ); // 01.01.00 Решение


Для определения примитивного типа строка/число подойдет оператор typeof. Примеры его работы:

1 alert(typeof 2 alert(typeof 3 alert(typeof 4 alert(typeof

123); // "number" "строка"); // "string" new Date()); // "object" []); // "object"

Оператор typeof не умеет различать разные типы объектов, они для него все на одно лицо: "object". Поэтому он не сможет отличить Date от Array. Используем для того, чтобы их различать, свойство [[Class]]. Функция:

01 function outputDate(date) { 02 if (typeof date == 'number') { 03 // перевести секунды в миллисекунды и преобразовать к Date 04 date = new Date(date*1000); 05 } else if(typeof date == 'string') { 06 // разобрать строку и преобразовать к Date 07 date = date.split('-'); 08 date = new Date(date[0], date[1]-1, date[2]); 09 } else if ( {}.toString.call(date) == '[object Array]' ) { 10 date = new Date(date[0], date[1], date[2]); 11 } 12 13 var day = date.getDate(); 14 if (day < 10) day = '0' + day; 15 16 var month = date.getMonth()+1; 17 if (month < 10) month = '0' + month; 18 19 // взять 2 последние цифры года 20 var year = date.getFullYear() % 100; 21 if (year < 10) year = '0' + year; 22 23 var formattedDate = day + '.' + month + '.' + year; 24 25 alert(formattedDate);


26 } 27 28 outputDate( 29 outputDate( 30 outputDate( 31 outputDate(

'2011-10-02' ); // 02.10.11 1234567890 ); // 14.02.09 [2000,0,1] ); // 01.01.00 new Date(2000,0,1) ); // 01.01.00 [Открыть задачу в новом окне]

2

Есть функция для проверки, является ли значение встроенным объектом Object:

1 function isObject(x) { 2 if (x == null) return false; // (1) 3 4 return {}.toString.call(x) == "[object Object]"; // (2) 5} Как будет работать эта функция в случае вызова isObject(undefined)?

1. Вернёт ли она ответ сразу в строке: (1)? 2. Если да, то что будет, если убрать строку (1)? То есть, что вернёт{}.toString.call(undefined) и почему? Решение

1. Да. Так как undefined == null, то строка (1) завершит выполнение возвратом false.

2. Если убрать строку (1), то браузеры ведут себя более интересно… По идее, если нестрогий режим, то вызов f.call(null/undefined) должен передать в f глобальный объект в качестве контекста this. Так что браузер должен метод {}.toString запустить в контексте window. А далее — результат зависит от того, какое у него свойство [[Class]]. В реальности же — большинство браузеров применяют здесь современный стандарт и возвращают [object Undefined]. Попробуйте сами..

1 // возможны варианты (в зависимости от браузера) 2 // [object Undefined] 3 // [object Object]


4 // [object DOMWindow] 5 alert( {}.toString.call(undefined) ); [Открыть задачу в новом окне] setTimeout и setInterval

1. setTimeout 1. Параметры для функции и контекст 2. Отмена исполнения 2. setInterval 3. Очередь и наложение вызовов в setInterval 4. Повторение вложенным setTimeout 5. Минимальная задержка таймера 6. Реальная частота срабатывания 7. Разбивка долгих скриптов 8. Трюк setTimeout(func, 0) 9. Итого Почти все реализации JavaScript имеют внутренний таймер-планировщик, который позволяет задавать вызов функции через заданный период времени. В частности, эта возможность поддерживается в браузерах и в сервере Node.JS. setTimeout Синтаксис:

var timerId = setTimeout(func/code, delay[, arg1, arg2...]) Параметры: func/code Функция или строка кода для исполнения. Строка поддерживается для совместимости, использовать её не рекомендуется. delay Задержка в милисекундах, 1000 милисекунд равны 1 секунде. arg1, arg2… Аргументы, которые нужно передать функции. Не поддерживаются в IE9-. Исполнение функции произойдёт спустя время, указанное в параметре delay. Например, следующий код вызовет alert('Привет') через одну секунду:

1 function func() { 2 alert('Привет'); 3} 4 setTimeout(func, 1000); Если первый аргумент является строкой, то интерпретатор создаёт анонимную функцию из этой


строки. То есть такая запись работает точно так же:

1 setTimeout("alert('Привет')", 1000); Использование строк не рекомендуется, так как они могут вызвать проблемы при минимизации кода, и, вообще, сама возможность использовать строку сохраняется лишь для совместимости. Вместо них используйте анонимные функции:

1 setTimeout(function() { alert('Привет') }, 1000); Параметры для функции и контекст Во всех современных браузерах, с учетом IE10, setTimeout позволяет указать параметры функции. Пример ниже выведет "Привет, я Вася" везде, кроме IE9-:

1 function sayHi(who) { 2 alert("Привет, я " + who); 3} 4 5 setTimeout(sayHi, 1000, "Вася"); …Однако, в большинстве случаев нам нужна поддержка старого IE, а он не позволяет указывать аргументы. Поэтому, для того, чтобы их передать, оборачивают вызов в анонимную функцию:

1 function sayHi(who) { 2 alert("Привет, я " + who); 3} 4 5 setTimeout(function() { sayHi('Вася') }, 1000); Вызов через setTimeout не передаёт контекст this. В частности, вызов метода объекта через setTimeout сработает в глобальном контексте. Это может привести к некорректным результатам. Например, вызовем user.sayHi() через одну секунду:

01 function User(id) { 02 this.id = id; 03 04 this.sayHi = function() {


05 alert(this.id); 06 }; 07 } 08 09 var user = new User(12345); 10 setTimeout(user.sayHi, 1000); // ожидается 12345, но выведет 11 "undefined" Так как setTimeout запустит функцию user.sayHi в глобальном контексте, она не будет иметь доступ к объекту через this. Иначе говоря, эти два вызова setTimeout делают одно и то же:

1 // (1) одна строка 2 setTimeout(user.sayHi, 1000); 3 4 // (2) то же самое в две строки 5 var func = user.sayHi; 6 setTimeout(func, 1000); К счастью, эта проблема также легко решается созданием промежуточной функции:

01 function User(id) { 02 this.id = id; 03 04 this.sayHi = function() { 05 alert(this.id); 06 }; 07 } 08 09 var user = new User(12345); 10 11 setTimeout(function() { 12 user.sayHi(); 13 }, 1000); Функция-обёртка используется, чтобы кросс-браузерно передать аргументы и сохранить контекст выполнения. В следующих главах мы разберём дополнительные способы привязки функции к объекту. Отмена исполнения Функция setTimeout возвращает идентификатор timerId, который можно использовать для отмены действия.


Синтаксис: clearTimeout(timerId). В следующем примере мы ставим таймаут, а затем удаляем (передумали). В результате ничего не происходит.

1 var timerId = setTimeout(function() { alert(1) }, 1000); 2 3 clearTimeout(timerId); setInterval Метод setInterval имеет синтаксис, аналогичный setTimeout.

var timerId = setInterval(func/code, delay[, arg1, arg2...]) Смысл аргументов — тот же самый. Но, в отличие от setTimeout, он запускает выполнение функции не один раз, а регулярно повторяет её через указанный интервал времени. Остановить исполнение можно вызовом clearInterval(timerId). Следующий пример при запуске станет выводить сообщение каждые две секунды, пока вы не нажмете на кнопку «Стоп»:

1 <input type="button" onclick="clearInterval(timer)" value="Стоп"> 2 3 <script> 4 var i = 1; 5 var timer = setInterval(function() { alert(i++) }, 2000); 6 </script> Очередь и наложение вызовов в setInterval Вызов setInterval(функция, задержка) ставит функцию на исполнение через указанный интервал времени. Но здесь есть тонкость. На самом деле пауза между вызовами меньше, чем указанный интервал. Для примера, возьмем setInterval(function() { func(i++) }, 100). Она выполняет funcкаждые 100 мс, каждый раз увеличивая значение счетчика. На картинке ниже, красный блок - это время исполнения func. Время между блоком — это время между запусками функции, и оно меньше, чем установленная задержка!


То есть, браузер инициирует запуск функции аккуратно каждые 100мс, без учета времени выполнения самой функции. Бывает, что исполнение функции занимает больше времени, чем задержка. Например, функция сложная, а задержка маленькая. Или функция содержит операторы alert/confirm/prompt, которые блокируют поток выполнения. В этом случае начинаются интересные вещи Если запуск функции невозможен, потому что браузер занят — она становится в очередь и выполнится, как только браузер освободится. Изображение ниже иллюстрирует происходящее для функции, которая долго исполняется. Вызов функции, инициированный setInterval, добавляется в очередь и незамедлительно происходит, когда это становится возможным:

Второй запуск функции происходит сразу же после окончания первого:

Больше одного раза в очередь выполнение не ставится. Если выполнение функции занимает больше времени, чем несколько запланированных исполнений, то в очереди она всё равно будет стоять один раз. Так что «накопления» запусков не происходит. На изображении ниже setInterval пытается выполнить функцию в 200 мс и ставит вызов в очередь. В 300 мс и 400 мс таймер пробуждается снова, но ничего не просходит.

Давайте посмотрим на примере, как это работает.


Модальные окна в Safari/Chrome блокируют таймер Внутренний таймер в браузерах Safari/Chrome во время показаalert/confirm/prompt не «тикает». Если до исполнения оставалось 3 секунды, то даже при показе alert на протяжении минуты — задержка остаётся 3 секунды. Поэтому пример ниже не воспроизводится в этих браузерах. В других браузерах всё в порядке.

1. Запустите пример ниже в любом браузере, кроме Chrome/Safari и дождитесь всплывающего окна. Обратите внимание, что это alert. Пока модальное окошко отображается, исполнение JavaScript блокируется. Подождите немного и нажмите OK. 2. Вы должны увидеть, что второй запуск будет тут же, а третий - через небольшое время от второго, меньшее чем 2000 мс.

3. Чтобы остановить повторение, нажмите кнопку Стоп. 1 <input type="button" onclick="clearInterval(timer)" value="Стоп"> 2 3 <script> 4 var i = 1; 5 var timer = setInterval(function() { alert(i++) }, 2000); 6 </script> Происходит следующее. 1. Браузер выполняет функцию каждые 2 секунды

2. Когда всплывает окно alert — исполнение блокируется и остается заблокированным всё время, пока alert отображается. 3. Если вы ждете достаточно долго, то внутренние часики-то идут. Браузер ставит следующее исполнение в очередь, один раз (в Chrome/Safari внутренний таймер не идёт! это ошибка в браузере).

4. Когда вы нажмёте OK - моментально вызывается исполнение, которое было в очереди. 5. Следующее исполнение вызовется с меньшей задержкой, чем указано. Так происходит потому, что планировщик просыпается каждые 2000мс. И если alert был закрыт в момент времени, соответствующий 3500мс от начала, то следующее исполнение назначено на 4000 мс, т.е. произойдёт через 500мс. Вызов setInterval(функция, задержка) не гарантирует реальной задержки междуисполнениями. Бывают случаи, когда реальная задержка больше или меньше заданной. Вообще, не факт, что будет хоть какая-то задержка.


5

Напишите функцию, которая последовательно выводит в консоль числа от 1 до 20, с интервалом между числами 100мс. То есть, весь вывод должен занимать 2000мс, в течение которых каждые 100мс в консоли появляется очередное число. Нажмите на кнопку, открыв консоль, для демонстрации:

Решение задачи должно использовать setInterval. Решение

01 function printNumbersInterval20_100() { 02 var i = 1; 03 var timerId = setInterval(function() { 04 console.log(i); 05 if (i == 20) clearInterval(timerId); 06 i++; 07 }, 100); 08 } 09 10 // вызов 11 printNumbersInterval20_100(); [Открыть задачу в новом окне] Повторение вложенн ым setTimeout В случаях, когда нужно не просто регулярное повторение, а обязательна задержка между запусками, используется повторная установка setTimeout при каждом выполнении функции. Ниже — пример, который выдает alert с интервалами 2 секунды между ними.

01 <input type="button" onclick="clearTimeout(timer)" value="Стоп"> 02 03 <script> 04 var i = 1; 05 06 var timer = setTimeout(function run() { 07 alert(i++); 08 timer = setTimeout(run, 2000);


09 }, 2000); 10 11 </script> На временной линии выполнения будут фиксированные задержки между запусками. Иллюстрация для задержки 100мс:

5

Сделайте то же самое, что в задаче Вывод чисел каждые 100мс, но с использованием setTimeout вместо setInterval. Решение

01 function printNumbersTimeout20_100() { 02 var i = 1; 03 var timerId = setTimeout(function go() { 04 console.log(i); 05 if (i < 20) setTimeout(go, 100); 06 i++; 07 }, 100); 08 } 09 10 // вызов 11 printNumbersTimeout20_100(); [Открыть задачу в новом окне] Минимальная задержка таймера У браузерного таймера есть минимальная возможная задержка. Она меняется от примерно нуля до 4мс в современных браузерах. В более старых она может быть больше и достигать 15мс. По стандарту, минимальная задержка составляет 4мс. Так что нет разницы междуsetTimeout(..,1) и setTimeout(..,4). Посмотреть минимальное разрешение «вживую» можно на следующем примере.


В примере ниже находятся DIV'ы, каждый удлиняется вызовом setInterval с указанной в нём задержкой — от 0мс (сверху) до 20мс (внизу). Запустите его в различных браузерах, в частности, в Chrome и Firefox. Вы наверняка заметите, что несколько первых DIV'ов анимируются с одинаковой скоростью. Это как раз потому, что слишком маленькие задержки таймер не различает.

Object 1

Открыть в новом окне Открыть в песочнице В поведении setTimeout и setInterval с нулевой задержкой есть браузерные особенности.

• В Opera, setTimeout(.., 0) — то же самое, что setTimeout(.., 4). Оно выполняется реже, чем setTimeout(.. ,2). Это особенность данного браузера.

• В Internet Explorer, нулевая задержка setInterval(.., 0) не сработает. Это касается именноsetInterval, т.е. setTimeout(.., 0) работает нормально. Пример ниже реализует такую же анимацию, но через setTimeout. Если посмотреть его в различных браузерах, то можно заметить отличия от setInterval.

Object 2

Открыть в новом окне Открыть в песочнице


Реальная частота срабат ывания

Частота срабатывания может быть ещё меньше В ряде случаев задержка может быть не 4мс, а 30мс или даже 1000мс.

• Большинство браузеров (десктопных в первую очередь) продолжают выполнятьsetTimeout/setInterval, даже если вкладка неактивна. При этом ряд из них (Chrome, FF, IE10) снижают минимальную частоту таймера, до 1 раза в секунду. Получается, что в «фоновой» вкладке будет срабатывать таймер, но редко. • При работе от батареи, в ноутбуке — браузеры тоже могут снижать частоту, чтобы реже выполнять код и экономить заряд батареи. Особенно этим известен IE. Снижение может достигать нескольких раз, в зависимости от настроек.

• При слишком большой загрузке процессора JavaScript может не успевать обрабатывать таймеры вовремя. При этом некоторые запуски setIntervalбудут пропущены.

Вывод: на частоту 4мс стоит ориентироваться, но не стоит рассчитывать.

Посмотрим снижении частоты в действии на небольшом примере. При клике на кнопку ниже запускается setInterval(..., 90), который выводит список интервалов времени между 25 последними срабатываниями таймера. Запустите его. Перейдите на другую вкладку и вернитесь.

Вы можете увидеть, что: 1. Во-первых, таймер не является идеально точным. 2. Во-вторых, если браузер это поддерживает, фоновая вкладка обрабатывает таймер реже.

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


01 var timeMark = new Date; 02 setTimeout(function go() { 03 var diff = new Date - timeMark; 04 05 // вывести очередную задержку в консоль вместо страницы 06 console.log(diff); 07 08 // запомним время в самом конце, 09 // чтобы измерить задержку именно между вызовами 10 timeMark = new Date; 11 12 setTimeout(go, 100); } , 13

1 0 0 ) ;

Разбивка долгих скриптов

Нулевой или небольшой таймаут также используют, чтобы разорвать поток выполнения «тяжелых» скриптов. Например, скрипт для подсветки синтаксиса должен проанализировать код, создать много цветных элементов для подсветки и добавить их в документ — на большом файле это займёт много времени. Браузер сначала будет есть 100% процессора, а затем может выдать сообщение о том, что скрипт выполняется слишком долго. Для того, чтобы этого избежать, сложная задача разбивается на части, выполнение каждой части запускается через мини-интервал после предыдущей, чтобы дать браузеру время. Например, планируется подсветка 20 строк каждые 10мс. 5


Стоит задача: реализовать подсветику синтаксиса в длинном коде при помощи JavaScript, для онлайн-редактора кода. Это требует сложных вычислений, особенно загружает процессор генерация дополнительных элементов страницы, визуально осуществляющих подсветку. Поэтому решаем обрабатывать не весь код сразу, что привело бы к зависанию скрипта, а разбить работу на части: подсвечивать по 20 строк раз в 10мс. Как мы знаем, есть два варианта реализации такой подсветки:

1. Через setInterval, с остановкой по окончании работы: 1 timer = setInterval(function() { 2 if (есть еще что подсветить) highlight(); 3 else clearInterval(timer); 4 }, 10); 2. Через вложенные setTimeout: 1 setTimeout(function go() { 2 highlight(); 3 if (есть еще что подсветить) setTimeout(go, 10); 4 }, 10); Какой из них стоит использовать? Почему? Решение Нужно выбрать вариант 2, который гарантирует браузеру свободное время между выполнениями highlight. Первый вариант может загрузить процессор на 100%, если highlight занимает время, близкое к 10мс или, тем более, большее чем 10мс, т.к. таймер не учитывает время выполнения функции. Что интересно, в обоих случаях браузер не будет выводить предупреждение о том, что скрипт занимает много времени. Но от 100% загрузки процессора возможны притормаживания других операций. В общем, это совсем не то, что мы хотим, поэтому вариант 2. [Открыть задачу в новом окне] Трюк setTimeout(func, 0) Этот трюк достоин войти в анналы JavaScript-хаков.


Функцию оборачивают в setTimeout(func, 0), если хотят запустить ее после окончания текущего скрипта. Дело в том, что setTimeout никогда не выполняет функцию сразу. Он лишь планирует ее выполнение. Но интерпретатор JavaScript начнёт выполнять запланированные функции лишь после выполнения текущего скрипта. По стандарту, setTimeout в любом случае не может выполнить функцию с задержкой 0. Как мы говорили раньше, обычно задержка составит 4мс. Но главное здесь именно то, что выполнение в любом случае будет после выполнения текущего кода. Например:

01 var result; 02 03 function showResult() { 04 alert(result); 05 } 06 07 setTimeout(showResult, 0); 08 09 result = 2*2; 10 11 // выведет 4 Позже, в главе Управление порядком обработки, setTimeout(…0), мы рассмотрим различные применения этого трюка при работе с событиями. Итого Методы setInterval(func, delay) и setTimeout(func, delay) позволяют запускать funcрегулярно/один раз через delay миллисекунд. Оба метода возвращают идентификатор таймера. Его используют для остановки выполнения вызовомclearInterval/clearTimeout.

setInterval Тайминг

Особенности setTimeout

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

Рекурсивный вызов setTimeoutиспользуется вместо setInterval там, где нужна фиксированная пауза между выполнениями.


окончания одного запуска до начала другого может быть различным. Минимальная задержка: 4мс.

Минимальная задержка: 4мс.

Минимальная задержка для этих методов в современных браузерах различна и колеблется от примерно нуля до 4мс. В старых браузерах она может доходить до 15мс.

Задержка

Браузерные особенности

В IE не работает задержка 0.

В Opera нулевая задержка эквивалентна 4мс, остальные задержки обрабатываются точно, в том числе нестандартные 1мс, 2мс и 3мс.

5

Есть два бегуна:

var runner1 = new Runner(); var runner2 = new Runner(); У каждого есть метод step(), который делает шаг. Какой из двух бегунов будет быстрее?

01 // первый? 02 setInterval(function() { 03 runner1.step(); 04 }, 15); 05 06 // или второй? 07 setTimeout(function go() { 08 runner2.step(); 09 setTimeout(go, 15); 10 }, 15); 11 12 setTimeout(function() { 13 alert(runner1.steps); 14 alert(runner2.steps); 15 }, 5000); Кто сделает больше шагов? Почему вы так думаете? Решение


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

01 function Runner() { 02 this.steps = 0; 03 04 this.step = function() { 05 doSomethingHeavy(); 06 this.steps++; 07 } 08 09 function doSomethingHeavy() { 10 for(var i=0; i<10000; i++) { 11 this[i] = this.step + i; 12 } 13 } 14 15 } 16 17 var runner1 = new Runner(); 18 var runner2 = new Runner(); 19 20 // запускаем бегунов 21 setInterval(function() { 22 runner1.step(); 23 }, 15); 24 25 setTimeout(function go() { 26 runner2.step(); 27 setTimeout(go, 15); 28 }, 15); 29 30 // кто сделает больше шагов? 31 setTimeout(function() { 32 alert(runner1.steps); 33 alert(runner2.steps); 34 }, 5000);


• Если бы в шаге step() не было вызова doSomethingHeavy(), то количество шагов было бы равным, так как времени на такой шаг нужно очень мало. Интерпретатор JavaScript старается максимально оптимизировать такие часто повторяющиеся «узкие места». В данном случае вызов step свёлся бы к одной операции микропроцессора. Это пренебрежимо мало по сравнению с остальным кодом.

• Так как у нас шаг, всё же, что-то делает, и функция doSomethingHeavy()специально написана таким образом, что к одной операции свести её нельзя, то первый бегун успеет сделать больше шагов. Ведь в setTimeout пауза 15 мс будет между шагами, а setInterval шагает равномерно, каждые 15 мс. Получается чаще.

• Наконец, есть браузеры (IE9), в которых при выполнении JavaScript таймер «не тикает». Для них оба варианта будут вести себя как setTimeout, так что количество шагов будет одинаковым. [Открыть задачу в новом окне] 5

Выполнение функции f занимает примерно 1 секунду. Что выведет alert в коде ниже? Когда сработает setTimeout? Выберите нужный вариант:

1. 2. 3. 4.

До выполнения f. Во время выполнения f. Сразу же по окончании f. Через 10мс после окончания f.

01 setTimeout(function() { 02 alert(i); 03 }, 10); 04 05 var i; 06 07 function f() { 08 // точное время выполнения не играет роли 09 // здесь оно заведомо больше задержки setTimeout 10 for(i=0; i<1e8; i++) f[i%10] = i;


11 } 12 13 f(); Решение Вызов alert(i) в setTimeout выведет 100000000, так как срабатывание будет гарантировано после окончания работы текущего кода. Очередь до запланированных вызовов доходит всегда лишь после окончания текущего скрипта. Можете проверить это запуском:

01 setTimeout(function() { 02 alert(i); 03 }, 10); 04 05 var i; 06 07 function f() { 08 // точное время выполнения не играет роли 09 // здесь оно заведомо больше задержки setTimeout 10 for(i=0; i<1e8; i++) f[i%10] = i; 11 } 12 13 f(); Ответ на второй вопрос: 3 (сразу после). Вызов планируется на 10мс от времени вызова setTimeout, но функция выполняется больше, чем 10мс, поэтому к моменту ее окончания время уже подошло и отложенный вызов выполняется тут же. [Открыть задачу в новом окне] 5

Выполнение функции f занимает примерно 1 секунду. Что выведет alert в коде ниже? Когда сработает setInterval? Выберите нужный вариант:

1. До выполнения f, во время и после, перемежаясь с выполнением f.


2. 3. 4. 5. 6. 7.

Во время выполнения f, один раз. Во время выполнения f, возможно несколько раз. Сразу же по окончании f один раз. Сразу же по окончании f, возможно несколько раз. Через 10мс после окончания f, один раз. Через 10мс после окончания f, возможно несколько раз.

Является ли такое поведение кросс-браузерным?

01 var timer = setInterval(function() { 02 i++; 03 }, 10); 04 05 setTimeout(function() { 06 clearInterval(timer); 07 alert(i); 08 }, 50); 09 10 var i; 11 12 function f() { 13 // точное время выполнения не играет роли 14 // здесь оно заведомо больше 50мс 15 for(i=0; i<1e8; i++) f[i%10] = i; 16 } 17 18 f(); Решение Вызов alert(i) в setTimeout введет 100000001. Почему — будет понятно из ответа на второй вопрос. Можете проверить это запуском:

01 var timer = setInterval(function() { 02 i++; 03 }, 10); 04 05 setTimeout(function() { 06 clearInterval(timer); 07 alert(i); 08 }, 50);


09 10 var i; 11 12 function f() { 13 // точное время выполнения не играет роли 14 // здесь оно заведомо больше 50мс 15 for(i=0; i<1e8; i++) f[i%10] = i; 16 } 17 18 f(); Ответ на второй вопрос: 4 (сразу же по окончании f один раз). Планирование setInterval будет вызывать функцию каждые 10мс после текущего времени. Но так как интерпретатор занят долгой функцией, то до конца ее работы никакого вызова не происходит. За время выполнения f может пройти время, на которое запланированы несколько вызовов setInterval, но в этом случае остается только один, т.е. накопления вызовов не происходит. Такова логика работы setInterval. После окончания текущего скрипта интерпретатор обращается к очереди запланированных вызовов, видит в ней setInterval и выполняет. А затем тут же выполняется setTimeout, очередь которого тут же подошла. Итого, как раз и видим, что setInterval выполнился ровно 1 раз по окончании работы функции. Такое поведение кросс-браузерно. [Открыть задачу в новом окне] 5

Напишите функцию delay(f, ms), которая возвращает обёртку вокруг f, задерживающую вызов на ms миллисекунд. Например:

1 function f(x) { 2 alert(x); 3} 4 5 var f1000 = delay(f, 1000); 6 var f1500 = delay(f, 1500); 7 8 f1000("тест"); // выведет "тест" через 1000 миллисекунд


9 f1500("тест2"); // выведет "тест2" через 1500 миллисекунд Иначе говоря, f1000 — это «задержанный на 1000мс» вызов f. В примере выше у функции только один аргумент, но delay должна быть универсальной: передавать любое количество аргументов и контекст this. Решение

01 function delay(f, ms) { 02 03 return function() { 04 var savedThis = this; 05 var savedArgs = arguments; 06 07 setTimeout(function() { 08 f.apply(savedThis, savedArgs); 09 }, ms); 10 }; 11 12 } 13 14 function f(x) { 15 alert(x); 16 } 17 18 var f1000 = delay(f, 1000); 19 var f1500 = delay(f, 1500); 20 21 f1000("тест"); // выведет "тест" через 1000 миллисекунд 22 f1500("тест2"); // выведет "тест2" через 1500 миллисекунд Обратим внимание на то, как работает обёртка:

1 return function() { 2 var savedThis = this; 3 var savedArgs = arguments; 4 5 setTimeout(function() { 6 f.apply(savedThis , savedArgs); 7 }, ms); 8 };


Именно обёртка возвращается декоратором delay и будет вызвана. Чтобы передать аргумент и контекст функции, вызываемой через ms миллисекунд, они копируются в локальные переменные savedThis и savedArgs. Это один из самых простых, и в то же время удобных способов передать чтолибо в функцию, вызываемую через setTimeout. [Открыть задачу в новом окне] 5

Напишите функцию debounce(f, ms), которая возвращает обёртку, которая передаёт вызов f не чаще, чем раз в ms миллисекунд. «Лишние» вызовы игнорируются. Все аргументы и контекст — передаются. Например:

01 function f() { ... } 02 03 var f = debounce(f, 1000); 04 05 f(1); // выполнится сразу же 06 f(2); // игнор 07 setTimeout( function() { f(3) }, 100); // игнор (прошло только 08 100мс) 09 setTimeout( function() { f(4) }, 1100); // выполнится 10 setTimeout( function() { f(5) }, 1500); // игнор Исходный документ с более развёрнутым тестом: tutorial/timers/debounce-src.html Решение Решение: tutorial/timers/debounce.html Вызов debounce возвращает функцию-обёртку. Все необходимые данные для неё хранятся в замыкании. При вызове ставится таймер и состояние state меняется на константу COOLDOWN («в процессе охлаждения»). Последующие вызовы игнорируются, пока таймер не обнулит состояние. [Открыть задачу в новом окне] 5


Напишите функцию throttle(f, ms) — «тормозилку», которая возвращает обёртку, передающую вызов f не чаще, чем раз в msмиллисекунд. У этой функции должно быть важное существенное отличие от debounce: если игнорируемый вызов оказался последним, т.е. после него до окончания задержки ничего нет — то он выполнится. Чтобы лучше понять, откуда взялось это требование, и как throttle должна работать — разберём реальное применение, на которое и ориентирована эта задача. Например, нужно обрабатывать передвижения мыши. В JavaScript это делается функцией, которая будет запускаться при каждом микро-передвижении мыши и получать координаты курсора. По мере того, как мышь двигается, эта функция может запускаться очень часто, может быть 100 раз в секунду (каждые 10мс). Функция обработки передвижения должна обновлять некую информацию на странице. При этом обновление — слишком «тяжёлый» процесс, чтобы делать его при каждом микро-передвижении. Имеет смысл делать его раз в 100мс, не чаще. Пусть функция, которая осуществляет это обновление по передвижению, называется onmousemove. Вызов throttle(onmousemove, 100), по сути, предназначен для того, чтобы «притормаживать» обработку onmousemove. Технически, он должен возвращать обёртку, которая передаёт все вызовы onmousemove, но не чаще чем раз в 100мс. При этом промежуточные движения можно игнорировать, но мышь в конце концов где-то остановится. И это последнее, итоговое положение мыши обязательно нужно обработать! Визуально это даст следующую картину обработки перемещений мыши: 1. Первое обновление произойдёт сразу (это важно, посетитель тут же видит реакцию на своё действие). 2. Дальше будет много вызовов (микро-передвижений) с разными координатами, но пока не пройдёт 100мс — ничего не будет. 3. По истечении 100мс — опять обновление, с последними координатами. Промежуточные микро-передвижения игнорированы. 4. В конце концов мышь где-то остановится, обновление по окончании очередной паузы 100мс (иначе мы не знаем, последнее оно или нет) сработает с последними координатами.


Ещё раз заметим — задача из реальной жизни, а в ней принципиально важно, чтопоследнее передвижение обрабатывается. Пользователь должен увидеть, где остановил мышь. Чтобы было удобнее такой throttle писать, в исходном документе содержатся еще два теста: tutorial/timers/throttle-src.html Решение Решение: tutorial/timers/throttle.html Вызов throttle возвращает функцию-обёртку. Все необходимые данные для неё хранятся в замыкании. В первую очередь — это состояние state, которое вначале не назначено (null), при первом вызове получает значение COOLDOWN («в процессе охлаждения»), а при следующем вызове CALL_SCHEDULED. При таймауте состояние проверяется, и если оно равно CALL_SCHEDULED — происходит новый вызов. [Открыть задачу в новом окне] setImmediate

1. Метод setImmediate(func) 2. Тест производительности Функция, отложенная через setTimeout(..0) выполнится не ранее следующего «тика» таймера, минимальная частота которого может составлять от 4 до 1000мс. И, конечно же, это произойдёт после того, как все текущие изменения будут перерисованы. Но нужна ли нам эта дополнительная задержка? Как правило, используя setTimeout(func, 0), мы хотим перенести выполнение func на «ближайшее время после текущего кода», и какая-то дополнительная задержка нам не нужна. Если бы была нужна — мы бы её указали вторым аргументом вместо 0. Метод setImmediate(func) Для того, чтобы поставить функцию в очередь на выполнение без задержки, в Microsoft предложили метод setImmediate(func). Он реализован в IE10. У setImmediate единственный аргумент — это функция, выполнение которой нужно запланировать. В других браузерах setImmediate нет, но его можно эмулировать, используя, к примеру, методpostMessage, предназначенный для пересылки сообщений от одного окна другому. Детали работы с postMessage вы найдёте в статье Общение окон с разных доменов: postMessage . Желательно читать её после освоения темы «События». Эмуляция setImmediate с его помощью для всех браузеров, кроме IE<8 (в которых


нет postMessage, так что будет использован setTimeout): Раскрыть код Есть и более сложные эмуляции, включая MessageChannel для работы с Web Workers и хитрый метод для поддержки IE6-8: https://github.com/NobleJS/setImmediate. Все они по существу являются «хаками», направленными на то, чтобы обеспечить поддержку setImmediate в тех браузерах, где его нет. Тест производительности Чтобы сравнить реальную частоту срабатывания — измерим время на подсчет от 1 до 100 приsetTimeout/setImmediate:

Object 3

Открыть в новом окне Открыть в песочнице Запустите пример выше — и вы увидите реальную разницу во времени между setTimeout(.., 0) иsetImmediate. Да, она может быть более в 50, 100 и более раз. Позднее связывание "bindLate"

1. 2. 3. 4.

Раннее связывание Позднее связывание Привязка метода, которого нет Итого

Обычный метод bind называется «ранним связыванием», поскольку фиксирует привязку сразу же. Как только значения привязаны — они уже не могут быть изменены. В том числе, если метод объекта, который привязали, кто-то переопределит — «привязанная» функция этого не заметит. Позднее связывание — более гибкое, оно позволяет переопределить привязанный метод когда угодно. Раннее связ ывание Например, попытаемся переопределить метод при раннем связывании:

01 function bind(func, context) { 02 return function() { 03 return func.apply(context, arguments); 04 }; 05 } 06 07 var user = { 08 sayHi: function() { alert('Привет!'); } 09 }


10 11 // привязали метод к объекту 12 var userSayHi = bind(user.sayHi, user); 13 14 // понадобилось переопределить метод 15 user.sayHi = function() { alert('Новый метод!'); } 16 17 // будет вызван старый метод, а хотелось бы - новый! 18 userSayHi(); // выведет "Привет!" …Привязка всё ещё работает со старым методом, несмотря на то что он был переопределён. Позднее связ ывание При позднем связывании bind вызовет не ту функцию, которая была в sayHi на момент привязки, а ту, которая есть на момент вызова.** Встроенного метода для этого нет, поэтому нужно реализовать. Синтаксис будет таков:

var func = bindLate(obj, "method"); obj Объект method Название метода (строка) Код:

1 function bindLate(context, funcName) { 2 return function() { 3 return context[funcName].apply(context, arguments); 4 }; 5} Этот вызов похож на обычный bind, один из вариантов которого как раз и выглядит какbind(obj, "method"), но работает по-другому. Поиск метода в объекте: context[funcName], осуществляется при вызове, самой обёрткой. Поэтому, если метод переопределили — будет использован всегда последний вариант. В частности, пример, рассмотренный выше, станет работать правильно:

01 function bindLate(context, funcName) { 02 return function() {


03 return context[funcName].apply(context, arguments); 04 }; 05 } 06 07 var user = { 08 sayHi: function() { alert('Привет!'); } 09 } 10 11 var userSayHi = bindLate(user, 'sayHi'); 12 13 user.sayHi = function() { alert('Здравствуйте!'); } 14 15 userSayHi(); // Здравствуйте! Привязка метода, которого нет Позднее связывание позволяет привязать к объекту даже метод, которого ещё нет!

Конечно, предполагается, что к моменту вызова он уже будет определён

.

Например:

01 function bindLate(context, funcName) { 02 return function() { 03 return context[funcName].apply(context, arguments); 04 }; 05 } 06 07 // метода нет 08 var user = { }; 09 10 // ..а привязка возможна! 11 var userSayHi = bindLate(user, 'sayHi'); 12 13 // по ходу выполнения добавили метод.. 14 user.sayHi = function() { alert('Привет!'); } 15 16 userSayHi(); // Метод работает: Привет! В некотором смысле, позднее связывание всегда лучше, чем раннее. Оно удобнее и надежнее, так как всегда вызывает нужный метод, который в объекте сейчас. Но оно влечет и небольшие накладные расходы — поиск метода при каждом вызове.


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

1 function bindLate(context, funcName) { 2 return function() { 3 return context[funcName].apply(context, arguments); 4 }; 5}

Статические и фабричные методы объектов

1. 2. 3. 4. 5.

Статические свойства Статические методы Пример: сравнение объектов Фабричные методы Итого

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

1 function Article() { 2 Article.count++; 3} 4 5 Article.count = 0; // статическое свойство-переменная 6 Article.DEFAULT_FORMAT = "html"; // статическое свойство-константа Они хранят данные, специфичные не для одного объекта, а для всех статей целиком. В примере выше это общее количество статей Article.count и формат «по умолчанию»Article.DEFAULT_FORMAT. Статические метод ы Статические методы также привязывают к функции-конструктору. Примерами являются


встроенные методы String.fromCharCode, Date.parse. Article в примере ниже считает количество созданных объектов, а метод Article.showCount()выводит его:

01 function Article() { 02 Article.count++; 03 04 //... 05 } 06 Article.count = 0; 07 08 Article.showCount = function() { 09 alert(this.count); // (1) 10 } 11 12 // использование 13 new Article(); 14 new Article(); 15 Article.showCount(); // (2) Здесь Article.count — статическое свойство, а Article.showCount — статический метод. Обратите внимание на контекст this. Несмотря на то, что переменная и метод — статические, он всё ещё полезен. В строке (1) он равен Article! Пример: сравнение объектов Ещё один хороший способ применения — сравнение объектов. Например, у нас есть объект Journal для журналов. Журналы можно сравнивать — по толщине, по весу, по другим параметрам. Объявим «стандартную» функцию сравнения, которая будет сравнивать по дате издания. Эта функция сравнения, естественно, не привязана к конкретному журналу, но относится к журналам вообще. Поэтому зададим её как статический метод Journal.compare:

1 function Journal(date) { 2 this.date = date; 3 // ... 4} 5 6 // возвращает значение, большее 0, если A больше B, иначе меньшее 0


7 Journal.compare = function(journalA, journalB) { 8 return journalA.date - journalB.date; 9 }; В примере ниже эта функция используется для поиска самого раннего журнала из массива:

01 function Journal(date) { 02 this.date = date; 03 04 this.formatDate = function(date) { return date.getDate() + '.' + (date.getMonth()+1) + '.' + 05 date.getFullYear(); 06 }; 07 08 this.getTitle = function() { 09 return "Выпуск от " + this.formatDate(this.date); 10 }; 11 12 } 13 14 Journal.compare = function(journalA, journalB) { 15 return journalA.date - journalB.date; 16 }; 17 18 // использование: 19 var journals = [ 20 new Journal(new Date(2012,1,1)), 21 new Journal(new Date(2012,0,1)), 22 new Journal(new Date(2011,11,1)) 23 ]; 24 25 function findMin(journals) { 26 var min = 0; 27 for(var i=0; i<journals.length; i++) { 28 // используем статический метод 29 if ( Journal.compare(journals[min], journals[i]) > 0 ) min = i; 30 } 31 return journals[min]; 32 } 33 34 alert( findMin(journals).getTitle() ); Статический метод также можно создать для реализации функционала, который вообще не требует существования объекта.


Например, метод formatDate(date) можно сделать статическим. Он будет форматировать дату «как это принято в журналах», при этом его можно использовать в любом месте кода, не обязательно создавать журнал. Например:

1 function Journal() { /*...*/ } 2 3 Journal.formatDate = function(date) { return date.getDate() + '.' + (date.getMonth()+1) + '.' + 4 date.getFullYear(); 5} 6 7 // ни одного объекта Journal нет, просто форматируем дату 8 alert( Journal.formatDate(new Date) ); 5 Добавить в конструктор Article: • Подсчёт общего количества созданных объектов. • Запоминание даты последнего созданного объекта. Используйте для этого статические свойства. Пусть вызов Article.showStats() выводит то и другое. Использование:

01 function Article() { 02 this.created = new Date(); 03 // ... ваш код ... 04 } 05 06 new Article(); 07 new Article(); 08 09 Article.showStats(); // Всего: 2, Последняя: (дата) 10 11 new Article(); 12 13 Article.showStats(); // Всего: 3, Последняя: (дата) Решение


Решение (как вариант):

01 function Article() { 02 this.created = new Date; 03 04 Article.count++; // увеличиваем счетчик при каждом вызове 05 Article.last = this.created; // и запоминаем дату 06 } 07 Article.count = 0; // начальное значение 08 // (нельзя оставить undefined, т.к. Article.count++ будет NaN) 09 10 Article.showStats = function() { 11 alert('Всего: ' + this.count + ', Последняя: ' + this.last); 12 }; 13 14 new Article(); 15 new Article(); 16 17 Article.showStats(); // Всего: 2, Последняя: (дата) 18 19 new Article(); 20 21 Article.showStats(); // Всего: 3, Последняя: (дата) [Открыть задачу в новом окне] Фабричн ые метод ы Рассмотрим ситуацию, когда объект нужно создавать различными способами, по разным данным. Например, это реализовано во встроенном объекте Date. Он по-разному обрабатывает аргументы разных типов:

• • • •

new Date() — создаёт объект с текущей датой, new Date(milliseconds) — создаёт дату по количеству миллисекунд milliseconds, new Date(year, month, day ...) — создаёт дату по компонентам год, месяц, день… new Date(datestring) — читает дату из строки datestring

Фабричный статический метод — удобная альтернатива такому конструктору. Так называется статический метод, который служит для создания новых объектов (поэтому и называется «фабричным»). Пример встроенного фабричного метода — String.fromCharCode(code). Этот метод создает


строку из кода символа:

1 var str = String.fromCharCode(65); 2 alert(str); // 'A' Но строки — слишком простой пример, посмотрим что-нибудь посложнее. Допустим, нам нужно создавать объекты User: анонимные new User() и с даннымиnew User({name: 'Вася', age: 25}). Можно, конечно, создать полиморфную функцию-конструктор User:

01 function User(userData) { 02 if (userData) { // если указаны данные -- одна ветка if 03 this.name = userData.name; 04 this.age = userData.age; 05 } else { // если не указаны -- другая 06 this.name = 'Аноним'; 07 } 08 09 this.sayHi = function() { alert(this.name) }; 10 // ... 11 } 12 13 // Использование 14 15 var guest = new User(); 16 guest.sayHi(); // Аноним 17 18 var knownUser = new User({name: 'Вася', age: 25}); 19 knownUser.sayHi(); // Вася Подход с использованием фабричных методов был бы другим. Вместо разбора параметров в конструкторе — делаем два метода: User.createAnonymous и User.createFromData. Код:

01 function User() { 02 this.sayHi = function() { alert(this.name) }; 03 } 04 05 User.createAnonymous = function() { 06 var user = new User; 07 user.name = 'Аноним';


08 return user; 09 } 10 11 User.createFromData = function(userData) { 12 var user = new User; 13 user.name = userData.name; 14 user.age = userData.age; 15 return user; 16 } 17 18 // Использование 19 20 var guest = User.createAnonymous(); 21 guest.sayHi(); // Аноним 22 23 var knownUser = User.createFromData({name: 'Вася', age: 25}); 24 knownUser.sayHi(); // Вася Преимущества использования фабричных методов: 1. Лучшая читаемость кода. Как конструктора — вместо одной большой функции несколько маленьких, так и вызывающего кода — явно видно, что именно создаётся.

2. Лучший контроль ошибок, т.к. если в createFromData ничего не передали, то будет ошибка, а полиморфный конструктор создал бы анонимного посетителя. 3. Удобная расширяемость. Например, нужно добавить создание администратора, без аргументов. Фабричный метод сделать легко: User.createAdmin = function() { ... }. А для полиморфного конструктора вызов без аргумента создаст анонима, так что нужно добавить параметр — «тип посетителя» и усложнить этим код.

Поэтому полиморфные конструкторы лучше использовать там, где нужна именно полиморфность, т.е. когда непонятно, какого типа аргумент передадут, и хочется в одном конструкторе охватить все варианты. А в остальных случаях отличная альтернатива — фабричные методы. Итого Статические свойства и методы объекта удобно применять в следующих случаях: • Общие действия и подсчёты, имеющие отношения ко всем объектам данного типа. В примерах выше это подсчёт количества.


• Методы, не привязанные к конкретному объекту, например сравнение. • Вспомогательные методы, которые полезны вне объекта, например для форматирования даты. • Фабричные методы.

Массив: Перебирающие методы

1. 2. 3. 4. 5.

forEach filter map every/some reduce/reduceRight

Современный стандарт JavaScript предоставляет много методов для «умного» перебора массивов. Для их поддержки в IE<9 подключите библиотеку ES5-shim. forEach Метод arr.forEach(callback[, thisArg]) вызывает функцию callback для каждого элемента массива. Необязательный аргумент thisArg будет передан как this. Функция вызывается с параметрами:callback(item, i, arr):

• item — очередной элемент массива. • i — его номер. • arr — массив, который перебирается. 1 var arr = ["Яблоко", "Апельсин"]; 2 3 function outputItem(item, i, arr) { 4 alert(i + ": " + item + " (массив:" + arr + ")"); 5} 6 7 arr.forEach(outputItem); filter Метод arr.filter(callback[, thisArg]) создаёт новый массив, в который войдут только те элементы arr, для которых вызов callback(item, i, arr) возвратит true. Например:

1 var arr = [1, -1, 2, -2, 3]; 2 3 function isPositive(number) {


4 return number > 0; 5} 6 7 var positiveArr = arr.filter(isPositive); 8 9 alert(positiveArr); // 1,2,3 map Метод arr.map(callback[, thisArg]) создаёт новый массив, который будет состоять из результатов вызова callback(item, i, arr) для каждого элемента arr. Например:

1 var arr = [1, 2, 3, 4]; 2 3 function square(number) { 4 return number * number; 5} 6 7 var squaredArr = arr.map(square); 8 9 alert(squaredArr); // 1, 4, 9, 16 every/some Метод arr.every(callback[, thisArg]) возвращает true, если вызов callback вернёт true для каждогоэлемента arr. Метод arr.some(callback[, thisArg]) возвращает true, если вызов callback вернёт true для какогонибудь элемента arr.

1 var arr = [1, -1, 2, -2, 3]; 2 3 function isPositive(number) { 4 return number > 0; 5} 6 7 alert( arr.every(isPositive) ); // false, не все положительные 8 alert( arr.some(isPositive) ); // true, есть хоть одно положительное reduce/reduceRight Метод arr.reduce(reduceCallback[, initialValue]) применяет функцию reduceCallback по очереди к каждому элементу массива слева направо, сохраняя при этом промежуточный результат.


Аргументы функции reduceCallback(previousValue, currentItem, index, arr):

• previousValue — последний результат вызова функции, он же «промежуточный результат».

• currentItem — текущий элемент массива, элементы перебираются по очереди слеванаправо.

• index — номер текущего элемента. • arr — обрабатываемый массив. Значение previousValue при первом вызове равно initialValue. Если initialValue нет, то оно равно первому элементу массива, а перебор начинается со второго. Проще всего это понять на примере соединения строк: Пусть мы хотим соединить все элементы массива, т.е. сделать arr.join(""). Это можно сделать, по очереди прибавив каждый элемент, начиная с первого, к постоянно увеличивающейся результирующей строке.

01 var arr = ["a", "b", "c", "d"]; 02 03 function join(previousStr, currentItem, i) { 04 // прибавляем к строке очередной элемент 05 var str = previousStr + currentItem; 06 alert(str); // a, ab, abc, abcd 07 return str; 08 } 09 10 var result = arr.reduce(join, ""); // abcd 11 12 alert("result = " + result); 1. Функция join начнёт работать с начала массива. Её первые аргументы: • previousStr = "" (начальное значение) • currentItem = "a" (первый элемент) • i = 0 (его индекс) К пустой строке прибавится "a", что даст вывод "a". Это значение нужно возвратить! Оно будет передано как previousStr при следующем запуске функции. 2. Второй запуск будет с аргументами:

• previousStr = "a" (предыдущее возвращённое значение) • currentItem = "b" (второй элемент) • i = 1 (его индекс) Его результат: "ab". Как видно, в previousStr аккумулируется промежуточный результат. 3. ..И так далее до последнего запуска с аргументами:


• previousStr = "abc" (предыдущее возвращённое значение) • currentItem = "d" (последний элемент) • i = 3 (его индекс) Значение, возвращённое из последнего запуска, вернётся как результат: "abcd". Пример без начального значения: Посмотрим, что будет, если не указать initialValue в вызове arr.reduce:

1 var arr = ["a", "b", "c", "d"]; 2 3 function join(previousStr, currentItem, i) { 4 return previousStr + currentItem; 5} 6 7 var result = arr.reduce(join); // только один аргумент 8 9 alert("result = " + result); // abcd Результат — точно такой же! Это потому, что при отсутствии initialValue в качестве первого значения берётся первый элемент массива, а перебор стартует со второго. То есть, первый вызов функции join был с аргументами:

• previousStr = "a" (первый элемент) • currentItem = "b" (следующий) • i = 1 (его индекс) …А далее — всё так же, как в предыдущем примере. Пример с суммой:

1 function sum(previousSum, currentItem) { 2 return previousSum + currentItem; 3} 4 5 alert( [1,2,3,4,5].reduce(sum) ); // 1+2+3+4+5 = 15 Метод arr.reduceRight работает аналогично, но идёт по массиву справа-налево:

1 function join(previousStr, currentItem) { 2 return previousStr + currentItem; 3} 4


5 alert( ["a","b","c","d"].reduceRight(join) ); // dcba 2 На входе массив чисел, например: arr = [1,2,3,4,5]. Ваша функция getSums(arr) должна возвращать новый массив из такого же числа элементов, в котором на каждой позиции должна быть сумма элементов arr до этой позиции включительно. То есть:

для arr = [1,2,3,4,5] getSums(arr) = [1, 1+2, 1+2+3, 1+..+4, 1+..+5] = [1, 3, 6, 10, 15] Еще пример: getSums([-2,-1,0,1]) = [-2,-3,-3,-2]. • Функция не должна модифицировать входной массив.

• В решении используйте метод arr.reduce. Решение Метод arr.reduce подходит здесь идеально. Достаточно пройтись по массиву слева-направа, накапливая текущую сумму в переменной и, кроме того, добавляя её в результирующий массив. Первоначальный вариант может выглядеть так:

01 function getSums(arr) { 02 var result = []; 03 04 arr.reduce(function(sum, item) { 05 result.push(sum); 06 return sum + item; 07 }); 08 09 return result; 10 } 11 12 alert(getSums([1,2,3,4,5])); // результат: 1,3,6,10 Если вы его запустите, то обнаружите, что результат не совсем тот. В получившемся массиве всего четыре элемента, отсутствует последняя сумма.


Дело в том, что последняя сумма — это и есть результат метода reduce, который на ней заканчивает проход и далее функцию не вызывает. Поэтому она оказывается не добавленной в result. Исправим это:

01 function getSums(arr) { 02 var result = []; 03 04 var totalSum = arr.reduce(function(sum, item) { 05 result.push(sum); 06 return sum + item; 07 }); 08 result.push(totalSum); 09 10 return result; 11 } 12 13 alert(getSums([1,2,3,4,5])); // 1,3,6,10,15 14 alert(getSums([-2,-1,0,1])); // -2,-3,-3,-2 [Открыть задачу в новом окне] Запуск кода из строки: eval

1. 2. 3. 4. 5.

Пример eval eval и локальные переменные Запуск кода в глобальном контексте Взаимодействие с внешним кодом, new Function Итого

Функция eval(code) позволяет выполнить код, переданный ей в виде строки. Этот код будет выполнен в текущей области видимости. Пример eval В простейшем случае eval всего лишь выполняет код, например:

1 var a = 1; 2 3 (function() { 4 5 var a = 2; 6


7 eval(' alert(a) '); // 2 8 9 })() Но он может не только выполнить код, но и вернуть результат. Вызов eval возвращает последнее вычисленное выражение: Например:

1 alert( eval('1+1') ); // 2 eval и локальные переменные Код выполняется в текущей области видимости. Это означает, что текущие переменные могут быть изменены или дополнены:

1 eval("var a = 5"); 2 alert(a); // 5, определена новая переменная 1 var a = 0; 2 eval("a = 5"); 3 alert(a); // 5, переписана существующая переменная Новые переменные не появятся, если код работает в строгом режиме:

1 "use strict"; 2 3 eval("var a = 5"); 4 alert(a); // ошибка, переменная не определена …Но существующие переменные всё равно будут переопределены:

1 "use strict"; 2 3 var a = 0; 4 eval("a = 5"); 5 alert(a); // 5, переписана существующая переменная Это естественно и полностью соответствует логике работы eval, которая заключается в том, чтобы динамически включить новый код в существующий. Запуск кода в глобальном контексте Хотя, технически, код из eval имеет доступ к текущим локальным переменным, но


использование этой возможности считается очень плохой практикой. Если уж мы выполняем динамический (загруженный с сервера?) код, то пусть он будет сам по себе, со своими переменными, и ни в коем случае не приведет к конфликту с текущими именами. Поэтому были придуманы трюки, которые позволяют запускать eval в глобальной области видимости, без доступа к локальным переменным.

• Например, можно вызвать eval не напрямую, а через window.eval. Это работает везде, кроме IE<9, например:

1 var a = 1; 2 3 (function() { 4 5 var a = 2; 6 7 window.eval(' alert(a) '); // 1, везде кроме IE<9 8 9 })(); • В старых IE<9 можно применить нестандартную фунцию execScript. Она, как и eval, выполняет код, но всегда в глобальной области видимости и не возвращает значение. Оба способа можно объединить в единой функции, выполняющей код без доступа к локальным переменным:

01 function globalEval(code) { // объединим два способа в одну функцию 02 window.execScript ? execScript(code) : window.eval(code); 03 } 04 05 var a = 1; 06 07 (function() { 08 09 var a = 2; 10 11 globalEval(' alert(a) '); // 1, во всех браузерах 12 13 })(); Взаимодействие с внешним кодом, new Function Итак, через eval не стоит менять локальные переменные. Это плохо, так как нарушает целостность кода, делает его фрагментарным и сложно поддерживаемым. Но, кроме того, eval не стоит даже и читать локальные переменные. А то вдруг мы решим


переименовать переменную и забудем, что она использовалась в eval? К счастью, существует отличная альтернатива eval, которая позволяет корректно взаимодействовать c внешним кодом: new Function. Вызов new Function('a,b', '..тело..') создает функцию с указанными аргументами a,b и телом. Доступа к текущему замыканию у такой функции не будет, но можно передать локальные переменные и получить результат. Например:

1 var a = 2; 2 3 // вместо обращения к локальной переменной через eval 4 // будем принимать ее как аргумент функции 5 var f = new Function('x', 'alert(x)'); 6 7 f(a); // 2 Так как областью видимости функции, созданной через new Function, является глобальный объект, она даже технически не может обратиться к *локальной переменной a:

1 (function() { 2 3 var a = 2; 4 5 var f = new Function('', 'alert(a)'); 6 7 f(); // ошибка 8 9 })(); Итого

• Функция eval(str) выполняет код и возвращает последнее вычисленное выражение. В современном JavaScript она используется редко.

• Она выполняется в текущей области видимости, поэтому может получать и изменять локальные переменные. Этого следует избегать. Если выполняемый код всё же должен взаимодействовать с локальными переменными — используйте new Function. Создавайте функцию из строки и передавайте переменные ей, получайте результат явным образом.

• Есть трюки для выполнения eval в глобальной области видимости. Но лучше, все же, использовать new Function. Ещё примеры использования eval вы найдёте далее, в главе Формат JSON.


4

Напишите интерфейс, который принимает математическое выражение (prompt) и возвращает его результат. Проверять выражение на корректность не требуется. Демо в новом окне: tutorial/intro/eval-calc.html Решение Вычислить любое выражение нам поможет eval:

1 var expr = prompt("Введите выражение?", '2*3+2'); 2 3 alert(eval(expr));

При этом посетитель потенциально может делать все, что угодно.

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

1. Типы ошибок 1. Конструкция try..catch 2. Объект ошибки 3. Секция finally 2. Генерация своих ошибок 1. Пример: проверка значений 2. Генерация ошибки: throw 3. Вложенные вызовы и try..catch 3. Итого Конструкция try..catch — мощное встроенное средство по обработке ошибок и управлению потоком выполнения. Здесь мы рассмотрим её использование, и общие подходы к обработке ошибок в коде. Тип ы ошибок Ошибки делятся на два глобальных типа.


1. Синтаксические ошибки — когда нарушена структура кода, например: for (a = 5) { // неправильный аргумент for ]; // неправильная закрывающая скобка Обычно это ошибки программиста. Их также называют «ошибки времени компиляции», поскольку они происходят на этапе разбора кода. Как-либо «восстановить» выполнение скрипта здесь нельзя. Браузер не может такой код разобрать и выполнить.

2. Семантические ошибки, также называемые «ошибки времени выполнения» — возникают, когда браузер смог прочитать скрипт и уже выполняет код, но вдруг натыкается на проблему. Например, «не определена переменная»:

alert(nonexistant); // ошибка, переменная не определена! Эти ошибки можно перехватить и обработать самостоятельно, не дать скрипту «упасть». Что особенно важно, семантические ошибки, в отличие от программных, могут быть частью нормальной работы скрипта! Конечно, это не касается забытых определений переменных

. Но ошибки могут возникать и в

других ситуациях. Например, посмотрим на следующий интерфейс, который переводит введенное пользователем число в заданную систему счисления, используя вызов toString(основание системы счисления):

1 var number = +prompt("Введите число", '9'); 2 var base = +prompt("Введите основание системы счисления", '2'); 3 4 var convertedNumber = number.toString(base); 5 alert( convertedNumber ); // для 9 в двоичной системе счисления: 1001 Пользователь при его использовании может попытаться ввести что-то нестандартное. Например, не число. А вызов toString с base, не являющимся числом, приведет к ошибке JavaScript. Вы можете попробовать сами, запустив этот пример. Мы можем перед вызовом toString дотошно проверять аргументы, с риском забыть какую-то ситуацию. В данном конкретном случае можно ввести и числа, но все равно получить ошибку JavaScript. А бывают случаи гораздо менее очевидные, в которых возможных ошибок может быть куча, и все их проверять — дело чрезвычайно муторное…

…К счастью, JavaScript предоставляет нам альтернативу, которая называется try..catch!


Он заключается в том, что мы выполняем код «как есть», а если в нем возникнет ошибка, то он не рухнет, а передаст ее в специально выделенный блок. Конструкция try..catch Конструкция try..catch состоит из двух основных блоков: try, и затем catch. Например:

01 try { 02 var number = 9; 03 alert( number.toString(2) ); 04 05 // ... 06 07 alert('Выполнено без ошибок!'); 08 09 } catch(e) { 10 11 alert('Ошибка!'); 12 13 } Работает это так:

1. Выполняется код внутри блока try. 2. Если в нём возникнет ошибка, то выполнение try прерывается, и управление прыгает в начало блока catch. Например:

01 try { 02 03 alert('Начало блока try'); // (1) <-04 05 lalala; // ошибка, переменная не определена! 06 07 alert('Конец блока try'); 08 09 } catch(err) { 10 11 alert('Управление получил блок catch!'); // (2) <-12 13 } Будут два alert'а: (1) и (2). Внутри catch получает объект с информацией, какая именно ошибка произошла. В


примере выше он назван err, но можно и любое другое имя. Мы можем его использовать, для того, чтобы правильно проинформировать посетителя и корректно возобновить работу. Более подробно мы поговорим о нем немного позже, после того, как разберем общую логику работыtry..catch.

3. Если ошибки нет — блок catch игнорируется. Например:

01 try { 02 03 alert('Начало блока try'); // (1) <-04 05 // .. код без ошибок 06 07 alert('Конец блока try'); // (2) <-08 09 } catch(e) { 10 11 alert('Блок catch не получит управление'); 12 13 } Будут два alert'а: (1) и (2). Блок catch не будет использован. Логика работы try..catch позволяет отловить любые семантические ошибки. В том числе, обработать ошибку при неверном основании системы счисления:

01 var number = +prompt("Введите число", '9'); 02 var base = +prompt("Введите основание системы счисления", '2'); 03 04 try { 05 var convertedNumber = number.toString(base); 06 07 alert( convertedNumber ); 08 } catch(err) { 09 alert("Произошла ошибка " + err); 10 } Объект ошибки У блока catch есть аргумент, в примере выше он обозначен err. Это — объект ошибки или, как ещё говорят, объект исключения (exception object). Он содержит информацию о том, что произошло, и может быть разным, в зависимости от


ошибки. Как правило, err — объект встроенного типа Error и производных от него. Есть несколько свойств, которые присутствуют в объекте ошибки: name Тип ошибки. Например, при обращении к несуществующей переменной равен"ReferenceError". message Текстовое сообщение о деталях ошибки. stack Везде, кроме IE<9, есть также свойство stack, указывающее, где в точности произошла ошибка. В зависимости от браузера, у него могут быть и дополнительные свойства, см. например Error в MDN и Error в MSDN. В примере ниже идёт попытка вызова числовой переменной как функции. Это ошибка типаname = "TypeError", с сообщением message, которое зависит от браузера:

1 try { 2 var a = 5; 3 4 var res = a(1); // ошибка! 5 6 } catch(err) { alert("name: " + err.name + "\nmessage: " + err.message + "\nstack: 7 " + err.stack); 8} ..А попытка преобразовать число к неверной системе счисления даст ошибку типаname = "RangeError":

01 try { 02 03 var number = 9; 04 var base = 100; 05 06 var convertedNumber = number.toString(base); 07 08 } catch(err) { 09 10 alert("name: " + err.name + "\nmessage: " + err.message + "\nstack:


" + err.stack); 11 12 } Секция finally Конструкция try..catch может содержать ещё один блок: finally. Выглядит этот расширенный синтаксис так:

1 try { 2 .. пробуем выполнить код .. 3 } catch(e) { 4 .. перехватываем исключение .. 5 } finally { 6 .. выполняем всегда .. 7} Секция finally не обязательна, но если она есть, то она выполняется всегда:

• после блока try, если ошибок не было, • после catch, если они были. Её используют, чтобы завершить начатые операции и очистить ресурсы, которые должны быть очищены в любом случае — как при ошибке, так и при нормальном потоке выполнения. Например, функция в процессе работы создает новый объект, который нужно в конце уничтожить. Реализация будет выглядеть так:

01 function doSomethingCool() { var tmpObject = document.createElement("div"); // создать что-то, 02 что нужно будет очистить 03 04 try { 05 06 .. поработать с tmpObject .. 07 08 } catch(e) { 09 10 .. обработать ошибку .. 11 12 } finally { 13 14 .. удалить объект tmpObject .. 15 16 } 17 18 }


Блок finally позволяет избежать дублирования кода в try/catch.

finally и return Блок finally срабатывает при любом выходе из try..catch, в том числе и return. В примере ниже, из try происходит return, но finally получает управление до того, как контроль возвращается во внешний код.

01 function func() { 02 03 try { 04 // сразу вернуть значение 05 return 1; 06 07 } catch(e) { 08 alert('Сюда управление не попадёт, ошибок нет'); 09 } finally { 10 alert('Вызов завершён'); 11 } 12 } 13 14 alert( func() ); Это гарантирует освобождение ресурсов в любом случае. Генерация своих ошибок Конструкцию try..catch можно использовать не только со встроенными ошибками JavaScript. Можно создавать свои ошибки. Разберем это на примере «из жизни», который позволит понять, зачем это нужно. Пример: проверка значений Пример заключается в проверке значений формы. Обычно это реализуется функцией с именемcheck... или validate..., которая получает то, что ввёл посетитель и, в простейшем случае, возвращает true/false:

1 function checkValidAge(age) { 2 if (age >= 18) { 3 return true; // OK


4 } else { 5 return false; // Ошибка 6 } 7} В реальной жизни простого true/false может оказаться недостаточно! Если проверка достаточно сложная, то хорошо бы вернуть ошибку, с текстовым описанием, что именно не так. Решение без исключений (плохое!) выглядит так:

01 // функция возвращает: 02 // false, если всё хорошо 03 // или строку с ошибкой 04 function checkValidAge(age) { 05 if (age >= 18) { 06 return false; 07 } else { 08 return "Вы слишком молоды. Приходите через " + (18-age) + " лет"; 09 } 10 } ..То есть, при таком подходе возврат false используется в смысле «всё в порядке», а строка означает ошибку. Использовать эту функцию нужно так:

1 var error = checkValidAge(age); 2 if (error) { 3 // показать ошибку 4} Почему это решение плохое? В данном случае оно работает, но что, если строка является нормальным результатом функции? Как тогда обозначить ошибку? Вернуть специальный объект?.. Зачем придумывать? Ответ уже есть — и это исключения! Генерация своей ошибки предоставляет альтернативный подход к проблеме, который мы сейчас рассмотрим. Генерация ошибки: throw Синтаксис: throw <объект ошибки>. Инициирует ошибку, которую можно поймать в catch. Объектом ошибки может быть что угодно, например число:

1 try { 2 throw 123;


3 } catch(e) { 4 alert(e); // 123 5} Можно использовать для этих целей и встроенный объект Error:

1 try { 2 //... 3 throw new Error("Упс, ошибка"); 4 //... 5 } catch(e) { 6 alert(e.message); // Упс, ошибка 7 alert(e.stack); // Где ошибка 8} ..А можно и свой объект, в том числе с дополнительной информацией. Перепишем функции, которые осуществляют проверки, с использованием throw и нашего объекта BadValueError:

01 function BadValueError(message) { 02 this.name = "BadValue"; 03 this.message = message; 04 } 05 06 function checkValidAge(age) { 07 if (age < 18) { 08 throw new BadValueError("Возраст не подходит"); 09 } 10 } 1 function checkRequired(value) { 2 if (value == '') { 3 throw new BadValueError("Отсутствует значение"); 4 } 5} Код для проверки:

01 var value = ageInput.value; 02 03 try { 04 checkRequired(value); // проверить, что значение есть 05 checkInteger(value); // проверить, что значение целое checkValidAge(value);// проверить, что диапазон соответствует 06 возрасту 07 // ... еще проверки 08


09 /* ввод успешен */ 10 } catch(e) { 11 /* обработать ошибку */ 12 } Гораздо короче, не правда ли? И при этом надёжнее, т.к. конструкция try..catch поймает любуюошибку. Вложенные вызовы и try..catch Брошенное исключение выпадает из всех циклов, функций и т.п. Это — особое, уникальное свойство исключений. Оно означает, что в случае вложенных вызовов — не важно, где было исключение, оно будет поймано ближайшим внешним try..catch. Например:

01 function checkAll(value) { 02 checkRequired(value); 03 checkInteger(value); 04 checkValidAge(value); 05 } 06 07 08 try { 09 checkAll(value); 10 11 alert('Да, вы нам подходите!'); 12 13 } catch(e) { 14 if (e.name == "BadValue") { 15 // ок, я знаю, что делать с этой ошибкой 16 alert('Ошибка '+e.message); 17 } else { 18 // ошибка неизвестна, пробросить ее дальше 19 throw e; 20 } 21 } В примере выше дополнительно использована техника «проверки и проброса» исключения. Взглянем внимательнее на фрагмент кода:

1 if (e.name == "BadValue") { 2 // ок, я знаю что делать с этой ошибкой 3 alert('Ошибка '+e.message);


4 } else { 5 // ошибка неизвестна, пробросить ее дальше 6 throw e; 7} Его идея — в том, что исключения могут быть самыми разными. Возможно, это ошибка в значении, а может быть — нет прав на действие, а может быть — доступ к необъявленной переменной… Возможно что угодно. Данная конкретная функция checkAll умеет обрабатывать только ошибку проверки BadValue, что и делает. В том случае, если исключение какое-то другое, то она «пробрасывает» (throw e) его дальше, а там оно либо попадёт во внешний блок try..catch, который знает, что с ним делать, либо выпадает из скрипта как ошибка — и тогда разработчик увидит его в консоли.

try..finally Синтаксис try..finally (без catch) применяется тогда, когда мы хотим произвести очистку в finally, но совсем не умеем обрабатывать ошибки.

1 try { 2 .. 3 } finally { 4 // очистить ресурсы, а ошибки пусть летят во внешний try..catch // (который мы предусмотрели, не правда ли? иначе скрипт 5 упадёт) 6

}

Итого

Исключения в JavaScript нужны редко. Но там, где они нужны — они нужны.

• Основная область применения — попытка сделать что-то, что может вернуть ошибку, но проверить заранее нельзя. Например, получить информацию из IFRAME'а.

• Другая область применения — завернуть блок кода, в котором может быть ошибка, в конструкцию try..catch, и вместо многочисленных проверок обработать ошибку один раз, внизу. Мы видели это в примере с checkRequired/checkInteger/checkAge. Ошибка, генерируемая throw, «выпадает» из функции, и передаёт управление на ближайший catch:


01 function a() { b(); } 02 03 function b() { c(); } 04 05 function c() { throw new Error("Ошибка!"); } 06 07 try { 08 a(); // a() -> вызывает b() -> вызывает c() -> Ошибка! 09 } catch(e) { 10 alert(e); // <-- ошибка выпадет в ближайший try..catch 11 } Это делает try..catch + throw мощным средством контроля выполнения. При использовании своих ошибок рекомендуется генерировать объекты встроенного типа Error или, когда мы познакомимся с наследованием в JavaScript — наследующие от него. Ваши объекты также могут содержать дополнительную информацию об обстоятельствах ошибки, полезную для её обработки:

1 var err = new Error("Ошибка"); 2 err.extra = new Date(); 3 4 alert(err.message + " в " + err.extra); // "Ошибка в (дата)" 5

Сравните два кода.

1. Первый использует finally для выполнения кода по выходу из try..catch:

1 try { 2 работать 3 } catch(e) { 4 обработать ошибку 5 } finally { 6 очистить ресурсы 7} 2. Второй фрагмент просто ставит очистку ресурсов за try..catch: 1 try { 2 работать


3 } catch(e) { 4 обработать ошибку 5} 6 7 очистить ресурсы Будет ли их поведение одинаковым? Ведь в обоих случаях код очистки выполнится после try..catch. Нужен ли, вообще, finally? Решение Поведение будет разным, если управление каким-то образом выпрыгнет изtry..catch, например:

01 function f() { 02 try { 03 ... 04 return result; 05 } catch(e) { 06 ... 07 } finally { 08 очистить ресурсы 09 } 10 } Или ошибка будет проброшена наружу:

01 function f() { 02 try { 03 ... 04 05 } catch(e) { 06 ... 07 if (не умею обрабатывать эту ошибку) { 08 throw e; 09 } 10 11 } finally { 12 очистить ресурсы 13 } 14 }


В этих случаях finally гарантирует выполнение кода и корректную очистку ресурсов функции. [Открыть задачу в новом окне] 5

Напишите интерфейс, который принимает математическое выражение (в prompt) и возвращает его результат. При ошибке нужно выводить сообщение и просить переввести выражение. Ошибкой считается некорректное выражение, такое как 2+ и выражение, возвращающее NaN, например 0/0. Демо в новом окне: tutorial/intro/eval-calc-try.html. Решение Схема решения Вычислить любое выражение нам поможет eval:

1 alert( eval("2+2") ); // 4 Считываем выражение в цикле while(true). Если при вычислении возникает ошибка — ловим её в try..catch. Ошибкой считается, в том числе, получение NaN из eval, хотя при этом исключение не возникает. Можно бросить своё исключение в этом случае. Код решения

01 while(true) { 02 expr = prompt("Введите выражение?", '2-'); 03 04 try { 05 res = eval(expr); // при ошибке будет catch 06 07 if (isNaN(res)) { // наша ошибка 08 throw new Error("Результат неопределён"); 09 } 10 11 break; // все ок, выход из цикла 12


13 } catch(e) { 14 15 alert("Ошибка: "+e.message+", повторите ввод"); 16 17 } 18 } 19 20 alert(res); Полное решение: tutorial/intro/eval-calc-try.html. [Открыть задачу в новом окне] Формат JSON

1. Формат JSON 2. JSON.stringify и JSON.parse 3. Детали JSON.stringify 1. Сериализация объектов, toJSON 2. Исключение свойств 3. Красивое форматирование 4. Умный разбор: JSON.parse(str, reviver) 5. IE<8: разбор JSON с eval 1. Безопасность, проверка JSON В этой главе мы рассмотрим работу с форматом JSON. Это один из наиболее удобных форматов для JavaScript. В современных браузерах (для IE7- реализуются библиотекой) есть замечательные методы, знание тонкостей которых делает операции с JSON простыми и комфортными. Формат JSON Данные в формате JSON (RFC 4627) представляют собой значения или JavaScript-объекты { ... }или массивы [ ... ], содержащие значения одного из типов: • строки в двойных кавычках, • число,

• логическое значение true/false, • null. Объекты JSON отличаются от обычных JavaScript-объектов более строгими требованиями к строкам — они должны быть именно в двойных кавычках. В частности, первые два свойства объекта ниже — некорректны:

1{ 2 3 4 5 6}

name: "Вася", "surname": 'Петров', "age": 35 "isAdmin": false

// // // //

ошибка: ключ name без кавычек! ошибка: одинарные кавычки у значения! .. а тут всё в порядке. и тут тоже всё ок


JSON.stringify и JSON.parse

• Метод JSON.stringify(value, replacer, space) преобразует («сериализует») значение в JSON-строку. Он поддерживается во всех браузерах, включая IE8+. Для более старых IE рекомендуется библиотека JSON-js, которая добавляет аналогичную функциональность.

• Метод JSON.parse(str, reviver) читает JavaScript-значение из строки. Пример использования:

01 var event = { 02 title: "Конференция", 03 date: "сегодня" 04 }; 05 06 var str = JSON.stringify(event); 07 alert( str ); // {"title":"Конференция","date":"сегодня"} 08 09 // Обратное преобразование. 10 event = JSON.parse(str); Детали JSON.stringify Метод JSON.stringify обладает рядом расширенных возможностей, которые бывают очень полезны в реальных задачах. Сериализация объектов, toJSON При сериализации объекта вызывается его метод toJSON. Если такого метода нет — перечисляются его свойства, кроме функций. Посмотрим это в примере посложнее:

01 function Room() { 02 this.number = 23; 03 04 this.occupy = function() {}; 05 } 06 07 event = { 08 title: "Конференция", 09 date: new Date(2012, 11, 1), 10 room: new Room() 11 }; 12


13 alert( JSON.stringify(event) ); 14 /* 15 { 16 "title":"Конференция", 17 "date":"2012-11-30T20:00:00.000Z", 18 "room": {"number":23} 19 } 20 */

// (1) // (2)

Обратим внимание на два момента:

1. Дата превратилась в строку. Это не случайно: у всех дат есть встроенный метод toJSON. Его результат в данном случае — строка в зоне UTC. Так как объект new Date создавался на компьютере с локальной временной зоной GMT+4, то сериализованный UTC-вариант — на 4 часа меньше.

2. У объекта new Room нет метода toJSON. Поэтому он сериализуется перечислением свойств. Мы, конечно, могли бы добавить такой метод, тогда в итог попал бы её результат:

1 function Room() { 2 this.number = 23; 3 4 this.toJSON = function() { 5 return this.number; 6 }; 7} 8 9 alert( JSON.stringify( new Room() ) ); // 23 Исключение свойств Попытаемся преобразовать в JSON объект, содержащий ссылку на DOM. Например:

1 var user = { 2 name: "Вася", 3 age: 25, 4 elem: document.body 5} 6 7 alert( JSON.stringify(user) ); // ошибка! 8 // TypeError: Converting circular structure to JSON (текст из Chrome)


Произошла ошибка! В чём же дело, неужели DOM-объекты запрещены? Как видно из текста ошибки — дело совсем в другом. DOM-объект — сложная структура с круговыми ссылками, поэтому его преобразовать невозможно. Да и нужно ли? Во втором параметре JSON.stringify(value, replacer) можно указать массив свойств, которые подлежат сериализации. Например:

1 var user = { 2 name: "Вася", 3 age: 25, 4 elem: document.body 5 }; 6 7 alert( JSON.stringify(user, ["name", "age"]) ); 8 // {"name":"Вася","age":25} Для более сложных ситуаций вторым параметром можно передать функцию function(key, value), которая возвращает сериализованное value либо undefined, если его не нужно включать в результат:

01 var user = { 02 name: "Вася", 03 age: 25, 04 elem: document.body 05 }; 06 07 var str = JSON.stringify(user, function(key, value) { 08 if (key == 'elem') return undefined; 09 return value; 10 } ); 11 12 alert(str); // {"name":"Вася","age":25} В примере выше функция пропускает свойства с ключом elem. Для остальных она просто возвращает значение, передавая его стандартному алгоритму. А могла бы и как-то обработать. Функция replacer работает рекурсивно. То есть, если объект содержит вложенные объекты, массивы и т.п., то все они пройдут черезreplacer. Красивое форматирование В методе JSON.stringify(value, replacer, space) есть ещё третий параметр space.


Если он является числом — то уровни вложенности в JSON оформляются указанным количеством пробелов, если строкой — вставляется эта строка. Например:

01 var user = { 02 name: "Вася", 03 age: 25, 04 roles: {isAdmin: false, isEditor: true} 05 }; 06 07 var str = JSON.stringify(user, "", 4); 08 09 alert(str); 10 /* Результат -- красиво сериализованный объект: 11 { 12 "name": "Вася", 13 "age": 25, 14 "roles": { 15 "isAdmin": false, 16 "isEditor": true 17 } 18 } 19 */ Умный разбор: JSON.parse(str, reviver) Предположим, мы получили с сервера корректно сериализованный объект event. И теперь нужно восстановить его. Вызовем для этого JSON.parse:

1 var str = '{"title":"Конференция","date":"2012-11-30T00:00:00.000Z"}'; 2 3 var event = JSON.parse(str); 4 5 alert( event.date.getDate() ); // ошибка! 6 // TypeError: Object 2012-11-30T00:00:00.000Z has no method 'getDate' …Увы, ошибка! Дело в том, что значением event.date является строка, а отнюдь не объект Date. Откуда методуJSON.parse знать, что нужно превратить строку именно в дату? Для интеллектуального восстановления из строки у JSON.parse(str, reviver) есть второй


параметр, который является функцией function (key, value). Она принимает по очереди все создаваемые пары ключ-значение и может возвратить преобразованное значение value, либо undefined, если его нужно игнорировать. В данном случае мы можем создать правило, что ключ date всегда означает дату:

1 // дата в строке - в формате UTC 2 var str = '{"title":"Конференция","date":"2012-11-30T20:00:00.000Z"}'; 3 4 var event = JSON.parse(str, function(key, value) { 5 if (key == 'date') return new Date(value); 6 return value; 7 }); 8 alert( event.date.getDate() ); // 1 или 30, в зависимости от локальной 9 зоны IE<8: разбор JSON с eval Функция eval(code) выполняет код и, если это выражение, то возвращает его значение. Поэтому её можно использовать для чтения JSON, например:

1 var str= '{ \ 2 "name": "Вася", \ 3 "age": 25 \ 4 }'; 5 6 var user = eval('(' + str + ')'); 7 8 alert(user.name); // Вася Зачем здесь нужны скобки, почему не просто eval(str)? Как вы считаете? Подумали?… Нет, правда, зачем?

…Всё дело в том, что в JavaScript с фигурной скобки { начинаются и объекты и блоки кода. Если передать eval объект напрямую, то он подумает, что первая { начинает блок кода:

1{ 2 3 4}

"name": "Вася", "age": 25

Выполнение такого кода, конечно, приведёт к ошибке.


А если eval получает выражение в скобках ( ... ), то интерпретатор точно знает, что блока кода внутри быть не может, значит это объект. Безопасность, проверка JSON Если JSON разбирается при помощи eval, то мы должны быть уверены, что это именно JSON, а не злонамеренный скрипт. При получении JSON с нашего сервера такая уверенность есть, а если он из другого источника? Метод JSON.parse гарантирует, что некорректный JSON просто выдаст ошибку, а в eval можно добавить дополнительную проверку регулярным выражением, описанным в RFC 4627, секции 6. Код преобразования с проверкой:

var obj = !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test( str.replace(/"(\\.|[^"\\])*"/g, ''))) && eval('(' + str + ')'); Пример опасного сценария с eval:

1 // злой объект 2 var str = '{}(function() {alert(1)}())'; 3 4 eval('(' + str + ')'); // 1, выполнилась функция Опасный сценарий с проверкой:

1 // злая функция 2 var str = '{}(function() {alert(1)}())'; 3 4 var user = !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test( 5 str.replace(/"(\\.|[^"\\])*"/g, ''))) && 6 eval('(' + str + ')'); 7 8 alert(user); // false, объект не преобразован 3 Превратите объект team из примера ниже в JSON:

01 var leader = { 02 name: "Василий Иванович", 03 }; 04 05 var soldier = {


06 name: "Петька" 07 }; 08 09 leader.soldier = soldier; 10 soldier.leader = leader; 11 12 var team = [ leader, soldier ]; Может ли это сделать прямой вызов JSON.stringify(team)? Если нет, то почему? Какой подход вы бы предложили для чтения и восстановления таких объектов? Решение Обычный вызов JSON.stringify(team) выдаст ошибку, так как объекты leader иsoldier внутри структуры ссылаются друг на друга. Формат JSON не предусматривает средств для хранения ссылок. Чтобы превращать такие структуры в JSON, обычно используются два подхода:

1. Добавить в team свой код toJSON: team.toJSON = function() { /* свой код, который может создавать копию объекта без круговых ссылок и передавать управление JSON.stringify */ } При этом, конечно, понадобится и своя функция чтения из JSON, которая будет восстанавливать объект, а затем дополнять его круговыми ссылками.

2. Можно учесть возможную проблему в самой структуре, используя вместо ссылок id. Как правило, это несложно, ведь на сервере у данных тоже есть идентификаторы. Изменённая структура может выглядеть так:

01 var leader = { 02 id: 12, 03 name: "Василий Иванович", 04 }; 05 06 var soldier = { 07 id: 51,


08 name: "Петька" 09 }; 10 11 // поменяли прямую ссылку на ID 12 leader.soldierId = 51; 13 soldier.leaderId = 12; 14 15 var team = { 16 12: leader, 17 51: soldier 18 }; ..Но действительно ли это решение будет оптимальным? Использовать структуру стало сложнее, и вряд ли это изменение стоит делать лишь из-за JSON. Вот если есть другие преимущества, тогда можно подумать. 3. Универсальный вариант подхода, описанного выше — это особая реализация JSON, расширяющая формат для поддержки ссылок. Она, к примеру, сделана во фреймворке Dojo. При вызове dojox.json.ref.toJson(team) будет создано следующее строковое представление:

[{"name":"Василий Иванович","soldier": {"name":"Петька","leader":{"$ref":"#0"}}}, {"$ref":"#0.soldier"}] Метод разбора такой строки — также свой: dojox.json.ref.fromJson. [Открыть задачу в новом окне] Тест: повторение тонких мест

Если вы внимательно читали учебник и выполняли задания, то решение большинства этих задач не составит для вас проблему. Ряд вопросов взят из тестов http://dmitry.baranovskiy.com/post/91403200,http://perfectionkills.com/javascript-quiz/ и http://d mitrysoshnikov.com/ecmascript/the-quiz/. Спасибо их составителям, рекомендую пройти и их тоже. • Все задачи выполняются в достаточно современных браузерах, IE8+. • Код задачи является единственным кодом, который запускается в окне. В частности, никаких дополнительных переменных, кроме указанных, нет. Итак, поехали…


Что выведет этот код?

1 f.call(f); 2 3 function f() { 4 alert( this ); 5} 1. [object Object] 2. код функции f 3. ошибка — слишком глубокая рекурсия

4. ошибка — переменная f не определена 5. другое Решение Ответ: 2 (код функции).

1 f.call(f); 2 3 function f() { 4 alert( this ); 5} Функция f определяется при входе в область видимости, так что на первой строке она уже есть. Далее, вызов f.call(f) вызывает функцию, передавая ей f в качестве this, так что выводится строковое представление f. [Открыть задачу в новом окне] Что выведет этот код?

1 f.call(null); 2 3 function f() { 4 alert(this); 5} 1. null 2. undefined 3. другое Решение


Ответ: 3 Если при вызове функции через call/apply первым аргументом являетсяnull/undefined, то при работе в режиме старого стандарта JavaScript браузер вызывает её в контексте window.

1 function f() { 2 alert(this); // выведет текстовое представление window 3} 4 f.call(null); С другой стороны, если стоит режим соответствия стандартам, то будет выведен именно null:

1 "use strict"; 2 3 function g() { 4 alert(this); // выведет null, если браузер поддерживает strict mode 5} 6 g.call(null); То есть, результат может быть разным, в зависимости от того, включён ли строгий режим. В этой задаче предполагается, что режим обычный, поэтому ответ 3. [Открыть задачу в новом окне] Что выведет этот код?

var f = function g(){ return 23; }; alert( typeof g() ); 1. number 2. undefined 3. function 4. ошибка 5. зависит от браузера Решение Ответ: 5 (зависит от браузера)


В современных браузерах будет ошибка, так как это Named Function Expression. Его имя g видно только внутри функции, а снаружи переменная g не определена.

1 var f = function g(){ return 23; }; 2 alert( typeof g() ); Приоритет вызова функции выше, чем typeof, так что браузер попытается вызвать функцию g(), а такой переменной нет. Старый IE<9 выдаст 'number', т.к. в нём это ограничение видимости не поддерживается. [Открыть задачу в новом окне] Что выведет этот код?

1 var y = 1; 2 var x = y = typeof x; 3 4 alert(x + 1); 1. 2. 3. 4.

2 number1 NaN undefined1

5. Будет ошибка Решение Ответ: 4

1 var y = 1; 2 var x = y = typeof x; 3 4 alert(x + 1); 1. До начала выполнения кода обе переменные равны undefined. 2. Затем выполняется присваивание y = 1. 3. Присваивание x = y = typeof x выполняется справа налево (как и все тройные присваивания). Сначала y = typeof x, и так как x равен undefined, то в y записывается typeof x == "undefined" — строка.


Затем идёт присвоениеx = y. В итоге получаем, что x == "undefined".

4. Последняя строка прибавляет 1. Так как слева строка, то оператор "+"преобразует и 1 к строке.

5. Ответ: "undefined1". [Открыть задачу в новом окне] Что выведет этот код в современном браузере?

01 "use strict"; 02 03 var f, user; 04 05 user = { 06 sayHi: function() { alert(this.fullName); }, 07 fullName: "Василий Петрович" 08 }; 09 10 (f = user.sayHi)(); 1. undefined 2. Василий Петрович 3. [object Object] 4. Будет ошибка Решение Ответ: 4 (ошибка).

01 "use strict"; 02 03 var f, user; 04 05 user = { 06 sayHi: function(){ alert(this.fullName); }, 07 fullName: "Василий Петрович" 08 }; 09 10 (f = user.sayHi)(); Строка (f = user.sayHi)() идентична последовательности операций:

f = user.sayHi; // присвоить


f();

// вызвать При этом в функцию передаётся null в качестве this. А в null свойства fullNameнет. [Открыть задачу в новом окне] Что выведет этот код?

alert( [] + 1 + 2 ); 1. 2. 3. 4.

1 NaN undefined 12

5. другое Решение Ответ: 4 (выведет 12).

1 alert( [] + 1 + 2 ); Первым делом объект [] превращается в примитив. У него нет valueOf (с примитивным результатом), так что вызывается toString и возвращает список элементов через запятую, т.е. пустую строку. Получается сложение "" + 1 + 2. Далее, так как один из аргументов — строка, то оператор "+" преобразует второй тоже к строке, и в итоге получится строка "12". [Открыть задачу в новом окне] Что выведет этот код?

alert( null == undefined ); 1. 2. 3. 4.

true false undefined NaN

Какое преобразование типов используется? Решение


Ответ: 1.

1 alert( null == undefined ); Эти два значения равны друг другу и больше ничему не равны. В частности:

1 alert( null == 0 || undefined == 0); // false 2 alert( null == '' || undefined == ''); // false Преобразования типов здесь не происходит. Оператор == работает сnull/undefined без преобразования, все варианты прописаны в спецификации. [Открыть задачу в новом окне] Что выведет этот код?

1 var x = 1; 2 if (function f(){}) { 3 x += typeof f; 4} 5 alert(x); 1. 2. 3. 4.

1 1function 1undefined NaN

5. Ошибка 6. Зависит от браузера Решение Ответ: 6 (зависит от браузера).

1 var x = 1; 2 if (function f(){}) { 3 x += typeof f; // (*) 4} 5 alert(x);


Во всех, кроме IE<9, в if находится Named Function Expression, так что имя fдоступно только из него, а снаружи не видно. Таким образом, переменная f в строке(*) не определена и к x прибавляется строка "undefined" (будет вариант 3). В старых IE функция f будет видна везде, так что в этих браузерах получится1function (вариант 2). [Открыть задачу в новом окне] Что выведет этот код?

1 function f() { 2 var a = 5; 3 return new Function('b', 'return a + b'); 4} 5 6 alert( f()(1) ); 1. 2. 3. 4.

1 6 undefined NaN

5. Будет ошибка Решение Ответ: ошибка.

1 function f() { 2 var a = 5; 3 return new Function('b', 'return a + b'); 4} 5 6 alert( f()(1) ); Функция, созданная через new Function получает в [[Scope]] ссылку на window, а не на текущий контекст. Поэтому новая функция не увидит a и выдаст ошибку. [Открыть задачу в новом окне] Что выведет этот код?

1 function F(){ } 2


3 alert( F instanceof Function ); 4 alert( new F() instanceof Object ); 1. 2. 3. 4.

false, false false, true true, false true, true

Решение Ответ: 4 (true, true).

1 function F(){ } 2 3 alert( F instanceof Function ); 4 alert( new F() instanceof Object ); 1. Функция является объектом встроенного класса Function, так что true. 2. Вспомним, как работает оператор new. Он генерирует пустой объект {} (new Object) и передаёт его функции-конструктору. В конце работы функции, если нет явного вызова return obj, где obj — какой-то другой объект, то возвращается this. В данном случае функция ничего не делает, так что возвращается this, равный пустому объекту {} (new Object). Результатом проверкиnew Object instanceof Object является true. [Открыть задачу в новом окне] Что выведет этот код?

1 function F(){ return F; } 2 3 alert( new F() instanceof F ); 4 alert( new F() instanceof Function ); 1. 2. 3. 4.

false, false false, true true, false true, true

Решение Ответ: 2 (false, true).


1 function F(){ return F; } 2 3 alert( new F() instanceof F ); 4 alert( new F() instanceof Function ); Если функция, запущенная через new, возвращает объект (не примитив!), то именно этот объект служит результатом, вместо this. Так что результатом new F является сама функция F. Поэтому получается, что первый alert проверяет: F instanceof F. Это неверно, т.к. a instanceof B проверяет, был ли объект a создан конструктором B (с учетом прототипов, но здесь это неважно). Функция F не была создана собой же, а является объектом встроенного класса Function, поэтому false. Следующая строка идентична такой проверке: F instanceof Function. Это верно. [Открыть задачу в новом окне] Что выведет этот код?

alert( typeof null ); 1. 2. 3. 4.

null undefined object string

Решение Ответ: 3.

1 alert( typeof null ); Это особенность спецификации JavaScript. Значением оператора typeof nullявляется строка "object". [Открыть задачу в новом окне] Что выведет этот код?

alert( 20e-1['toString'](2) ); 1. 2 2. 10


3. 20 4. NaN 5. Ошибка Решение Ответ: 2.

1 alert( 20e-1['toString'](2) ); 1. Запись числа 20e-1 означает 20, сдвинутое на 1 знак после запятой, т.е. 2:

1 alert( 20e-1 ); // 2 2. У каждого числа есть метод toString(radix), который преобразует число в строку, используя radix как основание системы счисления. В этом кодеradix = 2. Так что возвращается 2 в двоичной системе: "10". [Открыть задачу в новом окне] Что выведет этот код?

var a = (1,5 - 1) * 2; alert(a); 1. 2. 3. 4. 5. 6.

0.999999999 1 0.5 8 -0.5 4

Решение Ответ: 4.

1 var a = (1,5 - 1) * 2; 2 alert(a); Оператор «запятая», вычисляемый в скобках, возвращает последнее вычисленное значение. То есть, 1 игнорируется и возвращается 4 (5-1). Затем идёт умножение на два, в результате — 8.


[Открыть задачу в новом окне] Что выведет этот код?

var a = [1,2] (function() { alert(a) })() 1. [object Array] 2. [object Object] 3. 1,2 4. Будет ошибка Решение Ответ: 4.

1 var a = [1,2] 2 3 (function() { alert(a) })() Всё дело в пропущенной точке с запятой после [1,2]. Браузер интерпретирует это так:

var a = [1,2](function() { alert(a) })() ..То есть, пытается вызвать массив как функцию, что, естественно, не выходит. Отсюда и ошибка. [Открыть задачу в новом окне] Что выведет этот код?

alert("ёжик" > "яблоко"); 1. true 2. false 3. NaN Решение Ответ: 1 (true).

1 alert("ёжик" > "яблоко");


В JavaScript строки сравниваются посимвольно. Символы соотносятся как их коды. В кодировке Unicode код буквы "ё" больше кода буквы "я". Поэтому "ёжик" больше. [Открыть задачу в новом окне] Какой результат будет у выражения ниже?

null + {0:1}[0] + [,[1],][1][0] 1. 2. 3. 4. 5.

0 1 2 undefined NaN

Решение Правильный ответ: пункт 3 (т.е. =2):

1 alert(null + {0:1}[0] + [,[1],][1][0]); Подробнее вычисления:

1. null превращается в 0 2. {0:1} — это объект, у которого ключ 0 имеет значение 1 {0:1}[0] == 1 3. Второе слагаемое [,[1],] — это массив с 3-мя элементами. Элемент с индексом 1 — это массив [1]. Берём от него первый элемент:

[,[1],][1][0] == 1 [js] [Открыть задачу в новом окне] Что выведет выражение ниже?

var a = new Array(1,2), b = new Array(3); alert(a[0] + b[0]); 1. 1


2. 4 3. undefined 4. NaN Решение Правильный ответ: пункт 4 (т.е. NaN):

1 var a = new Array(1,2), b = new Array(3); 2 alert(a[0] + b[0]); Дело в том, что new Array(1,2) создаёт массив из элементов [1,2], а вотnew Array(3) — это особая форма вызова new Array с одним численным аргументом. При этом создаётся массив без элементов, но с длиной. Любой элемент этого массива равен undefined.

1 var a = new Array(1,2), b = new Array(3); 2 alert(a[0]); // 1 3 alert(b[0]); // undefined При арифметических операциях undefined становится NaN, поэтому и общий результат — NaN. [Открыть задачу в новом окне] Что выведет выражение ниже?

1 var applePrice = 1.15; 2 var peachPrice = 2.30; 3 4 var sum = 3.45; 5 alert( sum - (applePrice + peachPrice) == 0 ); 1. true 2. false 3. NaN Решение Правильный ответ: пункт 2 (false):

1 var applePrice = 1.15; 2 var peachPrice = 2.30;


3 4 var sum = 3.45; 5 alert( sum - (applePrice + peachPrice) == 0 ); // false Дело в том, что из-за ошибок округления разность является не нулём, а очень маленьким числом:

1 var applePrice = 1.15; 2 var peachPrice = 2.30; 3 4 var sum = 3.45; 5 alert( sum - (applePrice + peachPrice) ); // число [Открыть задачу в новом окне] Что выведет код ниже?

var obj = {'1': 0, 1: 1, 0: 2}; alert(obj['1']); 1. 0 2. 1 3. Ошибку Решение Правильный ответ: пункт 2 (выведет 1):

1 var obj = {'1': 0, 1: 1, 0: 2}; 2 3 alert(obj['1']); При задании объекта через {...} — кавычки не обязательны, они нужны лишь в случаях, когда без них нельзя, например:

var obj = { "строка с пробелами" : 123 } Все ключи приводятся к строке. Поэтому второй ключ равен первому и перезаписал его. [Открыть задачу в новом окне] Что выведет код ниже?


for(var key in alert(key); }

{1:1, 0:0}) {

1. 1, 0 2. 0, 1 3. Ошибку 4. Зависит от браузера Решение Правильный ответ: пункт 4 (зависит от браузера):

Браузеры IE<9, Firefox, Safari

Перебирают ключи в том же порядке, в котором свойства присваивались. Для них: 1, 0.

Opera, современный IE, Chrome

Гарантируют сохранение порядка только для строковых ключей. Численные ключи сортируются и идут до строковых. Для них: 0, 1.

Попробуйте сами:

1 for(var key in 2 alert(key); 3}

{1:1, 0:0}) {

[Открыть задачу в новом окне] Есть ли различия между проверками:

• if( x <= 100 ) • if( !(x > 100) )


1. Да, существует значение x, для которого они работают по-разному 2. Нет, они полностью взаимозаменяемы. 3. Зависит от браузера. Решение Правильный ответ: пункт 1 (есть различия): Для x = NaN (или любого другого значения, которое преобразуется к NaN):

1 var x = NaN; 2 alert( x <= 100 ); // false 3 alert( !(x > 100) ); // true [Открыть задачу в новом окне]

1 var f = function(x) { 2 alert(x) 3} 4 5 (function() { 6 f(1) 7 }()) 1. никакого результата

2. выведет 1 3. будет ошибка 4. зависит от браузера Решение Правильный ответ: 3 (ошибка). Проверьте:

1 var f = function(x) { 2 alert(x) 3 } // (*) 4 5 (function() { 6 f(1) 7 }())


Всё дело в том, что в коде отсутствует точка с запятой в месте (*). Поэтому JavaScript воспринимает его как:

var f = function(x) { alert(x) }(function() { f(1) }()) То есть, хочет использовать function() { f(1) }() как аргумент для вызова первой функции. Но что такое function() { f(1) }()? Это же вызов функции «на месте», который передаёт управление f(1). А переменная f ещё не присвоена (равна undefined), поэтому её нельзя запустить и выдаётся ошибка об этом. [Открыть задачу в новом окне] Что выведет этот код?

1 var obj = { 2 "0": 1, 3 0: 2 4 }; 5 6 alert( obj["0"] + obj[0] ); 1. 2 2. 3 3. 4 4. 12 5. ошибка Решение Ответ: пункт 3, выведет 4.

1 var obj = { 2 "0": 1, 3 0: 2 4 }; 5 6 alert( obj["0"] + obj[0] );


Дело в том, что у объектов в JavaScript ключи всегда строковые. Если в качестве ключа передано что-то ещё, то оно приводится к строке. Значения обрабатываются в порядке поступления, поэтому 0: 2 перекроет "0": 1. В итоге получится объект с единственным ключом: {"0" : 2}. При доступе к ключу — obj["0"] и obj[0] эквивалентны, опять же, поскольку ключ приводится к строке. [Открыть задачу в новом окне] Что выведет это выражение?

alert( [] + false - null + true + undefined ); А такое?

alert( [] + false - null + true ); Варианты:

1. 2. 3. 4. 5.

0, 1 NaN, 1 undefined, NaN NaN, NaN 0, NaN

Решение Ответ: 4.

1 alert( [] + false - null + true + undefined ); 2 alert( [] + false - null + true ); Шаги преобразования:

[] + false = "false", т.к массив [] преобразуется к пустой строке, выходит "" + false = "false" "false" - null = NaN, т.к. вычитание преобразует к числу, получается NaN - 0 = NaN ... дальнейшие вычисления дадут NaN [Открыть задачу в новом окне] Чему равно значение?


4 - "5" + 0xf - "1e1" 1. цифра 2. строка

3. NaN Решение Ответ: 1. Если точнее, результат равен 4:

1. Первый минус превратит строку "5" в число 5 2. Далее идёт сложение с числом 15, записанным в 16-ричной форме.

3. Далее строка содержит число, записанное в научной форме: 1e1 = 10, минус преобразует эту строку в число. Получается:

4 - 5 + 15 - 10 = 4 [Открыть задачу в новом окне] … Продолжение смотрите в части второй! ...


Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.